org.eclipse.swordfish.p2.internal.deploy.server.MetadataProcessor.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.swordfish.p2.internal.deploy.server.MetadataProcessor.java

Source

/*******************************************************************************
 * Copyright (c) 2010 SOPERA GmbH.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 * 
 * Contributors:
 *     SOPERA GmbH - initial API and implementation
 *******************************************************************************/
package org.eclipse.swordfish.p2.internal.deploy.server;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

/**
 * Implementation of a processor that adds P2 touchpoint instructions to a metadata repository 
 * to mark bundles for startup during touchpoint configure phase and remove the mark during unconfigure phase.
 * @author jkindler
 */
public class MetadataProcessor implements IMetadataProcessor {
    private static final Log LOG = LogFactory.getLog(MetadataProcessor.class);
    private static final String CR = System.getProperty("line.separator");

    static final String MARK_BUNDLE_STARTUP = "markStarted(started: true);";
    static final String UNMARK_BUNDLE_STARTUP = "markStarted(started: false);";

    /**
     * We are only interested in bundles that are not fragments!
     */
    private static final String QUERY_ALL_BUNDLES = "//unit["
            + "(count(provides/provided[@namespace='osgi.bundle']) = 1) and "
            + "(count(provides/provided[@namespace='osgi.fragment']) = 0)]";

    private static final String QUERY_INSTRUCTIONS = "touchpointData/instructions/instruction[@key='";
    private static final String PHASE_CONFIGURE = "configure";
    private static final String PHASE_UNCONFIGURE = "unconfigure";
    private static final String CLOSE_EXPR = "']";
    private static final String KEY = "key";
    private static final String INSTRUCTION = "instruction";
    private static final String INSTRUCTIONS = INSTRUCTION + "s";
    private static final String TOUCHPOINT_DATA = "touchpointData";
    private static final String SIZE = "size";

    private static final String CONTENT_XML = "content.xml";
    private static final String CONTENT_JAR = "content.jar";

    private DocumentBuilderFactory docBuilderFactory;
    private NodeList emptyList;

    public MetadataProcessor() {
        this.docBuilderFactory = DocumentBuilderFactory.newInstance();
    }

    /* (non-Javadoc)
     * @see org.eclipse.swordfish.p2.internal.deploy.server.IMetadataProcessor#updateMetaData(java.io.File)
     */
    public void updateMetaData(File metadata) throws IOException {
        File repoFile = getInputFile(metadata);

        LOG.info("Adding instructions to metadata");
        Document processed = addInstructions(getInputStream(repoFile));

        LOG.info("Write modified metadata to disk");
        saveDocument(processed, repoFile);

        LOG.info("Finished update of metadata");
    }

    /**
     * Save the metadata document
     * 
     * NOTE: Package visibility to enable unit testing only!
     * 
     * @param docToSave - the document
     * @param metadataFile - the file to save to
     * @throws IOException - in case of IO errors
     */
    final void saveDocument(Document docToSave, File metadataFile) throws IOException {
        if (isXml(metadataFile)) {
            saveXml(docToSave, metadataFile);

        } else if (isJar(metadataFile)) {
            saveJar(docToSave, metadataFile);
        }
    }

    /**
     * Creates an input stream from a repository file.
     * Handles files that end with content.xml or content.jar (with content.xml file embedded!)
     * 
     * NOTE: Package visibility to enable unit testing only!
     * 
     * @param inputFile - the file to get the input stream from
     * @return an input stream
     * @throws IOException - in case of IO problems
     */
    final InputStream getInputStream(File inputFile) throws IOException {
        InputStream is = null;

        if (isXml(inputFile)) {
            is = new FileInputStream(inputFile);

        } else if (isJar(inputFile)) {
            ZipInputStream zin = new ZipInputStream(new FileInputStream(inputFile));
            ZipEntry zentry = zin.getNextEntry();

            if (zentry.getName().endsWith(CONTENT_XML)) {
                is = zin;
            } else {
                throw new IOException("Invalid metadata repository " + inputFile.getPath());
            }
        } else {
            throw new IllegalArgumentException("The file " + inputFile + " is invalid");
        }

        return is;
    }

