Chapter 13. Implementing a package installer

While the core set of installer services take care of the vast majority of scenarios (and virtually anything can be done using the InstallScript feature), you may find situations that are best solved by implementing a custom installer. Besides extending install-time behavior, implementing a custom installer allows you to implement custom dependency types.

Dependency Expansion

A dependency is represented, in a package description, as a <dep /> element inside a <requires /> or <conflicts /> element (conflicts are structurally and operationally the same as dependencies, except that instead of specifying a package that is required to be present, they specify a package that is required NOT to be present). Usually these dependencies are written by a package maintainer in the package description template file package-in.xml . Maintainers specify dependencies using an XML structure, but in order to manipulate them the Package Manager requires them to be translated into an XPath string. This process, performed at build time, is called Dependency Expansion.

Example Dependency in package-in.xml file:


<dep name="libhttps-native" predepends="true"
     ns='http://www.xoe.org/installer/base/package'/>

Expanded form:

<dep name="libhttps-native" predepends="true"
     ns="http://www.xoe.org/installer/base/package"
     xpath="self::proviso[name'libhttps-native']"/>

Install-Time Extension

Every time a package is installed or uninstalled, all Installer Services currently registered on the system are invoked and given a chance to perform some operation. In some situations, it may be necessary for one installer to be called before or after another. For example, if your installer needs to instantiate classes from the package, it must be called after the ClassInstaller and NativeInstaller have been invoked. This is specified in the implementation of the function IPackageXMLHandler.getHandlerPredependencies (). When uninstalling a package, the order of invocation is reversed.

Namespace

Every installer is identified by a unique namespace. Dependencies and provisos specify the a namespace as well. When processing provisos or dependencies, the package installer looks for an installer whose namespace 'contains' the namespace of the proviso/dependency.

Namespace A contains namespace B if:


  B == A

    -- or --

  B.startsWith (A + "/")

Note that trailing slashes are removed from A and B before the test is performed. If multiple installers match a proviso/dependency, the most specific one, that is, the one with the longer namespace, is used.

Installers published by Transvirtual have namespaces starting with "http://www.xoe.org/". Your organization should come up with a unique naming scheme in order to avoid name collisions.

org.xoe.core.packages.IPackageXMLHandler

The IPackageXMLHandler service definition provides a mechanism for extending the package installation process and defining custom dependency types. It also declares functions for use in the build process but these may not be supported in future releases and their use is discouraged.

While the following functions must be implemented in order to implement the interface, it's best not to rely on them being called:

  
  public void setPackage (XoePackage p) throws PackageException;
  public Vector generateRequires  () throws PackageException;
  public Vector generateConflicts () throws PackageException;
  public Vector generateProvides  () throws PackageException;

A good implementation would be as follows:


  public void setPackage (XoePackage p)
  { return;  }
  public Vector generateRequires ()
  { return null;  }
  public Vector generateConflicts ()
  { return null;  }
  public Vector generateProvides ()
  { return null;  }

The remaining functions in IPackageXMLHandler are:


  public Dependency buildDependency (Element depEl)
    throws PackageDescriptionException;

  public String getNamespaceURI ();

  public String[] getHandlerPredependencies ();

  public void verify (XoePackage xoePackage, Proviso[] provisos)
    throws PackageException;

  public void install (XoePackage xoePackage, Proviso[] provisos)
    throws InstallException;

  public void uninstall (XoePackage xoePackage, Proviso[] provisos);

Also, don't forget that it is necessary to implement the IService functions as well. For getFunctionality (), an installer should return IPackageXMLHandler.FUNCTIONALITY. An installer must also have a feature with the name IPackageXMLHander.FEATURE_NAMESPACE and the installer's namespace as value.

The most basic functions are getNamespaceURI () and getHandlerPredependencies (). The first should return the namespace of your installer. The second should return an array of strings representing the namespaces of installers that must be called before yours in the install process (and after in the uninstall process).

Implementing buildDependency ()

If your installer does not provide support for custom dependency types, this is easy:


  public Dependency buildDependency (Element depEl)
    throws PackageDescriptionException
  {
    throw new PackageDescriptionException ("Unrecognized dependency type:\n" + XMLWriter.getXML (depEl));
  }

For the other case, it's recommended to subclass Dependency. Here's how the ServiceInstaller does it:


  public Dependency buildDependency (Element depEl)
    throws PackageDescriptionException
  {
    return new ServiceDependency (depEl);
  }

ServiceDependency is written as follows:


