// PlainRemoteResource.java
// $Id: PlainRemoteResource.java,v 1.18 1997/07/16 15:33:09 ylafon Exp $
// (c) COPYRIGHT MIT and INRIA, 1997.
// Please first read the full copyright statement in file COPYRIGHT.html

package w3c.jigsaw.admin;

import java.io.*;
import java.net.*;
import java.util.*;
import java.lang.*;

import w3c.tools.store.Attribute;
import w3c.www.protocol.http.*;

public class PlainRemoteResource implements RemoteResource {
    /**
     * The client side admin context
     */
    protected AdminContext admin = null;
    /**
     * The remote resource class hierarchy.
     */
    protected String classes[] = null;
    /**
     * The remote resource set of attributes.
     */
    protected Attribute attributes[];
    /**
     * The remote resource attribute values.
     */
    protected Object values[] = null;
    /**
     * Is that resource a container resource ?
     */
    protected boolean iscontainer = false;
    /**
     * Is that resource a filtered resource ?
     */
    protected boolean isfiltered = false;
    /**
     * The name of that resource (ie it's identifier attribute).
     */
    protected String identifier = null;
    /**
     * The name of the parent of that resource, as an URL.
     */
    protected URL parent = null;
    /**
     * The admin URL for the wrapped resource.
     */
    protected URL url = null;
    /**
     * Set of attached filters.
     */
    protected RemoteResource filters[] = null;

    protected Request createRequest() {
	Request request = admin.http.createRequest();
	request.setURL(url);
	return request;
    }

    protected int lookupAttribute(String attr) 
	throws RemoteAccessException
    {
	Attribute attrs[] = getAttributes();
	for (int i = 0  ; i < attrs.length ; i++) {
	    if ( attrs[i] == null )
		continue;
	    if ( attrs[i].getName().equals(attr) )
		return i;
	}
	return -1;
    }

    protected void setFilters(RemoteResource filters[]) {
	this.isfiltered = true;
	this.filters    = filters;
    }

    /**
     * Get the target resource class hierarchy.
     * This method will return the class hierarchy as an array of String. The
     * first string in the array is the name of the resource class itself, the
     * last string will always be <em>java.lang.Object</em>.
     * @return A String array givimg the target resource's class description.
     * @exception RemoteAccessException If somenetwork failure occured.
     */

    public String[] getClassHierarchy()
	throws RemoteAccessException
    {
	return classes;
    }

    /**
     * Delete that resource, and detach it from its container.
     * @exception RemoteAccessException If somenetwork failure occured.
     */

    public void delete()
	throws RemoteAccessException
    {
	try {
	    Request req = createRequest();
	    // Prepare the request:
	    req.setMethod("DELETE-RESOURCE");
	    req.setContentType(admin.conftype);
	    // Run it:
	    Reply rep = admin.runRequest(req);
	} catch (RemoteAccessException rae) {
	    throw rae;
      	} catch (Exception ex) {
	    ex.printStackTrace();
	    throw new RemoteAccessException(ex.getMessage());
	}
    }

    /**
     * Get the target resource list of attributes.
     * This method returns the target resource attributes description. The
     * resulting array contains instances of the Attribute class, one item
     * per described attributes.
     * <p>Even though this returns all the attribute resources, only the
     * ones that are advertized as being editable can be set through this
     * interface.
     * @return An array of Attribute.
     * @exception RemoteAccessException If somenetwork failure occured.
     */

    public synchronized Attribute[] getAttributes()
	throws RemoteAccessException
    {
	if ( attributes == null ) {
	    // Load this class attributes:
	    try {
		attributes = admin.getAttributes(classes[0]);
	    } catch (InvalidResourceClass ex) {
		// Probably (?) an implementation bug...
		throw new RuntimeException("invalid class "+classes[0]);
	    }
	}
	return attributes;
    }

    /**
     * @param name The attribute whose value is to be fetched, encoded as
     * its name.
     * @exception RemoteAccessException If somenetwork failure occured.
     */

