/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright (c) 1997-2014 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 com.sun.enterprise.deployment.ApplicationClientDescriptor;
import com.sun.enterprise.universal.i18n.LocalStringsImpl;
import com.sun.logging.LogDomains;
import java.beans.PropertyChangeEvent;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URI;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.inject.Inject;
import org.glassfish.api.admin.ServerEnvironment;
import org.glassfish.api.container.EndpointRegistrationException;
import org.glassfish.api.deployment.DeploymentContext;
import org.glassfish.api.deployment.archive.ReadableArchive;
import org.glassfish.appclient.server.core.AppClientDeployerHelper;
import org.glassfish.appclient.server.core.AppClientServerApplication;
import org.glassfish.appclient.server.core.jws.ExtensionFileManager.Extension;
import org.glassfish.appclient.server.core.jws.servedcontent.ASJarSigner;
import org.glassfish.appclient.server.core.jws.servedcontent.AutoSignedContent;
import org.glassfish.appclient.server.core.jws.servedcontent.Content;
import org.glassfish.appclient.server.core.jws.servedcontent.DynamicContent;
import org.glassfish.appclient.server.core.jws.servedcontent.FixedContent;
import org.glassfish.appclient.server.core.jws.servedcontent.SimpleDynamicContentImpl;
import org.glassfish.appclient.server.core.jws.servedcontent.StaticContent;
import org.glassfish.appclient.server.core.jws.servedcontent.StreamedAutoSignedStaticContent;
import org.glassfish.appclient.server.core.jws.servedcontent.TokenHelper;
import org.glassfish.deployment.common.DeploymentUtils;
import org.jvnet.hk2.annotations.Service;
import org.glassfish.hk2.api.PerLookup;
import org.glassfish.logging.annotation.LogMessageInfo;
import org.glassfish.logging.annotation.LogMessagesResourceBundle;
import org.glassfish.logging.annotation.LoggerInfo;
import org.jvnet.hk2.config.ConfigListener;
import org.jvnet.hk2.config.UnprocessedChangeEvent;
import org.jvnet.hk2.config.UnprocessedChangeEvents;
import org.jvnet.hk2.config.types.Property;
/**
* Encapsulates information related to Java Web Start support for a single
* app client.
* <p>
* The AppClientServerApplication creates one instance of this class for each
* app client that is deployed - either standalone or as part of an EAR.
*
* @author tjquinn
*/
@Service
@PerLookup
public class JavaWebStartInfo implements ConfigListener {
private final static String GLASSFISH_DIRECTORY_PREFIX = "glassfish/";
@Inject
private JWSAdapterManager jwsAdapterManager;
@Inject
private ASJarSigner jarSigner;
@Inject
private DeveloperContentHandler dch;
@Inject
private ExtensionFileManager extensionFileManager;
@Inject
private ServerEnvironment serverEnv;
private AppClientServerApplication acServerApp;
private Set<Content> myContent = null;
private DeploymentContext dc = null;
private TokenHelper tHelper;
@LogMessagesResourceBundle
public static final String APPCLIENT_SERVER_LOGMESSAGE_RESOURCE = "org.glassfish.appclient.server.LogMessages";
@LoggerInfo(subsystem="SERVER", description="Appclient Server-side Logger", publish=true)
public static final String APPCLIENT_SERVER_MAIN_LOGGER = "javax.enterprise.system.container.appclient";
private static final Logger logger =
Logger.getLogger(APPCLIENT_SERVER_MAIN_LOGGER, APPCLIENT_SERVER_LOGMESSAGE_RESOURCE);
@LogMessageInfo(
message = "Java Web Start services started for the app client {0} (contextRoot: {1})",
level = "INFO")
public static final String JWS_STARTED = "AS-ACDEPL-00103";
@LogMessageInfo(
message = "Java Web Start services stopped for the app client {0}",
level = "INFO")
public static final String JWS_STOPPED = "AS_ACDEPL-00104";
@LogMessageInfo(
message = "Java Web Start services not started for the app client {0}; its developer has marked it as ineligible",
cause = "The developer's glassfish-application-client.xml file marks the app client as ineligible for Java Web Start support.",
action = "If users should be able to launch this client using Java Web Start, change the <java-web-start-support> 'enabled' attribute.")
public static final String JWS_INELIGIBLE = "AS_ACDEPL-00101";
@LogMessageInfo(
message = "Java Web Start services not started for the app client {0}; the administrator has disabled Java Web Start support for it",
cause = "The administrator disabled Java Web Start launches for the app client, either using '--properties java-web-start-enabled=false' during deployment or changing the properties afterwards.",
action = "If users should be able to launch this client using Java Web Start, either deploy the application again without --properties or adjust the configuration using the admin console or the asadmin 'set' command")
public static final String JWS_DISABLED = "AS_ACDEPL_00102";
private VendorInfo vendorInfo;
private String signingAlias;
final private Map<String,StaticContent> staticContent = new HashMap<String,StaticContent>();
final private Map<String,DynamicContent> dynamicContent = new HashMap<String,DynamicContent>();
private static final String JNLP_MIME_TYPE = "application/x-java-jnlp-file";
public static final String DOC_TEMPLATE_PREFIX = "/org/glassfish/appclient/server/core/jws/templates/";
private static final String MAIN_DOCUMENT_TEMPLATE =
DOC_TEMPLATE_PREFIX + "appclientMainDocumentTemplate.jnlp";
private static final String CLIENT_DOCUMENT_TEMPLATE =
DOC_TEMPLATE_PREFIX + "appclientClientDocumentTemplate.jnlp";
public static final String DEVELOPER_EXTENSION_DOCUMENT_TEMPLATE =
DOC_TEMPLATE_PREFIX + "developerProvidedDocumentTemplate.jnlp";
private static final String MAIN_IMAGE_XML_PROPERTY_NAME =
"appclient.main.information.images";
public static final String APP_LIBRARY_EXTENSION_PROPERTY_NAME = "app.library.extension";
private static final String APP_CLIENT_MAIN_CLASS_ARGUMENTS_PROPERTY_NAME =
"appclient.main.class.arguments";
private static final String CLIENT_FACADE_JAR_PATH_PROPERTY_NAME =
"client.facade.jar.path";
private static final String CLIENT_JAR_PATH_PROPERTY_NAME =
"client.jar.path";
private static final String GROUP_FACADE_PATH_PROPERTY_NAME =
"group.facade.jar.path";
/**
* records if the app client is eligible for Java Web Start support, as
* defined in the developer-provided sun-application-client.xml descriptor
*/
private boolean isJWSEligible;
/**
* records if the containing app is set to enable Java Web
* Start access (in the domain.xml config for the application and the
* module) - could be updated from a separate
* thread if the administrator changes the java-web-start-enabled setting
*/
private volatile boolean isJWSEnabledAtApp = true;
private volatile boolean isJWSEnabledAtModule = true;
private final JavaWebStartState jwsState = new JavaWebStartState();
private final static String JAVA_WEB_START_ENABLED_PROPERTY_NAME =
DeploymentUtils.DEPLOYMENT_PROPERTY_JAVA_WEB_START_ENABLED;
private AppClientDeployerHelper helper;
private ApplicationClientDescriptor acDesc;
private String developerJNLPDoc;
private static LocalStringsImpl localStrings = new LocalStringsImpl(JavaWebStartInfo.class);
private static LocalStringsImpl servedContentLocalStrings =
new LocalStringsImpl(TokenHelper.class);
private static class SignedSystemContentFromApp {
private final String tokenName;
private final String relativePath;
private SignedSystemContentFromApp(String tokenName, String relativePath) {
this.tokenName = tokenName;
this.relativePath = relativePath;
}
String getRelativePath() {
return relativePath;
}
String getTokenName() {
return tokenName;
}
URI getRelativePathURI() {
return URI.create(relativePath);
}
}
/**
* Completes initialization of the object. Should be invoked immediate
* after the object is created by the habitat.
*
* @param acServerApp the per-client AppClientServerApplication object for the client of interest
*/
public void init(final AppClientServerApplication acServerApp) {
this.acServerApp = acServerApp;
helper = acServerApp.helper();
acDesc = acServerApp.getDescriptor();
dc = acServerApp.dc();
isJWSEligible = acDesc.getJavaWebStartAccessDescriptor().isEligible();
isJWSEnabledAtApp = isJWSEnabled(dc.getAppProps());
isJWSEnabledAtModule = isJWSEnabled(dc.getModuleProps());
tHelper = TokenHelper.newInstance(helper, vendorInfo());
final String devJNLPDoc = acDesc.getJavaWebStartAccessDescriptor().getJnlpDocument();
final File sourceDir = acDesc.getApplication().isVirtual() ?
dc.getSourceDir() : new File(dc.getSource().getParentArchive().getURI());
this.developerJNLPDoc = devJNLPDoc;
signingAlias = JWSAdapterManager.signingAlias(dc);
dch.init(dc.getClassLoader(),
tHelper,
sourceDir,
dc.getSource(),
staticContent,
dynamicContent,
helper);
}
/**
* Starts Java Web Start services for this client, if the client is
* eligible (as decided by the developer) and enabled (as decided by the
* administrator).
*/
public void start() {
/*
* The developer might have disabled Java Web Start support in the
* sun-application-client.xml or in the domain's configuration,
* so check those before starting JWS services.
*/
if (isJWSRunnable()) {
jwsState.transition(JavaWebStartState.Action.START, new Runnable() {
@Override
public void run() {
try {
startJWSServices();
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
});
}
}
/**
* Stops Java Web Start services for this client.
*/
public void stop() {
jwsState.transition(JavaWebStartState.Action.STOP, new Runnable() {
@Override
public void run() {
try {
stopJWSServices();
} catch (EndpointRegistrationException ex) {
throw new RuntimeException(ex);
}
}
});
}
/**
* Suspends Java Web Start services for this client.
*/
public void suspend() {
jwsState.transition(JavaWebStartState.Action.SUSPEND, new Runnable() {
@Override
public void run() {
suspendJWSServices();
}
});
}
/**
* Resumes Java Web Start services for this client.
*/
public void resume() {
if (isJWSRunnable()) {
jwsState.transition(JavaWebStartState.Action.RESUME, new Runnable() {
@Override
public void run() {
resumeJWSServices();
}
});
}
}
static InputStream openEntry(final ReadableArchive appClientArchive,
final String pathToContent) throws IOException {
final int bang = pathToContent.indexOf('!');
if (bang == -1) {
return appClientArchive.getEntry(pathToContent);
} else {
if (appClientArchive.getParentArchive() == null) {
throw new IllegalArgumentException(pathToContent);
}
return appClientArchive.getParentArchive()
.getSubArchive(pathToContent.substring(0, bang))
.getEntry(pathToContent.substring(bang + 1));
}
}
private void startJWSServices() throws EndpointRegistrationException, IOException {
if (myContent == null) {
myContent = addClientContentToHTTPAdapters();
}
/*
* Currently, we implement the ability to disable or enable app clients
* within an EAR by marking the associated content as disabled or
* enabled, which the Grizzly adapter looks at before responding to
* a request for that bit of content. So mark all the content as
* started.
*/
for (Content c : myContent) {
c.start();
}
logger.log(Level.INFO, JWS_STARTED,
new Object[] {acServerApp.moduleExpression(),
JWSAdapterManager.userFriendlyContextRoot(acServerApp)});
}
private void processExtensionReferences() throws IOException {
// TODO: needs to be expanded to handle signed library JARS, perhap signed by different certs
final URI fileURI = URI.create("file:" + helper.appClientServerOriginalAnchor(dc).getRawSchemeSpecificPart());
Set<Extension> exts = extensionFileManager.findExtensionTransitiveClosure(
new File(fileURI),
//new File(helper.appClientServerURI(dc)).getParentFile(),
dc.getSource().getManifest().getMainAttributes());
tHelper.setProperty(APP_LIBRARY_EXTENSION_PROPERTY_NAME,
jarElementsForExtensions(exts));
for (Extension e : exts) {
final URI uri = URI.create(JWSAdapterManager.publicExtensionLookupURIText(e));
final StaticContent newSystemContent = createSignedStaticContent(
e.getFile(),
signedFileForDomainFile(e.getFile()),
uri,
extensionName(e.getFile()));
jwsAdapterManager.addStaticSystemContent(
uri.toString(),
newSystemContent);
}
}
private String extensionName(final File f) throws IOException {
JarFile jf = null;
try {
jf = new JarFile(f);
final Manifest mf = jf.getManifest();
final Attributes mainAttrs = mf.getMainAttributes();
final String extName = mainAttrs.getValue(Attributes.Name.EXTENSION_NAME);
return (extName == null ? "" : extName);
} finally {
if (jf != null) {
jf.close();
}
}
}
private File signedFileForDomainFile(final File unsignedFile) {
final File rootForSignedFilesInDomain = jwsAdapterManager.rootForSignedFilesInDomain();
mkdirs(rootForSignedFilesInDomain);
final URI signedFileURI = rootForSignedFilesInDomain.toURI().resolve(relativeURIToDomainFile(unsignedFile));
return new File(signedFileURI);
}
private URI relativeURIToDomainFile(final File domainFile) {
return serverEnv.getDomainRoot().toURI().relativize(domainFile.toURI());
}
private String jarElementsForExtensions(final Set<Extension> exts) {
final StringBuilder sb = new StringBuilder();
for (Extension e : exts) {
sb.append("<jar href=\"").append(JWSAdapterManager.publicExtensionHref(e)).append("\"/>");
}
return sb.toString();
}
private void stopJWSServices() throws EndpointRegistrationException {
/*
* Mark all this client's content as stopped so the Grizzly adapter
* will not serve it.
*/
for (Content c : myContent) {
c.stop();
}
jwsAdapterManager.removeContentForAppClient(
acServerApp.deployedAppName(),
(acDesc.isStandalone() ? null : acDesc.getModuleName()),
acServerApp);
logger.log(Level.INFO, JWS_STOPPED,
acServerApp.moduleExpression());
}
private void suspendJWSServices() {
for (Content c : myContent) {
c.suspend();
}
}
private void resumeJWSServices() {
for (Content c : myContent) {
c.resume();
}
}
/**
* Returns if this client is enabled for Java Web Start access.
* <p>
* The administrator can set the java-web-start-enabled property at
* either the application level or the module level or both. For this
* client to be enabled, any such specified property must be set to true.
* The default is true.
*/
private boolean isJWSEnabled(final Properties props) {
boolean result = true;
final String propsSetting = props.getProperty(JAVA_WEB_START_ENABLED_PROPERTY_NAME);
if (propsSetting != null) {
result &= Boolean.parseBoolean(propsSetting);
}
return result;
}
private boolean isJWSEnabled() {
return isJWSEnabledAtApp && isJWSEnabledAtModule;
}
private boolean isJWSRunnable() {
if ( ! isJWSEligible) {
logger.log(Level.INFO, JWS_INELIGIBLE,
acServerApp.moduleExpression());
}
if ( ! isJWSEnabled()) {
logger.log(Level.INFO, JWS_DISABLED,
acServerApp.moduleExpression());
}
return isJWSEligible && isJWSEnabled();
}
private Set<Content> addClientContentToHTTPAdapters() throws EndpointRegistrationException, IOException {
/*
* NOTE - Be sure to initialize the static content first. That method
* assigns some properties that can appear as placeholders in the
* dynamic content.
*/
initClientStaticContent();
initClientDynamicContent();
dch.addDeveloperContentFromPath(developerJNLPDoc);
Set<Content> result = new HashSet<Content>(staticContent.values());
result.addAll(dynamicContent.values());
jwsAdapterManager.addContentForAppClient(
acServerApp.deployedAppName(),
(acDesc.isStandalone() ? null : acDesc.getModuleName()),
acServerApp, tHelper.tokens(),
staticContent, dynamicContent);
return result;
}
private void initClientStaticContent()
throws IOException, EndpointRegistrationException {
/*
* The client-level adapter's static content is the app client JAR and
* the app client facade.
*/
createAndAddSignedContentFromAppFile(
staticContent,
helper.appClientServerURI(dc),
helper.appClientUserURI(dc),
CLIENT_JAR_PATH_PROPERTY_NAME,
acServerApp.getDescriptor().getName());
createAndAddSignedStaticContentFromMainJAR(
staticContent,
helper.facadeServerURI(dc),
helper.facadeUserURI(dc),
CLIENT_FACADE_JAR_PATH_PROPERTY_NAME);
if ( ! acDesc.isStandalone()) {
createAndAddSignedStaticContentFromGeneratedFile(
staticContent,
helper.groupFacadeServerURI(dc),
helper.groupFacadeUserURI(dc),
GROUP_FACADE_PATH_PROPERTY_NAME,
acServerApp.getDescriptor().getName());
}
/*
* Add static content representing any extension libraries this client
* (or the JARs it depends on) uses.
*/
processExtensionReferences();
/*
* Make sure that there are versions of all GF system JARs
* that are signed by the same cert used to sign the facade JAR for
* this app. That's because the user might have chosen to sign using
* a particular alias so the end-users will accept JARs signed by
* the corresponding cert. (Java Web Start will prompt them to do this
* during the download of signed JARs.) Also, Java Web Start (as of
* 1.6.0_19) warns users about apps which run boh signed and unsigned
* code and segregates the two into different class loaders which
* would not work for us.
*
* Note that the following logic makes sure that such signed versions
* exist. If multiple apps use the same cert to sign JARs, then the
* multiple instances of AutoSignedContent class for the same signed
* JAR will point to and reuse the same signed JAR, rather than
* re-sign it each time an app needed it is started.
*/
addSignedSystemContent();
/*
* The developer might have used the sun-application-client.xml
* java-web-start-support/vendor setting to communicate icon and/or
* splash screen images URIs.
*/
prepareImageInfo(staticContent);
for (Map.Entry<String,Map<URI,StaticContent>> signedEntry : helper.signingAliasToJar().entrySet()) {
for (Map.Entry<URI,StaticContent> contentEntry : signedEntry.getValue().entrySet()) {
staticContent.put(contentEntry.getKey().toASCIIString(), contentEntry.getValue());
}
}
}
private void addSignedSystemContent(
) throws FileNotFoundException, IOException {
final List<String> systemJARRelativeURIs = new ArrayList<String>();
final Map<String,StaticContent> addedStaticContent =
jwsAdapterManager.addStaticSystemContent(
systemJARRelativeURIs,
signingAlias);
final Map<String,DynamicContent> addedDynContent =
jwsAdapterManager.addDynamicSystemContent(
systemJARRelativeURIs,
signingAlias);
jwsAdapterManager.addContentIfAbsent(addedStaticContent, addedDynContent);
tHelper.setProperty("gf-client.jar", jwsAdapterManager.systemPathInClientJNLP(
jwsAdapterManager.gfClientJAR().toURI(), signingAlias));
tHelper.setProperty("gf-client-module.jar", jwsAdapterManager.systemPathInClientJNLP(
jwsAdapterManager.gfClientModuleJAR().toURI(), signingAlias));
}
private void createAndAddSignedContentFromAppFile(final Map<String,StaticContent> content,
final URI uriToFile,
final URI uriForLookup,
final String tokenName,
final String appName) throws FileNotFoundException {
final File unsignedFile = new File(uriToFile);
final File signedFile = signedFileForProvidedAppFile(unsignedFile);
createAndAddSignedStaticContent(content, unsignedFile, signedFile,
uriForLookup, tokenName, appName);
}
private void createAndAddSignedStaticContentFromGeneratedFile(final Map<String,StaticContent> content,
final URI uriToFile,
final URI uriForLookup,
final String tokenName,
final String appName) throws FileNotFoundException {
final File unsignedFile = new File(uriToFile);
final File signedFile = signedFileForGeneratedAppFile(unsignedFile);
createAndAddSignedStaticContent(content, unsignedFile, signedFile,
uriForLookup, tokenName, appName);
}
private void createAndAddSignedStaticContent(
final Map<String,StaticContent> content,
final File unsignedFile,
final File signedFile,
final URI uriForLookup,
final String tokenName,
final String appName
) throws FileNotFoundException {
final StaticContent signedJarContent = createSignedStaticContent(
unsignedFile, signedFile, uriForLookup, appName);
recordStaticContent(content, signedJarContent, uriForLookup, tokenName);
}
private StaticContent createSignedStaticContent(
final File unsignedFile,
final File signedFile,
final URI uriForLookup,
final String appName) throws FileNotFoundException {
mkdirs(signedFile.getParentFile());
final StaticContent signedJarContent = new AutoSignedContent(
unsignedFile,
signedFile,
signingAlias,
jarSigner,
uriForLookup.toASCIIString(),
appName);
return signedJarContent;
}
private void createAndAddSignedStaticContentFromMainJAR(
final Map<String,StaticContent> content,
final URI uriToFile,
final URI uriForLookup,
final String tokenName) throws FileNotFoundException {
final File unsignedFile = new File(uriToFile);
final StaticContent signedContent = new StreamedAutoSignedStaticContent(unsignedFile, signingAlias, jarSigner,
uriForLookup.toASCIIString(), acServerApp.getDescriptor().getName());
recordStaticContent(content, signedContent, uriForLookup, tokenName);
}
private void recordStaticContent(final Map<String,StaticContent> content,
final StaticContent newContent,
final URI uriForLookup,
final String tokenName) {
final String uriStringForLookup = uriForLookup.toASCIIString();
recordStaticContent(content, newContent, uriStringForLookup);
if (tokenName != null) {
tHelper.setProperty(tokenName, uriForLookup.toASCIIString());
}
}
private void recordStaticContent(final Map<String,StaticContent> content,
final StaticContent newContent,
final String uriStringForLookup) {
content.put(uriStringForLookup, newContent);
logger.log(Level.FINE, "Recording static content: URI for lookup = {0}; content = {1}",
new Object[]{uriStringForLookup, newContent.toString()});
}
public static URI relativeURIForProvidedOrGeneratedAppFile(
final DeploymentContext dc, final URI absURI, AppClientDeployerHelper helper) {
URI possiblyRelativeURI = rootForSignedFilesInApp(helper).relativize(absURI);
if ( ! possiblyRelativeURI.isAbsolute()) {
return possiblyRelativeURI;
}
/*
* The file could be a generated JAR file for a submodule in a
* directory-deployed app, in which case the URI is within the EAR's
* generated subdirectory.
*/
possiblyRelativeURI = rootForGeneratedSubmoduleJAR(dc, helper).relativize(absURI);
if ( ! possiblyRelativeURI.isAbsolute()) {
return possiblyRelativeURI;
}
return helper.URIWithinAppDir(dc, absURI);
}
private static URI rootForSignedFilesInApp(final AppClientDeployerHelper helper) {
return helper.rootForSignedFilesInApp().toURI();
}
/**
* Returns an absolute URI for the root directory that contains JARs
* for submodules that are generated from a directory deployment submodule
* directory.
* @param dc the deployment context for the app client deployment underway
* @return absolute URI to the generated submodule JAR root directory
*/
private static URI rootForGeneratedSubmoduleJAR(final DeploymentContext dc,
final AppClientDeployerHelper helper) {
final File f = new File(dc.getScratchDir("xml").getParentFile(),
NamingConventions.anchorSubpathForNestedClient(helper.appName(dc)));
return f.toURI();
}
private File signedFileForGeneratedAppFile(final File unsignedFile) {
/*
* Signed files at the app level go in
*
* generated/xml/(appName)/signed/(path-within-app-of-unsigned-file)
*
* and when we're signing a generated file we just use its URI
* relative to the app's scratch directory to compute the URI relative
* to generated/xml/(appName)/signed where the signed file should reside.
*/
final File rootForSignedFilesInApp = helper.rootForSignedFilesInApp();
mkdirs(rootForSignedFilesInApp);
final URI unsignedFileURIRelativeToXMLDir = dc.getScratchDir("xml").getParentFile().toURI().
relativize(unsignedFile.toURI());
final URI signedFileURI = rootForSignedFilesInApp.toURI().resolve(unsignedFileURIRelativeToXMLDir);
return new File(signedFileURI);
}
public File signedFileForProvidedAppFile(final File unsignedFile) {
return signedFileForProvidedAppFile(helper.appClientURIWithinApp(dc),
unsignedFile, helper, dc);
}
public static File signedFileForProvidedAppFile(final URI relURI,
final File unsignedFile,
final AppClientDeployerHelper helper,
final DeploymentContext dc) { /*
* Place a signed file for a developer-provided file at
*
* generated/xml/(appName)/signed/(path-within-app-of-unsigned-file)
*
*
*/
final File rootForSignedFilesInApp = helper.rootForSignedFilesInApp();
mkdirs(rootForSignedFilesInApp);
final URI signedFileURI = rootForSignedFilesInApp.toURI().resolve(relURI);
return new File(signedFileURI);
}
private void initClientDynamicContent() throws IOException {
helper.createAndAddLibraryJNLPs(helper, tHelper, dynamicContent);
tHelper.setProperty(APP_CLIENT_MAIN_CLASS_ARGUMENTS_PROPERTY_NAME, "");
final String mainDocument = dch.combineJNLP(
textFromURL(MAIN_DOCUMENT_TEMPLATE),
developerJNLPDoc);
createAndAddDynamicContentFromTemplateText(
dynamicContent, tHelper.mainJNLP(), mainDocument, true /* isMain */);
/*
* Add the main JNLP again but with an empty URI string so the user
* can launch the app client by specifying only the context root.
*/
createAndAddDynamicContentFromTemplateText(dynamicContent, "", mainDocument, true /* isMain */);
createAndAddDynamicContent(
dynamicContent, tHelper.clientJNLP(), CLIENT_DOCUMENT_TEMPLATE);
}
public static void createAndAddDynamicContent(
final TokenHelper tHelper,
final Map<String,DynamicContent> content,
final String uriStringForContent,
final String uriStringForTemplate) throws IOException {
createAndAddDynamicContentFromTemplateText(
tHelper, content, uriStringForContent,
textFromURL(uriStringForTemplate), false /* isMain */);
}
private void createAndAddDynamicContent(
final Map<String,DynamicContent> content,
final String uriStringForContent,
final String uriStringForTemplate) throws IOException {
createAndAddDynamicContentFromTemplateText(
content, uriStringForContent,
textFromURL(uriStringForTemplate));
}
private void createAndAddDynamicContentFromTemplateText(
final Map<String,DynamicContent> content,
final String uriStringForContent,
final String templateText) throws IOException {
createAndAddDynamicContentFromTemplateText(content, uriStringForContent, templateText, false /* isMain */);
}
private void createAndAddDynamicContentFromTemplateText(
final Map<String,DynamicContent> content,
final String uriStringForContent,
final String templateText,
final boolean isMain) throws IOException {
createAndAddDynamicContentFromTemplateText(tHelper,
content, uriStringForContent, templateText, isMain);
}
private static void createAndAddDynamicContentFromTemplateText(
final TokenHelper tHelper,
final Map<String,DynamicContent> content,
final String uriStringForContent,
final String templateText,
final boolean isMain) throws IOException {
final String processedTemplate = Util.replaceTokens(
templateText, tHelper.tokens());
content.put(uriStringForContent, newDynamicContent(processedTemplate,
JNLP_MIME_TYPE, isMain));
logger.log(Level.FINE, "Adding dyn content {0}{1}{2}",
new Object[]{uriStringForContent,
System.getProperty("line.separator"), logger.isLoggable(Level.FINER) ? processedTemplate : ""});
}
private static DynamicContent newDynamicContent(final String template,
final String mimeType, final boolean isMain) {
return new SimpleDynamicContentImpl(template, mimeType, isMain);
}
/**
* Prepares XML (for the generated JNLP) and the static content
* for the icon image, the splash screen image, neither, or
* both, depending on the contents (if any) of the <vendor> text in the
* developer-provided sun-application-client.xml descriptor.
*/
private void prepareImageInfo(final Map<String,StaticContent> staticContent) throws IOException {
/*
* Deployment has already expanded the app client module into a
* directory, so each image entry of the JAR which the developer
* specified in the descriptor should already reside as a
* file on the disk within the DeploymentContext.getSource() directory.
*/
addImageContentIfSpecified(vendorInfo().getImageURI(),
vendorInfo().JNLPImageURI(), staticContent);
addImageContentIfSpecified(vendorInfo().getSplashImageURI(),
vendorInfo().JNLPSplashImageURI(), staticContent);
}
private void addImageContentIfSpecified(
final String imageURIStringWithinAppClient,
final String imageURIStringForJNLP,
final Map<String,StaticContent> staticContent) {
if (imageURIStringWithinAppClient == null ||
imageURIStringWithinAppClient.length() == 0) {
return;
}
final URI absoluteImageURI = dc.getSource().getURI().resolve(imageURIStringWithinAppClient);
final File imageFile = new File(absoluteImageURI);
if ( ! imageFile.exists()) {
return;
}
staticContent.put(imageURIStringForJNLP,
new FixedContent(imageFile));
}
private VendorInfo vendorInfo() {
if (vendorInfo == null) {
vendorInfo = new VendorInfo(
acDesc.getJavaWebStartAccessDescriptor().getVendor(),
helper.pathToAppclientWithinApp(dc));
}
return vendorInfo;
}
public static class VendorInfo {
private String vendorStringFromDescriptor;
private String vendor = "";
private String imageURIString = "";
private String splashImageURIString = "";
private final String JNLPPathFullPrefix;
public VendorInfo(String vendorStringFromDescriptor,
final String JNLPPathPrefix) {
this.JNLPPathFullPrefix = "__content/" + JNLPPathPrefix;
this.vendorStringFromDescriptor = vendorStringFromDescriptor != null ?
vendorStringFromDescriptor : "";
String [] parts = this.vendorStringFromDescriptor.split("::");
if (parts.length == 1) {
vendor = parts[0];
} else if (parts.length == 2) {
imageURIString = parts[0];
vendor = parts[1];
} else if (parts.length == 3) {
imageURIString = parts[0];
splashImageURIString = parts[1];
vendor = parts[2];
}
if (vendor.length() == 0) {
vendor = servedContentLocalStrings.get("jws.defaultVendorName");
}
}
public String getVendor() {
return vendor;
}
public String getImageURI() {
return imageURIString;
}
public String getSplashImageURI() {
return splashImageURIString;
}
public String JNLPImageURI() {
return (imageURIString.length() > 0) ?
JNLPPathFullPrefix + imageURIString : "";
}
public String JNLPSplashImageURI() {
return (splashImageURIString.length() > 0) ?
JNLPPathFullPrefix + splashImageURIString : "";
}
}
@Override
public UnprocessedChangeEvents changed(PropertyChangeEvent[] events) {
/* Record any events we tried to process but could not. */
List<UnprocessedChangeEvent> unprocessedEvents = new ArrayList<UnprocessedChangeEvent>();
for (PropertyChangeEvent event : events) {
try {
processChangeEventIfInteresting(event);
} catch (Exception e) {
UnprocessedChangeEvent uce =
new UnprocessedChangeEvent(event, e.getLocalizedMessage());
unprocessedEvents.add(uce);
}
}
return (unprocessedEvents.size() > 0) ? new UnprocessedChangeEvents(unprocessedEvents) : null;
}
private void processChangeEventIfInteresting(final PropertyChangeEvent event) throws EndpointRegistrationException {
/*
* If the source is of type Application or Module and the newValue is of type
* Property then this could be a change we're interested in.
*/
final boolean isSourceApp = event.getSource() instanceof
com.sun.enterprise.config.serverbeans.Application;
final boolean isSourceModule = event.getSource() instanceof
com.sun.enterprise.config.serverbeans.Module;
if ( (! isSourceApp && ! isSourceModule)
|| ! (event.getNewValue() instanceof Property)) {
return;
}
/*
* Make sure the property name is java-web-start-enabled.
*/
Property newPropertySetting = (Property) event.getNewValue();
if ( ! newPropertySetting.getName().equals(JAVA_WEB_START_ENABLED_PROPERTY_NAME)) {
return;
}
String eventSourceName;
String thisAppOrModuleName;
if (isSourceApp) {
eventSourceName = ((com.sun.enterprise.config.serverbeans.Application) event.getSource()).getName();
thisAppOrModuleName = acServerApp.registrationName();
} else {
eventSourceName = ((com.sun.enterprise.config.serverbeans.Module) event.getSource()).getName();
thisAppOrModuleName = acDesc.getModuleName();
}
if ( ! thisAppOrModuleName.equals(eventSourceName)) {
return;
}
/*
* At this point we know that the event applies to this app client,
* so return a Boolean carrying the newly-assigned value.
*/
final Boolean newEnabledValue = Boolean.valueOf(newPropertySetting.getValue());
final Property oldPropertySetting = (Property) event.getOldValue();
final String oldPropertyValue = (oldPropertySetting != null)
? oldPropertySetting.getValue()
: null;
final Boolean oldEnabledValue = (oldPropertyValue == null
? Boolean.TRUE
: Boolean.valueOf(oldPropertyValue));
/*
* Record the new value of the relevant enabled setting.
*/
if (isSourceApp) {
isJWSEnabledAtApp = newEnabledValue;
} else {
isJWSEnabledAtModule = newEnabledValue;
}
/*
* Now act on the change of state.
*/
if ( ! newEnabledValue.equals(oldEnabledValue)) {
if (newEnabledValue) {
start();
} else {
stop();
}
}
}
public static String textFromURL(final String templateURLString) throws IOException {
final InputStream is = AppClientServerApplication.class.getResourceAsStream(templateURLString);
if (is == null) {
throw new FileNotFoundException(templateURLString);
}
StringBuilder sb = new StringBuilder();
InputStreamReader reader = new InputStreamReader(is);
char[] buffer = new char[1024];
int charsRead;
try {
while ((charsRead = reader.read(buffer)) != -1) {
sb.append(buffer, 0, charsRead);
}
return sb.toString();
} catch (IOException e) {
throw new IOException(templateURLString, e);
} finally {
try {
reader.close();
} catch (IOException ignore) {
throw new IOException("Error closing template stream after error", ignore);
}
}
}
private static void mkdirs(final File dir) {
if ( ! dir.exists()) {
if ( ! dir.mkdirs()) {
final String msg = logger.getResourceBundle().getString("enterprise.deployment.appclient.errormkdirs");
throw new RuntimeException(MessageFormat.format(msg, dir.getAbsolutePath()));
}
}
}
}