/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright (c) 1997-2012 Oracle and/or its affiliates. All rights reserved. * * The contents of this file are subject to the terms of either the GNU * General Public License Version 2 only ("GPL") or the Common Development * and Distribution License("CDDL") (collectively, the "License"). You * may not use this file except in compliance with the License. You can * obtain a copy of the License at * https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html * or packager/legal/LICENSE.txt. See the License for the specific * language governing permissions and limitations under the License. * * When distributing the software, include this License Header Notice in each * file and include the License file at packager/legal/LICENSE.txt. * * GPL Classpath Exception: * Oracle designates this particular file as subject to the "Classpath" * exception as provided by Oracle in the GPL Version 2 section of the License * file that accompanied this code. * * Modifications: * If applicable, add the following below the License Header, with the fields * enclosed by brackets [] replaced by your own identifying information: * "Portions Copyright [year] [name of copyright owner]" * * Contributor(s): * If you wish your version of this file to be governed by only the CDDL or * only the GPL Version 2, indicate your decision by adding "[Contributor] * elects to include this software in this distribution under the [CDDL or GPL * Version 2] license." If you don't indicate a single choice of license, a * recipient has the option to distribute your version of this file under * either the CDDL, the GPL Version 2 or to extend the choice of license to * its licensees as provided above. However, if you add GPL Version 2 code * and therefore, elected the GPL Version 2 license, then the option applies * only if the new code is made subject to such option by the copyright * holder. */ package org.glassfish.appclient.server.core.jws; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.StringReader; import java.io.StringWriter; import java.io.Writer; import java.net.URI; import java.net.URISyntaxException; import java.util.Map; import javax.inject.Inject; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpressionException; import org.glassfish.api.deployment.archive.ReadableArchive; import org.glassfish.appclient.server.core.AppClientDeployerHelper; import org.glassfish.appclient.server.core.jws.servedcontent.DynamicContent; import org.glassfish.appclient.server.core.jws.servedcontent.StaticContent; import org.glassfish.appclient.server.core.jws.servedcontent.TokenHelper; import org.jvnet.hk2.annotations.Service; import org.glassfish.hk2.api.PerLookup; import org.w3c.dom.DOMImplementation; import org.w3c.dom.Document; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.w3c.dom.bootstrap.DOMImplementationRegistry; import org.w3c.dom.ls.DOMImplementationLS; import org.w3c.dom.ls.LSOutput; import org.w3c.dom.ls.LSSerializer; import org.xml.sax.InputSource; import org.xml.sax.SAXException; /** * Processes developer-provided content in (or directly or indirectly * referenced from) an optional JNLP file included in the client or the EAR. * * @author tjquinn */ @Service @PerLookup public class DeveloperContentHandler { @Inject DeveloperContentService dcs; private ClassLoader loader; private ReadableArchive appClientArchive; private Map<String,StaticContent> staticContent; private Map<String,DynamicContent> dynamicContent; private TokenHelper tHelper; private URI appRootURI; private LSSerializer lsSerializer = null; private LSOutput lsOutput = null; private static DocumentBuilderFactory dbf = documentBuilderFactory(); private static DocumentBuilder db = documentBuilder(); private AppClientDeployerHelper helper; public void init( final ClassLoader loader, final TokenHelper tHelper, final File appRootDir, final ReadableArchive appClientArchive, final Map<String,StaticContent> staticContent, final Map<String,DynamicContent> dynamicContent, final AppClientDeployerHelper helper) { this.loader = loader; this.tHelper = tHelper; this.appRootURI = appRootDir.toURI(); this.appClientArchive = appClientArchive; this.staticContent = staticContent; this.dynamicContent = dynamicContent; this.helper = helper; } /** * Combines the developer-provided JNLP in the client with the JNLP * generated by the server. * * @param generatedJNLPTemplate JNLP generated by the server * @return combined JNLP; if the developer provided no customized JNLP then * the generated JNLP, unchanged */ String combineJNLP( final String generatedJNLPTemplate, final String developerJNLP) { final Document devDOM; try { devDOM = developerDOMFromPath(developerJNLP); } catch (Exception ex) { throw new RuntimeException(ex); } if (devDOM == null) { return generatedJNLPTemplate; } /* * Get the generated main JNLP document. */ final InputSource generatedJNLPSource = new InputSource( new StringReader(generatedJNLPTemplate)); /* * The result document starts as the developer-provided document. Then * override the parts that the server insists on providing itself, * then merge in other parts that the server wants to add to * whatever the developer provided there. */ Document generatedJNLPDOM; try { generatedJNLPDOM = db.parse(generatedJNLPSource); /* * Each CombinedXPath object knows how to combine the generated and * the developer-provided content whether defaulted, overridden, or * merged. */ for (CombinedXPath combinedXPath : dcs.xPathsToCombinedContent()) { combinedXPath.process(devDOM, generatedJNLPDOM); } return toXML(generatedJNLPDOM); } catch (Exception ex) { throw new RuntimeException(ex); } } private synchronized Document developerDOMFromPath(final String devJNLPDoc) throws SAXException, IOException { Document result = null; if (devJNLPDoc != null) { final InputStream devJNLPStream = JavaWebStartInfo.openEntry(appClientArchive, devJNLPDoc); if (devJNLPStream != null) { result = db.parse(devJNLPStream); } else { throw new FileNotFoundException(devJNLPDoc); } } return result; } private synchronized Document developerDOMFromContent(final String devContent) throws SAXException, IOException { final InputSource is = new InputSource(new StringReader(devContent)); Document result = db.parse(is); return result; } private String toXML(final Document dom) throws ClassNotFoundException, InstantiationException, IllegalAccessException { Writer writer = new StringWriter(); writeXML(dom, writer); return writer.toString(); } private synchronized void writeXML(final Node node, final Writer writer) throws ClassNotFoundException, InstantiationException, IllegalAccessException { if (lsSerializer == null) { final DOMImplementation domImpl = DOMImplementationRegistry.newInstance(). getDOMImplementation(""); final DOMImplementationLS domLS = (DOMImplementationLS) domImpl.getFeature("LS", "3.0"); lsOutput = domLS.createLSOutput(); lsOutput.setEncoding("UTF-8"); lsSerializer = domLS.createLSSerializer(); } lsOutput.setCharacterStream(writer); lsSerializer.write(node, lsOutput); } /** * Adds all developer-provided content that falls within the code base to * the static or dynamic content. * <p> * We need to do this so that the Grizzly adapter that serves the content * knows that it is OK to serve this content. Otherwise a hostile user or * app could conduct "fishing expeditions" for content on the server that * should not be exposed simply by using the Java Web Start-related URLs * and varying the path part to browse for files. */ void addDeveloperContentFromPath(final String devJNLPDocPath) { /* * There is no work to do unless the developer specified a JNLP * document. */ if (devJNLPDocPath == null || (devJNLPDocPath.length() == 0)) { return; } final Document devDOM; try { devDOM = developerDOMFromPath(devJNLPDocPath); addDeveloperContent(devJNLPDocPath, devDOM); } catch (Exception ex) { throw new RuntimeException(ex); } } void addDeveloperContent(final String devJNLPDocPath, final String devJNLP) { try { final Document devDOM = developerDOMFromContent(devJNLP); addDeveloperContent(devJNLPDocPath, devDOM); } catch (Exception ex) { throw new RuntimeException(ex); } } private void addDeveloperContent( final String contentPath, final Document devDOM) throws XPathExpressionException, URISyntaxException, IOException { /* * Search for hrefs to other content. Add each that falls within * the codebase to the relevant content. */ final URI codebaseURI = new URI(tHelper.appCodebasePath()); for (XPathToDeveloperProvidedContentRefs c : dcs.xPathsToDevContentRefs()) { NodeList nodes = (NodeList) c.xPathExpr().evaluate(devDOM, XPathConstants.NODESET); if (nodes.getLength() > 0) { for (int i = 0; i < nodes.getLength(); i++) { final String href = nodes.item(i).getNodeValue(); /* * Tokens have not been substituted at this point in the processing, * and developer-provided content should not use tokens for * hrefs. So don't process an href starting with ${. */ if ( ! href.startsWith("${")) { c.addToContentIfInApp(this, helper, contentPath, codebaseURI, href, loader, staticContent, dynamicContent, appRootURI, appClientArchive); } } } } } private static DocumentBuilderFactory documentBuilderFactory() { final DocumentBuilderFactory f = DocumentBuilderFactory.newInstance(); try { /* * Turn off deferred expansion or the adoptNode method - which * we use to migrate parts of the generated document into the * result document - will copy the unexpanded content! */ f.setFeature("http://apache.org/xml/features/dom/defer-node-expansion", false); } catch (ParserConfigurationException ex) { throw new RuntimeException(ex); } return f; } private static DocumentBuilder documentBuilder() { try { final DocumentBuilder b = dbf.newDocumentBuilder(); return b; } catch (ParserConfigurationException ex) { throw new RuntimeException(ex); } } }