    public Object getValue(String attr)
	throws RemoteAccessException
    {
	String attrs[] = new String[1];
	attrs[0] = attr;
	return getValues(attrs)[0];
    }

    /**
     * @param attrs The (ordered) set of attributes whose value is to be
     * fetched.
     * @return An (ordered) set of values, one per queried attribute.
     * @exception RemoteAccessException If somenetwork failure occured.
     */

    public Object[] getValues(String attrs[])
	throws RemoteAccessException
    {
	// Before going out to the server, check the cache:
	// FIXME

	// Load the attribute values:
	ByteArrayOutputStream bout = new ByteArrayOutputStream();
	DataOutputStream      out  = new DataOutputStream(bout);
	try {
	    // Write out the query body:
	    for (int i = 0 ; i < attrs.length ; i++) {
		int idx = lookupAttribute(attrs[i]);
		if ( idx < 0 )
		    throw new RuntimeException("invalid attribute "+attrs[i]);
		out.writeInt(idx);
	    }
	    out.writeInt(-1);
	    out.close();
	    byte bits[] = bout.toByteArray();
	    // Write the tunnelling HTTP request:
	    Request req = createRequest();
	    req.setMethod("GET-VALUES");
	    req.setContentType(admin.conftype);
	    req.setContentLength(bits.length);
	    req.setOutputStream(new ByteArrayInputStream(bits));
	    // Run that request:
	    Reply rep = admin.runRequest(req);
	    // Parse the admin reply:
	    DataInputStream in    = new DataInputStream(rep.getInputStream());
	    int             idx   = -1;
	    Object          ret[] = new Object[attrs.length];
	    int             reti  = 0;
	    while ((idx = in.readInt()) != -1) {
		if ( idx >= 0 ) {
		    Attribute a = attributes[idx];
		    ret[reti] = a.unpickle(in);
		}
		reti++;
	    }
	    in.close();
	    return ret;
	} catch (RemoteAccessException rae) {
	    throw rae;
	} catch (Exception ex) {
	    ex.printStackTrace();
	    throw new RemoteAccessException("exception "+ex.getMessage());
	}
    }

    /**
     * @param attr The attribute to set, encoded as it's name.
     * @param value The new value for that attribute.
     * @exception RemoteAccessException If somenetwork failure occured.
     */

    public void setValue(String attr, Object value)
	throws RemoteAccessException
    {
	String attrs[] = new String[1];
	Object vals[] = new Object[1];
	attrs[0] = attr;
	vals[0] = value;
	setValues(attrs, vals);
    }

    /**
     * Set a set of attribute values in one shot.
     * This method guarantees that either all setting is done, or none of
     * them are.
     * @param attrs The (ordered) list of attribute to set, encoded as their
     * names.
     * @param values The (ordered) list of values, for each of the above
     * attributes.
     * @exception RemoteAccessException If somenetwork failure occured.
     */

    public void setValues(String names[], Object values[])
	throws RemoteAccessException
    {
	// Prepare the output:
	Attribute             attrs[] = getAttributes();
	ByteArrayOutputStream bout    = new ByteArrayOutputStream();
	DataOutputStream      out     = new DataOutputStream(bout);
	boolean               change  = false;
	String                newId   = null;
	try {
	    // Write out the query body:
	    for (int i = 0 ; i < names.length ; i++) {
		int       idx = lookupAttribute(names[i]);
		Attribute a   = attrs[idx];
		out.writeInt(idx);
		a.pickle(out, values[i]);
		if(names[i].equals("identifier")) {
		    change = true;
		    newId = (String) values[i];
		}
	    }
	    out.writeInt(-1);
  	    out.close();
	    byte bits[] = bout.toByteArray();
	    // Write the tunnelling HTTP request:
	    Request req = createRequest();
	    req.setMethod("SET-VALUES");
	    req.setContentType(admin.conftype);
	    req.setContentLength(bits.length);
	    req.setOutputStream(new ByteArrayInputStream(bits));
	    // Run that request:
	    Reply rep = admin.runRequest(req);
	} catch (RemoteAccessException rae) {
	    throw rae;
	} catch (Exception ex) {
	    ex.printStackTrace();
	    throw new RemoteAccessException("exception "+ex.getMessage());
	}
	if(change)
	    identifier = new String(newId);
	return;
    }