public class ServiceDependency 
  extends Dependency
{
  String m_func = null;
  String m_iface = null;
  String m_features[][] = null;

  public ServiceDependency (Element depEl)
    throws PackageDescriptionException
  {
    super (depEl);

    Vector vFeats = new Vector();

    NodeList childNodes = depEl.getChildNodes ();

    for (int i = 0; childNodes != null && i < childNodes.getLength (); i++)
      {
	Node n = childNodes.item(i);

	if ("functionality".equals (n.getNodeName ()))
	  {
	    Node t = childNodes.item (i).getFirstChild ();
	    if(t.getNodeType () == Node.TEXT_NODE)
	      {
		m_func = ( (Text)t).getData ();
	      }
	  }
	else if ("implements".equals (n.getNodeName ()))
	  {
	    Node t = childNodes.item (i).getFirstChild ();
	    if(t.getNodeType () == Node.TEXT_NODE)
	      {
		m_iface = ( (Text)t).getData ();
	      }
	  }
	else if ("feature".equals (n.getNodeName ()) && n.getNodeType() == Node.ELEMENT_NODE)
	  {
	    String feat[] = {((Element)n).getAttribute ("name"), ((Element)n).getAttribute ("value")};
	    vFeats.addElement (feat);
	  }
      }

    m_features = (String[][])vFeats.toArray(new String[vFeats.size ()][]);
    if(m_func == null || m_iface == null)
      {
	throw new PackageDescriptionException ("Invalid Service Dependency:\n" + XMLWriter.getXML (depEl));
      }
    setXPath(toXPath(m_func, m_iface, m_features));
  }

  public Element getDepElement (Document ownerdoc)
  {
    Element depEl = super.getDepElement (ownerdoc);
    Element funcEl = ownerdoc.createElement ("functionality");
    Text func = ownerdoc.createTextNode (m_func);
    Element ifaceEl = ownerdoc.createElement ("implements");
    Text iface = ownerdoc.createTextNode (m_iface);
    funcEl.appendChild (func);
    ifaceEl.appendChild (iface);

    depEl.appendChild (funcEl);
    depEl.appendChild (ifaceEl);

    for(int i = 0; i < m_features.length; i++)
      {
	Element featEl = ownerdoc.createElement ("feature");
	featEl.setAttribute ("name", m_features[i][0]);
	featEl.setAttribute ("value", m_features[i][1]);
	depEl.appendChild (featEl);
      }

    return depEl;
  }

  static String toXPath (String functionality, String iface, String[][] features)
  {
    StringBuffer sxpath = new StringBuffer ();

    if (functionality != null || iface != null || features != null)
      {
	if (functionality != null)
	  {
	    sxpath.append ("functionality='" + functionality + "'");
	  }
	if (iface != null)
	  {
	    if (functionality != null)
	      sxpath.append (" and ");
	    
	    sxpath.append ("implements='" + iface + "'");
	  }
	if (features != null && features.length > 0)
	  {
	    if (functionality != null || iface != null )
	      sxpath.append (" and ");
	    
	    for (int i = 0; i < features.length; i++)
	      {
		sxpath.append ("feature[@name = '" + features[i][0] + 
			               "' and @value = '" + features[i][1] + "']");
		if (i+1 < features.length)
		  sxpath.append (" and ");
	      }
	  }
      }

    return sxpath.toString ();
  }
}

In the constructor, an Element is passed in and inspected. super (Element) is called to allow Dependency to parse the common attributes. Finally, before returning, setXPath () is called.

getDepElement () is responsible for returning an expanded version of the 'dep' element. In this case, we again call the superclass to get the basic behavior, and then perform the ServiceDependency specific tasks.

Implementing install () and uninstall ()

Let's examine the implementation of install () in the ServiceInstaller. This function looks for a file named services.xml inside the package's working directory. If found, the file is expected to list the class names of services to be registered, and optional configuration files to be used. Each service is instantiated, registered, and then stored in the XoePackage object for later retrieval.


  public void install (XoePackage xoePackage, Proviso[] provisos)
    throws InstallException
  {
    Vector providers = new Vector();
    try
      {
	ContentElement ce = Stash.fetch (new URL (Stash.getWorkingDirectory (xoePackage), SERVICES_FILENAME));

	if (ce != null)
	  {
	    Document doc = DOMlet.create (ce.getInputStream (), SERVICES_FILENAME);
	    
	    NodeList list = XPath.findNodes (doc, "services/service");
	    
	    for (int i = 0; i < list.getLength (); i++) 
	      {
		Element e = (Element) list.item (i);
		providers.addElement (installService (xoePackage, e.getAttribute ("class"), e.getAttribute ("config")));
	      }
	  }
	xoePackage.setExData (NAMESPACE,
			      new ServiceInstallerData ( (IService[])providers.toArray (new IService[providers.size()])));
      }
    catch (Throwable ex)
      {
	// uninstall any successfully installed services
	for (int i = 0; i < providers.size (); i++)
	  {
	    try
	      {
		ServiceLocator.unregisterService ((IService)providers.elementAt (i));
	      }
	    catch (Throwable t)
	      {
		Logger.log (this, "WARNING: While aborting installation of " + xoePackage
			    + " failed to uninstall a service.");
		Logger.log (this, t);
	      }
	  }
	throw new InstallException (ex, xoePackage,
				    ("An error occurred while searching for " +
				     SERVICES_FILENAME));
      }
  }

Notice the use of XoePackage.setExData (String, Object) to store the list of registered services (using a ServiceInstallerData object). This list is used at uninstall time to unregister all the services, as you can see in this implementation of uninstall ():


  public void uninstall (XoePackage xoePackage, Proviso[] provisos)
  {
    IService[] providers = ((ServiceInstallerData)xoePackage.getExData (NAMESPACE)).getInstalledServices ();
    for (int i = 0; i < providers.length; i++)
      {
	ServiceLocator.unregisterService (providers[i]);
      }
    xoePackage.setExData (NAMESPACE, null);
  }

Implementing verify ()

The core installers do not use the verify () function since it is usually safe to allow installation to fail and it's therefore unnecessary to slow down package installation with needless work. If your installer performs some task whose failure could be harmful, however, the verify () function can be used to abort installation before it starts by throwing a PackageException.