/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright (c) 1997-2017 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://oss.oracle.com/licenses/CDDL+GPL-1.1
* or 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 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.config.serverbeans.Config;
import org.glassfish.orb.admin.config.IiopService;
import com.sun.enterprise.deployment.ApplicationClientDescriptor;
import com.sun.logging.LogDomains;
import java.io.File;
import java.io.FileFilter;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URI;
import java.util.Collections;
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.concurrent.ConcurrentHashMap;
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 javax.inject.Named;
import org.glassfish.api.admin.ServerEnvironment;
import org.glassfish.api.container.EndpointRegistrationException;
import org.glassfish.api.container.RequestDispatcher;
import org.glassfish.api.deployment.DeploymentContext;
import org.glassfish.appclient.server.core.AppClientDeployer;
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.DynamicContent;
import org.glassfish.appclient.server.core.jws.servedcontent.SimpleDynamicContentImpl;
import org.glassfish.appclient.server.core.jws.servedcontent.StaticContent;
import org.glassfish.enterprise.iiop.api.GlassFishORBFactory;
import org.glassfish.internal.api.ServerContext;
import org.jvnet.hk2.annotations.Service;
import org.glassfish.hk2.api.PostConstruct;
import javax.inject.Singleton;
import org.glassfish.logging.annotation.LogMessageInfo;
/**
* Handles all management of the HTTP adapters created to support Java Web
* Start launches of app clients.
*
* @author tjquinn
*/
@Service
@Singleton
public class JWSAdapterManager implements PostConstruct {
private final static String SIGNING_ALIAS_PROPERTY_NAME = "jar-signing-alias";
private final static String DEFAULT_SIGNING_ALIAS = "s1as";
private final static String MANIFEST_APP_NAME_FOR_SYSTEM_FILES = "GlassFish";
@Inject
private ServerEnvironment serverEnv;
@Inject
private ServerContext serverContext;
@Inject
private RequestDispatcher requestDispatcher;
@Inject
private ASJarSigner jarSigner;
@Inject @Named(ServerEnvironment.DEFAULT_INSTANCE_NAME)
private Config config;
@Inject
AppClientDeployer appClientDeployer;
@Inject
private GlassFishORBFactory orbFactory;
private static final String LINE_SEP = System.getProperty("line.separator");
private static final List<String> DO_NOT_SERVE_LIST =
Collections.EMPTY_LIST; //Arrays.asList("glassfish/modules/jaxb-osgi.jar");
private static final String JWS_SIGNED_SYSTEM_JARS_ROOT = "java-web-start/___system";
private static final String JWS_SIGNED_DOMAIN_JARS_ROOT = "java-web-start/___domain";
private static final String JAVA_WEB_START_CONTEXT_ROOT_PROPERTY_NAME =
"javaWebStartContextRoot";
/**
* maps "(aliasName)/(systemJarRelativePath)" to AutoSignedContent for
* the system JAR as signed by the cert linked to the alias
*/
private final Map<String,AutoSignedContent> appLevelSignedSystemContent =
new HashMap<String,AutoSignedContent>();
private URI installRootURI = null;
private AppClientHTTPAdapter systemAdapter = null;
private Logger logger = null;
private IiopService iiopService;
private final HashMap<String,Set<AppClientServerApplication>> contributingAppClients =
new HashMap<String,Set<AppClientServerApplication>>();
private final ConcurrentHashMap<String,AppClientHTTPAdapter> httpAdapters = new
ConcurrentHashMap<String, AppClientHTTPAdapter>();
private URI umbrellaRootURI = null;
private File umbrellaRoot = null;
private File systemLevelSignedJARsRoot = null;
private File domainLevelSignedJARsRoot = null;
@LogMessageInfo(
message = "Error starting the adapter to serve static system-level content",
cause = "An unexpected internal system error occurred",
action = "Please consult the exception stack trace")
public static final String ERROR_STARTING_SYSTEM_ADAPTER = "AS-ACDEPL-00105";
@Override
public synchronized void postConstruct() {
installRootURI = serverContext.getInstallRoot().toURI();
logger = Logger.getLogger(JavaWebStartInfo.APPCLIENT_SERVER_MAIN_LOGGER, JavaWebStartInfo.APPCLIENT_SERVER_LOGMESSAGE_RESOURCE);
iiopService = config.getExtensionByType(IiopService.class);
umbrellaRoot = new File(installRootURI).getParentFile();
umbrellaRootURI = umbrellaRoot.toURI();
systemLevelSignedJARsRoot = new File(serverEnv.getDomainRoot(), JWS_SIGNED_SYSTEM_JARS_ROOT);
domainLevelSignedJARsRoot = new File(serverEnv.getDomainRoot(), JWS_SIGNED_DOMAIN_JARS_ROOT);
}
public static String signingAlias(final DeploymentContext dc) {
return chooseAlias(dc);
}
synchronized File rootForSignedFilesInDomain() {
return domainLevelSignedJARsRoot;
}
/**
* Adds more static content to the content served by the system adapter.
* @param lookupURI
* @param content
*/
void addStaticSystemContent(final String lookupURI, StaticContent newContent) throws IOException {
systemAdapter().addContentIfAbsent(lookupURI, newContent);
}
private static String chooseAlias(final DeploymentContext dc) {
final String userSpecifiedAlias;
return ((userSpecifiedAlias = extractUserProvidedAlias(dc)) != null)
? userSpecifiedAlias : DEFAULT_SIGNING_ALIAS;
}
private static String extractUserProvidedAlias(final DeploymentContext dc) {
return dc.getAppProps().getProperty(SIGNING_ALIAS_PROPERTY_NAME);
}
private synchronized AppClientHTTPAdapter startSystemContentAdapter() {
try {
AppClientHTTPAdapter sysAdapter = new AppClientHTTPAdapter(
NamingConventions.JWSAPPCLIENT_SYSTEM_PREFIX,
new Properties(),
serverEnv.getDomainRoot(),
new File(installRootURI),
iiopService,
orbFactory);
requestDispatcher.registerEndpoint(
NamingConventions.JWSAPPCLIENT_SYSTEM_PREFIX,
sysAdapter,
null);
if (logger.isLoggable(Level.FINE)) {
logger.log(Level.FINE, "Registered system content adapter serving {0}", sysAdapter);
}
return sysAdapter;
} catch (Exception e) {
logger.log(Level.SEVERE, ERROR_STARTING_SYSTEM_ADAPTER, e);
return null;
}
}
void addContentIfAbsent(final Map<String,StaticContent> staticContent,
final Map<String,DynamicContent> dynamicContent) throws IOException {
systemAdapter().addContentIfAbsent(staticContent, dynamicContent);
}
/**
* Records the need for signed copies of the GlassFish system JARs for the
* specified signing alias.
*
* @param systemJARRelativeURIs populated with the relative URIs for the added content
* @param signingAlias alias to use in signing the JARs
* @return map from the key by which the content will be stored to the content for that key
* @throws IOException
*/
Map<String,StaticContent> addStaticSystemContent(
final List<String> systemJARRelativeURIs,
final String signingAlias) throws IOException {
/*
* This method builds the static content for a given signing alias to
* be served by the "system" Grizzly
* adapter which serves files from the installation, as opposed to
* files from the domain or files from a specific app.
*/
Map<String,StaticContent> result = new HashMap<String,StaticContent>();
File gfClientJAR = gfClientJAR();
final String classPathExpr = getGFClientModuleClassPath(gfClientJAR);
final URI gfClientJARURI = gfClientJAR.toURI();
result.put(systemPath(gfClientJARURI, signingAlias),
systemJarSignedContent(gfClientJAR, signingAlias));
if (classPathExpr != null) {
for (String classPathElement : classPathExpr.split(" ")) {
final URI uri = gfClientJARURI.resolve(classPathElement);
final String systemPath = systemPath(uri, signingAlias);
/*
* There may be elements in the class path which do not exist
* on some platforms. So make sure the file exists before we offer
* to serve it.
*/
final File candidateFile = new File(uri);
final String relativeSystemPath = relativeSystemPath(uri);
if (candidateFile.exists() &&
( ! candidateFile.isDirectory()) && ( ! DO_NOT_SERVE_LIST.contains(relativeSystemPath))) {
result.put(systemPath,
systemJarSignedContent(candidateFile, signingAlias));
systemJARRelativeURIs.add(relativeSystemPath(uri));
}
}
}
/*
* Add the endorsed JARs to the system content.
*/
final File endorsedDir = new File(modulesDir(), "endorsed");
for (File endorsedJar : endorsedDir.listFiles(new FileFilter(){
@Override
public boolean accept(File pathname) {
return (pathname.isFile() && pathname.getName().endsWith(".jar"));
}
})) {
result.put(systemPath(endorsedJar.toURI()),
systemJarSignedContent(endorsedJar, signingAlias));
systemJARRelativeURIs.add(relativeSystemPath(endorsedJar.toURI()));
}
return result;
}
File gfClientJAR() {
return new File(
libDir(),
"gf-client.jar");
}
File gfClientModuleJAR() {
return new File(
modulesDir(),
"gf-client-module.jar");
}
private synchronized File modulesDir() {
return new File(new File(installRootURI), "modules");
}
private synchronized File libDir() {
return new File(new File(installRootURI), "lib");
}
static String publicExtensionHref(final Extension ext) {
return NamingConventions.JWSAPPCLIENT_SYSTEM_PREFIX + "/" + publicExtensionLookupURIText(ext);
}
static String publicExtensionLookupURIText(final Extension ext) {
return NamingConventions.JWSAPPCLIENT_EXT_INTRODUCER + "/" +
ext.getExtDirectoryNumber() + "/" +
ext.getFile().getName();
}
private AutoSignedContent systemJarSignedContent (
final File unsignedFile,
final String signingAlias) throws FileNotFoundException {
final String relativeURI = relativeSystemPath(unsignedFile.toURI());
final File signedFile = new File(signedSystemContentAliasDir(signingAlias),
relativeURI);
return new AutoSignedContent(unsignedFile, signedFile, signingAlias, jarSigner, relativeURI,
MANIFEST_APP_NAME_FOR_SYSTEM_FILES);
}
Map<String,DynamicContent> addDynamicSystemContent(final List<String> systemJARRelativeURIs,
final String signingAlias) throws IOException {
final Map<String,DynamicContent> result = new HashMap<String,DynamicContent>();
final String template = JavaWebStartInfo.textFromURL(
"/org/glassfish/appclient/server/core/jws/templates/systemJarsDocumentTemplate.jnlp");
final StringBuilder sb = new StringBuilder();
for (String relativeURIString : systemJARRelativeURIs) {
sb.append("<jar href=\"").append(signingAlias).append("/")
.append(relativeURIString).append("\"/>").append(LINE_SEP);
}
final Properties p = new Properties();
p.setProperty("system.jars", sb.toString());
final String replacedText = Util.replaceTokens(template, p);
result.put(NamingConventions.systemJNLPURI(signingAlias),
new SimpleDynamicContentImpl(replacedText, "jnlp"));
return result;
}
private String systemPath(final URI systemFileURI) {
return //NamingConventions.JWSAPPCLIENT_SYSTEM_PREFIX + "/" +
relativeSystemPath(systemFileURI);
}
String systemPath(final URI systemFileURI, final String signingAlias) {
return signingAlias + "/" + relativeSystemPath(systemFileURI);
}
String systemPathInClientJNLP(final URI systemFileURI, final String signingAlias) {
return "${request.scheme}://${request.host}:${request.port}" +
NamingConventions.JWSAPPCLIENT_SYSTEM_PREFIX + "/" +
signingAlias + "/" + relativeSystemPath(systemFileURI);
}
private synchronized String relativeSystemPath(final URI systemFileURI) {
return umbrellaRootURI.relativize(systemFileURI).getPath();
}
/**
* Adds content on behalf of a single app client to the HTTP adapters
* for the client and/or the containing EAR.
* <p>
* This method always creates at least one adapter for the client - one to
* receive requests for the user-friendly context path. (This is the
* path either assigned by the developer or defaulted by the server based
* on the application name and, for clients nested within an EAR, the
* URI to the app client within the EAR.) This adapter will only serve the
* main generated JNLP document. All other accesses to files that can be
* downloaded will use the "user-unfriendly" context path.
* <p>
* For stand-alone
* app clients it also creates an adapter for the "user-unfriendly" context
* path. This serves all content other than the main JNLP document.
* <p>
* For nested app clients this method will either create or reuse an
* adapter which serves content for the EAR. This includes content specific
* to the app client being added as well as for library JARs, etc. that
* could be common to more than one app client.
*
* @param appName
* @param contributor
* @param tokens
* @param staticContent
* @param dynamicContent
* @throws EndpointRegistrationException
* @throws IOException
*/
public synchronized void addContentForAppClient(
final String appName,
final String clientURIWithinEAR,
final AppClientServerApplication contributor, final Properties tokens,
final Map<String,StaticContent> staticContent,
final Map<String,DynamicContent> dynamicContent) throws EndpointRegistrationException, IOException {
AppClientHTTPAdapter appAdapter = httpAdapters.get(appName);
if (appAdapter == null) {
/*
* For a stand-alone app client, this is the adapter that will
* serve all the content (except for the main JNLP docuemnt which the
* user-friendly adapter will typically serve). For a nested
* app client, the app adapter is the EAR-level adapter.
*/
appAdapter = addAppAdapter(appName, staticContent, dynamicContent,
tokens, contributor);
} else {
/*
* This must be the 2nd through n-th nested app client in an EAR
* because the app adapter already exists.
*/
appAdapter.addContentIfAbsent(staticContent, dynamicContent);
}
/*
* Add a new adapter for this client's user-friendly context root.
*/
AppClientHTTPAdapter userFriendlyAppAdapter =
addAdapterForUserFriendlyContextRoot(staticContent, dynamicContent,
tokens, contributor);
appClientDeployer.recordContextRoot(appName, clientURIWithinEAR, userFriendlyAppAdapter.contextRoot());
logger.log(Level.FINE, "Registered at context roots {0},{1}",
new Object[]{appAdapter.contextRoot(), userFriendlyAppAdapter.contextRoot()});
addContributorToAppLevelAdapter(appName, contributor);
}
private synchronized AppClientHTTPAdapter createAndRegisterAdapter(
final String contextRoot,
final Map<String,StaticContent> staticContent,
final Map<String,DynamicContent> dynamicContent,
final Properties tokens,
final AppClientServerApplication contributor) throws IOException, EndpointRegistrationException {
final AppClientHTTPAdapter adapter = new AppClientHTTPAdapter(
contextRoot, staticContent,
dynamicContent, tokens,
serverEnv.getDomainRoot(),
new File(installRootURI),
iiopService,
orbFactory);
requestDispatcher.registerEndpoint(
contextRoot,
adapter,
null);
return adapter;
}
private synchronized AppClientHTTPAdapter addAdapterForUserFriendlyContextRoot(
final Map<String,StaticContent> staticContent,
final Map<String,DynamicContent> dynamicContent,
final Properties tokens,
final AppClientServerApplication contributor) throws IOException, EndpointRegistrationException {
final String ufContextRoot = userFriendlyContextRoot(contributor);
return createAndRegisterAdapter(ufContextRoot, staticContent, dynamicContent, tokens, contributor);
}
private synchronized AppClientHTTPAdapter systemAdapter() {
if (systemAdapter == null) {
systemAdapter = startSystemContentAdapter();
}
return systemAdapter;
}
private synchronized AppClientHTTPAdapter addAppAdapter(
final String appName,
final Map<String,StaticContent> staticContent,
final Map<String,DynamicContent> dynamicContent,
final Properties tokens,
final AppClientServerApplication contributor) throws IOException, EndpointRegistrationException {
systemAdapter(); // Make sure it is started
final String contextRoot = NamingConventions.contextRootForAppAdapter(appName);
final AppClientHTTPAdapter adapter = createAndRegisterAdapter(
contextRoot, staticContent,
dynamicContent, tokens, contributor);
httpAdapters.put(appName, adapter);
return adapter;
}
public static String userFriendlyContextRoot(final AppClientServerApplication contributor) {
return userFriendlyContextRoot(contributor.getDescriptor(),
contributor.dc().getAppProps());
}
public static String userFriendlyContextRoot(
final ApplicationClientDescriptor acDesc, final Properties p) {
String ufContextRoot = NamingConventions.defaultUserFriendlyContextRoot(
acDesc);
/*
* See if the context root setting has one for this client. The
* format of the property setting is:
*
* Stand-alone client deployment: javaWebStartContextRoot=contextRoot
* Nested in EAR: javaWebStartContextRoot.uri-to-client-without-.jar=contextRoot
*
* There can be multiple such javaWebStartContextRoot.xxx properties, one for
* each nested app client in the EAR.
*/
String overridingContextRoot = null;
if (acDesc.getApplication().isVirtual()) {
/*
* Stand-alone case.
*/
overridingContextRoot = p.getProperty(JAVA_WEB_START_CONTEXT_ROOT_PROPERTY_NAME);
} else {
/*
* Nested app clients case.
*/
final String uriToNestedClient = NamingConventions.uriToNestedClient(
acDesc);
overridingContextRoot = p.getProperty(
JAVA_WEB_START_CONTEXT_ROOT_PROPERTY_NAME + "." + uriToNestedClient);
}
if (overridingContextRoot != null) {
ufContextRoot = overridingContextRoot;
}
/*
* Grizzly wants the context root to start with a slash.
*/
if ( ! ufContextRoot.startsWith("/")) {
ufContextRoot = "/" + ufContextRoot;
}
return ufContextRoot;
}
public synchronized AutoSignedContent appLevelSignedSystemContent(
final String relativePathToSystemJar,
final String alias) throws FileNotFoundException {
/*
* The key to the map is also the subpath to the file within the
* domain's repository which holds signed system JARs.
*/
final String key = keyToAppLevelSignedSystemContentMap(relativePathToSystemJar, alias);
AutoSignedContent result = appLevelSignedSystemContent.get(key);
if (result == null) {
final File unsignedFile = new File(umbrellaRoot, relativePathToSystemJar);
final File signedFile = new File(systemLevelSignedJARsRoot, key);
result = new AutoSignedContent(unsignedFile, signedFile, alias, jarSigner, relativePathToSystemJar,
MANIFEST_APP_NAME_FOR_SYSTEM_FILES);
appLevelSignedSystemContent.put(key, result);
}
return result;
}
private static String keyToAppLevelSignedSystemContentMap(
final String relativePathToSystemJar,
final String alias) {
return alias + "/" + relativePathToSystemJar;
}
synchronized File signedSystemContentAliasDir(final String alias) {
return new File(systemLevelSignedJARsRoot, alias);
}
public String contextRootForAppAdapter(final String appName) {
final AppClientHTTPAdapter adapter = httpAdapters.get(appName);
if (adapter != null) {
return adapter.contextRoot();
} else {
return null;
}
}
private synchronized void addContributorToAppLevelAdapter(
final String appName,
final AppClientServerApplication contributor) {
/*
* Record that the calling app client server app has contributed content
* to the Grizzly adapter.
*/
Set<AppClientServerApplication> contributorsToAppLevelAdapter = contributingAppClients.get(appName);
if (contributorsToAppLevelAdapter == null) {
contributorsToAppLevelAdapter = new HashSet<AppClientServerApplication>();
contributingAppClients.put(appName, contributorsToAppLevelAdapter);
}
contributorsToAppLevelAdapter.add(contributor);
}
public synchronized void removeContentForAppClient(final String appName,
final String clientURIWithinEAR,
final AppClientServerApplication contributor) throws EndpointRegistrationException {
/*
* Remove the adapter for the user-friendly context root.
*/
removeAdapter(userFriendlyContextRoot(contributor));
removeContributorToAppLevelAdapter(appName, contributor);
appClientDeployer.removeContextRoot(appName, clientURIWithinEAR);
}
private synchronized void removeContributorToAppLevelAdapter(
final String appName,
final AppClientServerApplication contributor) throws EndpointRegistrationException {
/*
* If this is the last contributor for this app-level adapter then
* remove the app-level adapter also.
*/
final Set<AppClientServerApplication> contributorsToAppLevelAdapter = contributingAppClients.get(appName);
if (contributorsToAppLevelAdapter == null) {
return;
}
contributorsToAppLevelAdapter.remove(contributor);
if (contributorsToAppLevelAdapter.isEmpty()) {
contributingAppClients.remove(appName);
removeAdapter(NamingConventions.contextRootForAppAdapter(appName));
httpAdapters.remove(appName);
}
}
private synchronized void removeAdapter(final String contextRoot)
throws EndpointRegistrationException {
requestDispatcher.unregisterEndpoint(contextRoot);
}
private String getGFClientModuleClassPath(final File gfClientJAR) throws IOException {
final JarFile jf = new JarFile(gfClientJAR);
try {
final Manifest mf = jf.getManifest();
Attributes mainAttrs = mf.getMainAttributes();
return mainAttrs.getValue(Attributes.Name.CLASS_PATH);
} finally
{
jf.close();
}
}
}