    /**
     * @exception RemoteAccessException If somenetwork failure occured.
     */

    public boolean isContainer()
	throws RemoteAccessException
    {
	// a Hack to avoid multiple sub-trees under the main root resource
	if(identifier != null) {
	    if(identifier.equals("root"))
		return false;
	    if(identifier.equals("control")) {
		if(classes[0].equals("w3c.jigsaw.http.ControlResource"))
		    return false;
	    }
	}
	return iscontainer;
    }

    /**
     * @exception RemoteAccessException If somenetwork failure occured.
     */

    public String[] enumerateResourceIdentifiers()
	throws RemoteAccessException
    {
	if ( ! iscontainer )
	    throw new RuntimeException("not a container");
	try {
	    // Send the request:
	    Request req = createRequest();
	    req.setMethod("ENUMERATE-IDENTIFIERS");
	    req.setContentType(admin.conftype);
	    // Get and check the reply:
	    Reply rep = admin.runRequest(req);
	    // Decode the result:
	    DataInputStream in   = new DataInputStream(rep.getInputStream());
	    Vector          vids = new Vector();
	    String          id   = null;
	    while (! (id = in.readUTF()).equals(""))
		vids.addElement(id);
	    in.close();
	    // Wrapup the result
	    String ids[] = new String[vids.size()];
	    vids.copyInto(ids);
	    return ids;
	} catch (RemoteAccessException rae) {
	    throw rae;
	} catch (Exception ex) {
	    ex.printStackTrace();
	    throw new RemoteAccessException("http "+ex.getMessage());
	}
    }

    /**
     * @exception RemoteAccessException If somenetwork failure occured.
     */

    public RemoteResource loadResource(String identifier)
	throws RemoteAccessException
    {
	try {
	    // Prepare the request:
	    Request req = createRequest();
	    req.setMethod("LOAD-RESOURCE");
	    req.setContentType(admin.conftype);
	    req.setURL(new URL(url.toString() +  identifier));
	    // Run it:
	    Reply rep = admin.runRequest(req);
	    // Decode the reply:
	    DataInputStream in  = new DataInputStream(rep.getInputStream());
	    RemoteResource  ret = admin.reader.readResource(url,identifier,in);
	    in.close();
	    return ret;
	} catch (RemoteAccessException rae) {
	    throw rae;
	} catch (Exception ex) {
	    ex.printStackTrace();
	    throw new RemoteAccessException(ex.getMessage());
	}
    }

    /**
     * Register a new resource within this container.
     * @param id The identifier of the resource to be created.
     * @param classname The name of the class of the resource to be added.
     */

    public RemoteResource registerResource(String id, String classname) 
	throws RemoteAccessException
    {
	ByteArrayOutputStream bout    = new ByteArrayOutputStream();
	DataOutputStream      out     = new DataOutputStream(bout);
	try {
	    Request req = createRequest();
	    // Prepare the request:
	    req.setMethod("REGISTER-RESOURCE");
	    req.setContentType(admin.conftype);
	    req.setURL(url);
	    out.writeUTF(id);
	    out.writeUTF(classname);
  	    out.close();
	    byte bits[] = bout.toByteArray();	    
	    req.setContentLength(bits.length);
	    req.setOutputStream(new ByteArrayInputStream(bits));
	    // Run it:
	    Reply rep = admin.runRequest(req);
	    // Decode the result:
	    DataInputStream in  = new DataInputStream(rep.getInputStream());
	    RemoteResource  ret = admin.reader.readResource(url, id, in);
	    in.close();
	    return ret;
	} catch (RemoteAccessException rae) {
	    throw rae;
	} catch (Exception ex) {
	    ex.printStackTrace();
	    throw new RemoteAccessException(ex.getMessage());
	}
    }