    /**
     * Add instructions to startup bundles to a metadata file.
     * 
     * NOTE: Package visibility to enable unit testing only!
     * 
     * @param metaDataStream - an input stream to parse to a document
     * @return a meta data document with all bundles started
     * @throws IOException - in case of read / parse errors
     */
    final Document addInstructions(InputStream metaDataStream) throws IOException {
        Document metadataDoc = inputStreamToDocument(metaDataStream);
        NodeList bundleNodes = findAllBundles(metadataDoc);

        for (int entry = 0; entry < bundleNodes.getLength(); entry++) {
            Node bundle = bundleNodes.item(entry);

            if (needsStartup(bundle)) {
                addBundleStart(bundle);
            }

            if (needsShutdown(bundle)) {
                addBundleStop(bundle);
            }
        }

        return metadataDoc;
    }

    /**
     * Convert an input stream to a (DOM) document
     * 
     * NOTE: Package visibility to enable unit testing only!
     * 
     * @param is - the stream to read
     * @return a DOM
     * @throws IOException - in case of IO, parser or sax problems
     */
    final Document inputStreamToDocument(InputStream is) throws IOException {
        Document doc = null;

        try {
            doc = this.docBuilderFactory.newDocumentBuilder().parse(is);
        } catch (ParserConfigurationException e) {
            logAndRethrowAsIOException("Problems with XML parser configuration", e);

        } catch (SAXException e) {
            logAndRethrowAsIOException("Problems with XML parser", e);

        } finally {
            is.close();
        }

        return doc;
    }

