// PlainRemoteResource.java
// $Id: PlainRemoteResource.java,v 1.20 1998/01/23 10:08:22 bmahe Exp $
// (c) COPYRIGHT MIT and INRIA, 1997.
// Please first read the full copyright statement in file COPYRIGHT.html

package org.w3c.jigsaw.admin;

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

import org.w3c.tools.resources.Attribute;
import org.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 directory resource ?
   */
  protected boolean isDirectoryResource = false;
 /**
   * Is that resource a framed resource ?
   */
  protected boolean isframed = 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 frames.
   */
  protected RemoteResource frames[] = 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 setFrames(RemoteResource frames[]) {
    this.isframed  = true;
    this.frames    = frames;
  }

  /**
   * 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;
  }

  /**
   * Reindex the resource's children if this resource is a DirectoryResource.
   * @exception RemoteAccessException If it's not a DirectoryResource
   */
  public void reindex()
    throws RemoteAccessException
  {
    if (isDirectoryResource()) {
      try {
	Request req = createRequest();
	// Prepare the request:
	req.setMethod("REINDEX-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());
      }
    } else {
      throw new RemoteAccessException("Error, can't reindex! this is not"+
				      " a DirectoryResource.");
    }
  }

  /**
   * 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);
      try {
	//if (parent != null) {
	if (! isFrame()) {
	  if (iscontainer)
	    url = new URL(parent, identifier+"/");
	  else 
	    url = new URL(parent, identifier);
	  // update frames url
	  updateURL(new URL(parent, identifier));
	} else {
	  String oldFile = url.getFile();
	  int index = oldFile.lastIndexOf('?');
	  String newFile = oldFile.substring(0, index);
	  updateURL(new URL(url, newFile));
	}
      } catch (MalformedURLException ex) {
	ex.printStackTrace();
      }
    }
    return;
  }

  public void updateURL(URL parentURL) {
    if (! isFrame())
      this.parent = parentURL;
    else {
      try {
	url = new URL(parentURL, parentURL.getFile()+"?"+identifier);
      } catch (MalformedURLException ex) {
	return;
      }
    }
    //update frames URLs
    if (frames != null) {
      for(int i=0 ; i < frames.length ; i++) {
	frames[i].updateURL(url);
      }
    }
    //update sons ??? no! 
  }

  /**
   * @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("org.w3c.jigsaw.http.ControlResource"))
	  return false;
      }
    }
    return iscontainer;
  }

  public boolean isDirectoryResource()
    throws RemoteAccessException
  {
    if(identifier != null) {
      if(identifier.equals("root"))
	return false;
    }
    return isDirectoryResource;
  }

  /**
   * @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 framed resource ?
   * @return A boolean, <strong>true</strong> if the resource is framed
   * and it currently has some frames attached, <strong>false</strong>
   * otherwise.
   */

  public boolean isFramed() 
    throws RemoteAccessException
  {
    return isframed;
  }

  /**
   * Get the frames attached to that resource.
   * Each frame is itself a resource, so it is returned as an instance of
   * a remote resource.
   * @return A (posssibly <strong>null</strong>) array of frames attached
   * to that resource.
   */
  
  public RemoteResource[] getFrames()
    throws RemoteAccessException
  {
    if ( ! isframed )
      throw new RuntimeException("not a framed resource");
    return frames;
  }

  /**
   * Unregister a given frame from that resource.
   * @param frame The frame to unregister.
   */
  
  public void unregisterFrame(RemoteResource frame)
    throws RemoteAccessException
  {
    if ( ! isframed )
      throw new RuntimeException("not a framed resource");
    if ( frames == null )
      throw new RuntimeException("this resource has no frames");
    // Remove it:
    ByteArrayOutputStream bout    = new ByteArrayOutputStream();
    DataOutputStream      out     = new DataOutputStream(bout);
    String id = null;
    try {
      id = ((PlainRemoteResource)frame).identifier;
      Request req = createRequest();
      // Prepare the request:
      req.setMethod("UNREGISTER-FRAME");
      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[frames.length-1];
    int j = 0;
    for (int i = 0; i < frames.length ; i++) {
      if ( ((PlainRemoteResource)frames[i]).identifier.equals(id)) {
	// got it, copy the end of the array
	System.arraycopy(frames, i+1, f, j, frames.length-i-1);
	frames = f;
	return;
      } else {
	try {
	  f[j++] = frames[i];
	} catch (ArrayIndexOutOfBoundsException ex) {
	  return; // no modifications, return
	}
      }
    }
  }

  public boolean isFrame() {
    return isFrameURL(url);
  }

  protected boolean isFrameURL(URL furl) {
    return (furl.toString().lastIndexOf('?') != -1);
  }

  /**
   * Attach a new frame to that resource.
   * @param identifier The name for this frame (if any).
   * @param clsname The name of the frame's class.
   * @return A remote handle to the (remotely) created frame instance.
   */
  
  public RemoteResource registerFrame(String id, String classname)
    throws RemoteAccessException
  {
    // Can we add new resources ?
    if ( ! isframed )
      throw new RuntimeException("not a framed resource");
    // Add it:
    ByteArrayOutputStream bout    = new ByteArrayOutputStream();
    DataOutputStream      out     = new DataOutputStream(bout);
    try {
      Request req = createRequest();
      // Prepare the request:
      req.setMethod("REGISTER-FRAME");
      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 frame:
      DataInputStream in  = new DataInputStream(rep.getInputStream());
      RemoteResource  ret = admin.reader.readResource(url, id, in);
      // A nasty hack:
      PlainRemoteResource plain = (PlainRemoteResource) ret;

      //if (parent == null) {
      if (isFrame()) {
	plain.url = new URL(url, url.getFile()+"?" + plain.identifier);
      } else {
	plain.url = new URL(parent.toString() + 
			    identifier+"?"+plain.identifier);
      }
      // End nasty hack
      in.close();
      if ( frames != null ) {
	RemoteResource nf[] = new RemoteResource[frames.length+1];
	System.arraycopy(frames, 0, nf, 0, frames.length);
	nf[frames.length] = ret;
	frames = nf;
      } else {
	frames    = new RemoteResource[1];
	frames[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 frames available ?
    if ( isframed && (frames != null) ) {
      System.out.println("+ "+frames.length+" frames.");
      for (int i = 0 ; i < frames.length ; i++) {
	prt.println("\t"+((PlainRemoteResource)frames[i]).identifier);
	((PlainRemoteResource) frames[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("org.w3c.tools.resources.ContainerInterface")) 
	iscontainer = true;
      if (classes[i].equals("org.w3c.tools.resources.FramedResource"))
	isframed = true;
      if (classes[i].equals("org.w3c.tools.resources.DirectoryResource"))
	isDirectoryResource = 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");
    }
  }
}