    /**
     * Is this resource a filtered resource ?
     * @return A boolean, <strong>true</strong> if the resource is filtered
     * and it currently has some filters attached, <strong>false</strong>
     * otherwise.
     */

    public boolean isFiltered() 
	throws RemoteAccessException
    {
	return isfiltered;
    }

    /**
     * Get the filters attached to that resource.
     * Each filter is itself a resource, so it is returned as an instance of
     * a remote resource.
     * @return A (posssibly <strong>null</strong>) array of filters attached
     * to that resource.
     */

    public RemoteResource[] getFilters()
	throws RemoteAccessException
    {
	if ( ! isfiltered )
	    throw new RuntimeException("not a filtered resource");
	return filters;
    }

    /**
     * Unregister a given filter from that resource.
     * @param filter The filter to unregister.
     */

    public void unregisterFilter(RemoteResource filter)
	throws RemoteAccessException
    {
	if ( ! isfiltered )
	    throw new RuntimeException("not a filtered resource");
	if ( filters == null )
	    throw new RuntimeException("this resource has no filters");
        // Remove it:
	ByteArrayOutputStream bout    = new ByteArrayOutputStream();
	DataOutputStream      out     = new DataOutputStream(bout);
	String id = null;
	try {
	    id = ((PlainRemoteResource)filter).identifier;
	    Request req = createRequest();
	    // Prepare the request:
	    req.setMethod("UNREGISTER-FILTER");
	    req.setContentType(admin.conftype);
	    req.setURL(url);
	    out.writeUTF((id== null) ? "" : id);
  	    out.close();
	    byte bits[] = bout.toByteArray();    
	    req.setContentLength(bits.length);
	    req.setOutputStream(new ByteArrayInputStream(bits));
	    // Run it:
	    Reply rep = admin.runRequest(req);	
	} catch (RemoteAccessException rae) {
	    throw rae;
	} catch (Exception ex) {
	    ex.printStackTrace();
	    throw new RemoteAccessException(ex.getMessage());
	}
	RemoteResource f[] = new RemoteResource[filters.length-1];
	int j = 0;
	for (int i = 0; i < filters.length ; i++) {
	    if ( ((PlainRemoteResource)filters[i]).identifier.equals(id)) {
		// got it, copy the end of the array
		System.arraycopy(filters, i+1, f, j, filters.length-i-1);
		filters = f;
		return;
	    } else {
		try {
		    f[j++] = filters[i];
		} catch (ArrayIndexOutOfBoundsException ex) {
		    return; // no modifications, return
		}
	    }
	}
    }

    /**
     * Attach a new filter to that resource.
     * @param identifier The name for this filter (if any).
     * @param clsname The name of the filter's class.
     * @return A remote handle to the (remotely) created filter instance.
     */