    /**
     * Transform a document to a string
     * 
     * NOTE: Package visibility to enable unit testing only!
     * 
     * @param doc - the document to be transformed
     * @param isSimpleOutput - if true, omits xml declaration, otherwise indents nicely
     * @return the resulting string (null in case of an error)
     * @throws IOException 
     */
    final String documentToString(Document doc, boolean isSimpleOutput) throws IOException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        document2OutputStream(doc, isSimpleOutput, bos);
        return new String(bos.toByteArray(), "UTF-8");
    }

    /**
     * Write a document to an output stream
     * 
     * NOTE: Package visibility to enable unit testing only!
     * 
     * @param doc - the document
     * @param isSimpleOutput - false = no idention
     * @param sink - the out put stream to write to
     * @throws IOException - on write errors.
     */
    final void document2OutputStream(Document doc, boolean isSimpleOutput, OutputStream sink) throws IOException {
        TransformerFactory tf = TransformerFactory.newInstance();
        Transformer t;

        try {
            t = tf.newTransformer();
            t.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");

            if (!isSimpleOutput) {
                t.setOutputProperty(OutputKeys.METHOD, "xml");
                t.setOutputProperty(OutputKeys.INDENT, "yes");
            }

            StreamResult sr = new StreamResult(sink);
            t.transform(new DOMSource(doc), sr);

        } catch (TransformerException e) {
            logAndRethrowAsIOException("Error serializing document", e);
        }

    }

    /**
     * Find all unit nodes that are non-fragment bundles
     * 
     * NOTE: Package visibility to enable unit testing only!
     * 
     * @param root - the document root of the 
     * @return a list of nodes that represent non-fragment bundles
     */
    final NodeList findAllBundles(Node root) {
        return query(root, QUERY_ALL_BUNDLES);
    }

    /**
     * Check if this unit is needs to be marked for starting
     * 
     * NOTE: Package visibility to enable unit testing only!
     * 
     * @param item - the unit node of a non-fragment bundle
     * @return false if the unit has a markStart instruction in configure phase, otherwise false
     */
    final boolean needsStartup(Node item) {
        return !isMatchingRequirement(item, PHASE_CONFIGURE, MARK_BUNDLE_STARTUP);
    }

    /**
     * Check if this unit is needs a shutdown
     * 
     * NOTE: Package visibility to enable unit testing only!
     * 
     * @param item - the unit node of a non-fragment bundle
     * @return false if the unit has a shutdown instruction in unconfigure phase, otherwise false
     */
    final boolean needsShutdown(Node item) {
        return !isMatchingRequirement(item, PHASE_UNCONFIGURE, UNMARK_BUNDLE_STARTUP);
    }

    /**
     * Add an instruction that marks the bundle for startup
     * 
     * NOTE: Package visibility to enable unit testing only!
     * 
     * @param bundle - the bundle to receive a startup marker
     * @return the modified node
     */
    final Node addBundleStart(Node bundle) {
        return addInstruction(bundle, PHASE_CONFIGURE, MARK_BUNDLE_STARTUP);
    }

    /**
     * Add an instruction that marks the bundle to be stopped
     * 
     * NOTE: Package visibility to enable unit testing only!
     * 
     * @param bundle - the bundle to receive a stop marker
     * @return the modified node
     */
    final Node addBundleStop(Node bundle) {
        return addInstruction(bundle, PHASE_UNCONFIGURE, UNMARK_BUNDLE_STARTUP);
    }

    /**
     * Determine repository file in case we get a directory instead of a full file path.
     * In that case we first look for a content.jar file, then for content.xml
     * 
     * @param metadata - a file pointing to a repository - possibly only to a directory that contains it.
     * @return a full file path
     * @throws IOException - in case no meta data repository file can be found.
     */
    private final File getInputFile(File metadata) throws IOException {
        File repo = metadata;
        boolean isRepositoryFound = false;

        if (metadata.isDirectory()) {
            String[] repoList = new String[] { CONTENT_JAR, CONTENT_XML };

            for (int i = 0; i < repoList.length && !isRepositoryFound; i++) {
                repo = new File(metadata.getPath() + File.separator + repoList[i]);
                isRepositoryFound = repo.exists();
                LOG.info("Repository @ " + repo.getPath() + " => " + isRepositoryFound);
            }

            if (!isRepositoryFound) {
                String message = "No repository found in directory " + metadata;
                LOG.error(message);
                throw new IOException(message);
            }
        }
        return repo;
    }

    /**
     * Save meta data to a jar file
     * @param metadataDoc - the document to be saved
     * @param metadataFile - the file to be saved to
     * @throws IOException - In case of IO problems
     */
    private final void saveJar(Document metadataDoc, File metadataFile) throws IOException {
        ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(metadataFile));

        try {
            ZipEntry contentEntry = new ZipEntry(CONTENT_XML);
            zos.putNextEntry(contentEntry);
            document2OutputStream(metadataDoc, false, zos);

        } catch (IOException iox) {
            LOG.error("Error saving meta data to " + metadataFile);
            throw iox;

        } finally {
            try {
                zos.closeEntry();
            } catch (IOException iox) {
                // oops ...  seems like the error occurred before/while the entry was created
            }

            zos.close();
        }
    }

    /**
     * Save meta data to a jar file
     * @param metadataDoc - the document to be saved
     * @param metadataFile - the file to be saved to
     * @throws IOException - In case of IO problems
     */
    private final void saveXml(Document metadataDoc, File metadataFile) throws IOException {
        FileOutputStream fos = new FileOutputStream(metadataFile);

        try {
            document2OutputStream(metadataDoc, false, fos);

        } catch (IOException iox) {
            LOG.error("Error saving meta data to " + metadataFile);
            throw iox;

        } finally {
            fos.close();
        }
    }

    /**
     * Check if a certain instruction is already present in a phase
     * @param item - the bundle node to be checked
     * @param phase - the phase which should be checked
     * @param expectedInstr - the instruction that is expected to be present
     * @return true, if instruction was found in the phase, otherwise false.
     */
    private final boolean isMatchingRequirement(Node item, String phase, String expectedInstr) {
        boolean matchesReq = false;
        NodeList instructions = query(item, QUERY_INSTRUCTIONS + phase + CLOSE_EXPR);

        if (instructions.getLength() == 0) {
            return matchesReq;

        } else {
            for (int in = 0; in < instructions.getLength() && !matchesReq; in++) {
                Node instruction = instructions.item(in);
                Node instrTextNode = getTextNode(instruction);

                if (instrTextNode != null) {
                    String instr = instrTextNode.getNodeValue();
                    instr = instr.replaceAll(" ", "").replaceAll(CR, "");
                    matchesReq = (instr.contains(expectedInstr.replaceAll(" ", "")));
                }
            }
        }

        return matchesReq;
    }

    /**
     * Add an instruction to the bundle IU metadata
     * This creates all possibly missing nodes and appends the instruction to the
     * node designated for the phase.
     *  
     * @param bundle - the bundle node
     * @param phase - the phase where the instruction should be added.
     * @param instructionCode - the instruction to be added
     * @return the modified bundle node
     */
    private final Node addInstruction(Node bundle, String phase, String instructionCode) {
        Element touchPointDataElem = getOrCreateChild(bundle, TOUCHPOINT_DATA);
        Element instructionsElem = getOrCreateChild(touchPointDataElem, INSTRUCTIONS);

        String instructionQuery = INSTRUCTION + "[@" + KEY + " = '" + phase + "']";
        NodeList instrucList = query(instructionsElem, instructionQuery);

        Element instruction;

        if (instrucList.getLength() == 0) {
            instruction = appendChild(instructionsElem, INSTRUCTION);
            instruction.setAttribute(KEY, phase);
            instruction.appendChild(instruction.getOwnerDocument().createTextNode(""));
        } else {
            instruction = (Element) instrucList.item(0);
        }

        Node instructionTextNode = getTextNode(instruction);

        if (instructionTextNode == null) {
            instructionTextNode = instruction.appendChild(instruction.getOwnerDocument().createTextNode(""));
        }

        String val = instructionTextNode.getNodeValue();

        if ("".equals(val) || val == null) {
            instructionTextNode.setNodeValue(instructionCode);
        } else {
            String delimiter = val.replaceAll(" ", "").replaceAll(CR, "").endsWith(";") ? "" : ";";
            instructionTextNode.setNodeValue(val + delimiter + instructionCode);
        }

        int iCounter = query(instructionsElem, INSTRUCTION).getLength();
        instructionsElem.setAttribute(SIZE, "" + iCounter);

        int tdCounter = query(touchPointDataElem, INSTRUCTIONS).getLength();
        touchPointDataElem.setAttribute(SIZE, "" + tdCounter);
        return bundle;
    }

    /**
     * Get the first text node child of a node
     * @param parent - the enclosing parent
     * @return the text node
     */
    private final Node getTextNode(Node parent) {
        NodeList childs = parent.getChildNodes();
        Node result = null;

        for (int i = 0; i < childs.getLength() && result == null; i++) {
            if (childs.item(i).getNodeType() == Node.TEXT_NODE) {
                result = childs.item(i);
            }
        }

        return result;
    }

    /**
     * Find a child node below a parent - if not found, add it to the parent
     * @param parent - the parent node
     * @param childName - the name of the child node to be found or added
     * @return the child added as Element
     */
    private final Element getOrCreateChild(Node parent, String childName) {
        NodeList childNodes = query(parent, childName);
        Element childElem;

        if (childNodes.getLength() == 0) {
            childElem = appendChild(parent, childName);
        } else {
            childElem = (Element) childNodes.item(0);
        }

        return childElem;
    }

    /**
     * Append a child node to an owner node
     * @param parent - the parent node
     * @param childName - the name of the child node
     * @return the child node that was added
     */
    private final Element appendChild(Node parent, String childName) {
        Element e = parent.getOwnerDocument().createElement(childName);
        parent.appendChild(e);
        return e;
    }

    /**
     * Get a list of nodes below a root item that match an XPath query
     * @param item - the root item
     * @param queryString - the XPath query string
     * @return a list of matching nodes of an empty list
     */
    private final NodeList query(Node item, String queryString) {
        NodeList result = null;

        try {
            result = getEmptyList();
            result = (NodeList) createQuery(queryString).evaluate(item, XPathConstants.NODESET);

        } catch (Exception e) {
            LOG.warn("Query failed: " + queryString, e);
        }
        return result;
    }

    /**
     * Create an XPath query from a string 
     * @param expression - the XPath string to be compiled
     * @return a compiled XPath expression
     * @throws XPathExpressionException
     */
    private final XPathExpression createQuery(String expression) throws XPathExpressionException {
        XPathFactory factory = XPathFactory.newInstance();
        XPath xpath = factory.newXPath();
        XPathExpression expr = xpath.compile(expression);
        return expr;
    }

    /**
     * @return an empty node list as a default result
     * @throws ParserConfigurationException
     */
    private final NodeList getEmptyList() throws ParserConfigurationException {
        if (this.emptyList == null) {
            this.emptyList = this.docBuilderFactory.newDocumentBuilder().newDocument().getChildNodes();
        }

        return this.emptyList;
    }

    /**
     * Handle logging and rethrowing of exceptions
     * @param message - the message to be logged
     * @param ex - the original exception.
     * @throws IOException - the rethrown exception
     */
    private void logAndRethrowAsIOException(String message, Exception ex) throws IOException {
        LOG.error(message, ex);
        throw new IOException(message + ": " + ex);
    }

    /**
     * Determine by file name if we have a repository in xml format
     * @param inputFile - the file to check
     * @return true if a content.xml is found
     */
    private final boolean isXml(File inputFile) {
        return inputFile.getName().endsWith(CONTENT_XML);
    }

    /**
     * Determine by file name if we have a repository in jar format (with xml inside the jar)
     * @param inputFile - the file to check
     * @return true if a content.jar is found
     */
    private final boolean isJar(File inputFile) {
        return inputFile.getName().endsWith(CONTENT_JAR);
    }
}