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