    public RemoteResource registerFilter(String id, String classname)
	throws RemoteAccessException
    {
	// Can we add new resources ?
	if ( ! isfiltered )
	    throw new RuntimeException("not a filtered resource");
	// Add it:
	ByteArrayOutputStream bout    = new ByteArrayOutputStream();
	DataOutputStream      out     = new DataOutputStream(bout);
	try {
	    Request req = createRequest();
	    // Prepare the request:
	    req.setMethod("REGISTER-FILTER");
	    req.setContentType(admin.conftype);
	    req.setURL(url);
	    out.writeUTF((id == null) ? "" : id);
	    out.writeUTF(classname);
  	    out.close();
	    byte bits[] = bout.toByteArray();	    
	    req.setContentLength(bits.length);
	    req.setOutputStream(new ByteArrayInputStream(bits));
	    // Run it:
	    Reply rep = admin.runRequest(req);
	    // Decode the result and add it as a filter:
	    DataInputStream in  = new DataInputStream(rep.getInputStream());
	    RemoteResource  ret = admin.reader.readResource(url, id, in);
	    // A nasty hack:
	    PlainRemoteResource plain = (PlainRemoteResource) ret;
	    plain.url = new URL(parent.toString() + 
				identifier+"?"+plain.identifier);
	    // End nasty hack
	    in.close();
	    if ( filters != null ) {
		RemoteResource nf[] = new RemoteResource[filters.length+1];
		System.arraycopy(filters, 0, nf, 0, filters.length);
		nf[filters.length] = ret;
		filters = nf;
	    } else {
		filters    = new RemoteResource[1];
		filters[0] = ret;
	    }
	    return ret;
	} catch (RemoteAccessException rae) {
	    throw rae;
	} catch (Exception ex) {
	    ex.printStackTrace();
	    throw new RemoteAccessException(ex.getMessage());
	}
	
    }
   
    /**
     * Dump that resource to the given output stream.
     * @param prt A print stream to dump to.
     */

    public void dump(PrintStream prt)
	throws RemoteAccessException
    {
	// Dump the class hierarchy:
	System.out.println("+ classes: ");
	String classes[] = getClassHierarchy();
	for (int i = 0 ; i < classes.length ; i++) 
	    System.out.println("\t"+classes[i]);
	// Dump attribute values:
	Attribute attrs[]   = getAttributes();
	Vector    query     = new Vector();
	for (int i = 0 ; i < attrs.length ; i++) {
	    if ( attrs[i] != null )
		query.addElement(attrs[i].getName());
	}
	String attnames[] = new String[query.size()];
	query.copyInto(attnames);
	// Any filters available ?
	if ( isfiltered && (filters != null) ) {
	    System.out.println("+ "+filters.length+" filters.");
	    for (int i = 0 ; i < filters.length ; i++) {
		prt.println("\t"+((PlainRemoteResource)filters[i]).identifier);
		((PlainRemoteResource) filters[i]).dump(prt);
	    }
	}
	// Run the query, and display results:
	System.out.println("+ attributes: ");
	Object values[] = getValues(attnames);
	for (int i = 0 ; i < attnames.length ; i++) {
	    Attribute a = attrs[lookupAttribute(attnames[i])];
	    if ( values[i] != null )
		prt.println("\t"+a.getName()+"="+a.stringify(values[i]));
	    else 
		prt.println("\t"+a.getName()+" <undef>");
	}
    }

    PlainRemoteResource(AdminContext admin, String classes[]) {
	this(admin, null, null, classes);
    }
		
    PlainRemoteResource(AdminContext admin
			, URL parent, String id
			, String classes[]) {
	this(admin, parent, id, null, classes);
    }

    PlainRemoteResource(AdminContext admin
			, URL parent
			, String identifier
			, URL url
			, String classes[]) {
	this.admin      = admin;
	this.identifier = identifier;
	this.parent     = parent;
	this.classes    = classes;
	this.attributes = null;
	this.values     = null;
	// That's how we do it (!) - could make better
	// [container implies filtered => break;]
	for (int i = 0 ; i < classes.length ; i++) {
	    if (classes[i].equals("w3c.tools.store.ContainerInterface")) 
		iscontainer = true;
	    if (classes[i].equals("w3c.jigsaw.resources.FilteredResource"))
		isfiltered = true;
	}
	// Build up that resource URL:
	try {
	    if ( url != null ) {
		this.url = url;
	    } else if (parent != null) {
		String urlpart = iscontainer ? identifier+"/" : identifier;
		this.url = ((identifier != null)
			    ? new URL(parent.toString() + urlpart)
			    : parent);
	    } else {
		this.url = null;
	    }
	} catch (MalformedURLException ex) {
	    // Again, an implementation bug:
	    throw new RuntimeException("invalid server URL");
	}
    }
}

