diff options
Diffstat (limited to 'org.eclipse.gemini.web.core/src/main/java/org')
32 files changed, 3117 insertions, 0 deletions
diff --git a/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/core/ConnectorDescriptor.java b/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/core/ConnectorDescriptor.java new file mode 100644 index 0000000..12cdd82 --- /dev/null +++ b/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/core/ConnectorDescriptor.java @@ -0,0 +1,51 @@ +/******************************************************************************* + * Copyright (c) 2009, 2010 VMware Inc. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at + * http://www.opensource.org/licenses/apache2.0.php. + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * VMware Inc. - initial contribution + *******************************************************************************/ + +package org.eclipse.gemini.web.core; + +/** + * <p> + * ConnectorDescriptor describes a configured connector to the web layer + * </p> + * + * <strong>Concurrent Semantics</strong><br /> + * + * ConnectorDescriptor is thread-safe + * + */ +public interface ConnectorDescriptor { + + /** + * @return The protocol being used, eg. HTTP-1.1 + */ + String getProtocol(); + + /** + * @return The scheme of the connector, eg. http. + */ + String getScheme(); + + /** + * @return The port the connector is listening on, eg. 8080 + */ + int getPort(); + + /** + * @return true iff ssl is enabled + */ + boolean sslEnabled(); + +} diff --git a/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/core/InstallationOptions.java b/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/core/InstallationOptions.java new file mode 100644 index 0000000..b0b5ff0 --- /dev/null +++ b/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/core/InstallationOptions.java @@ -0,0 +1,202 @@ +/******************************************************************************* + * Copyright (c) 2009, 2010 VMware Inc. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at + * http://www.opensource.org/licenses/apache2.0.php. + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * VMware Inc. - initial contribution + *******************************************************************************/ + +package org.eclipse.gemini.web.core; + +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import org.eclipse.gemini.web.internal.WebContainerUtils; +import org.osgi.framework.Constants; + + +/** + * Simple utility class that parses the user-supplied installation options from a {@link Map}. + */ +public final class InstallationOptions { + + private static final class CaseInsensitiveMap extends HashMap<String, String> { + + private static final long serialVersionUID = -514044030419642872L; + + @Override + public String get(Object key) { + if (key instanceof String) { + return super.get(normaliseOption((String) key)); + } + return super.get(key); + } + + @Override + public String put(String key, String value) { + return super.put(normaliseOption(key), value); + } + + private String normaliseOption(String key) { + return key.toLowerCase(Locale.ENGLISH); + } + + } + + private final String bundleSymbolicName; + + private final String bundleVersion; + + private final String bundleManifestVersion; + + private final String bundleClassPath; + + private final String importPackageDeclaration; + + private final String exportPackageDeclaration; + + private final String webContextPath; + + private final String webJSPExtractLocation; + + private volatile boolean defaultWABHeaders; + + /** + * Creates a new <code>InstallationOptions</code> from the supplied <code>options</code> {@link Map}. + * <p/> + * Changes to the <code>options</code> <code>Map</code> are not reflected in the new instance. + * + * @param options the options + */ + public InstallationOptions(Map<String, String> options) { + Map<String, String> normalisedOptions = normalise(options); + + this.bundleSymbolicName = normalisedOptions.get(Constants.BUNDLE_SYMBOLICNAME); + this.bundleVersion = normalisedOptions.get(Constants.BUNDLE_VERSION); + this.bundleManifestVersion = normalisedOptions.get(Constants.BUNDLE_MANIFESTVERSION); + this.bundleClassPath = normalisedOptions.get(Constants.BUNDLE_CLASSPATH); + + this.importPackageDeclaration = normalisedOptions.get(Constants.IMPORT_PACKAGE); + this.exportPackageDeclaration = normalisedOptions.get(Constants.EXPORT_PACKAGE); + + this.webContextPath = normalisedOptions.get(WebContainerUtils.HEADER_WEB_CONTEXT_PATH); + this.webJSPExtractLocation = normalisedOptions.get(WebContainerUtils.HEADER_WEB_JSP_EXTRACT_LOCATION); + + this.defaultWABHeaders = options.get(WebContainerUtils.HEADER_SPRINGSOURCE_DEFAULT_WAB_HEADERS) != null; + } + + private Map<String, String> normalise(Map<String, String> options) { + Set<String> keys = options.keySet(); + Map<String, String> normalisedOptions = new CaseInsensitiveMap(); + for (String key : keys) { + normalisedOptions.put(key, options.get(key)); + } + return normalisedOptions; + } + + /** + * Gets the symbolic name installation option. This option overrides the <code>Bundle-SymbolicName</code> set in the + * web bundle manifest. + * + * @return the symbolic name installation option. + */ + public String getBundleSymbolicName() { + return this.bundleSymbolicName; + } + + /** + * Get the bundle version installation option. This option overrides the <code>Bundle-Version</code> set in the web + * bundle manifest. + * + * @return the bundle version installation option. + */ + public String getBundleVersion() { + return this.bundleVersion; + } + + /** + * Gets the unvalidated bundle manifest version installation option. This option overrides the + * <code>Bundle-ManifestVersion</code> set in the web bundle manifest. + * <p/> + * The bundle manifest version must not be validated early otherwise the IllegalArgumentException will not be turned + * into a BundleException by the OSGi framework. + * + * @return the unvalidated bundle manifest version installation option. + */ + public String getBundleManifestVersion() { + return this.bundleManifestVersion; + } + + /** + * Gets the bundle class path installation option. This option overrides the <code>Bundle-ClassPath</code> set in + * the web bundle manifest. + * + * @return the bundle class path installation option. + */ + public String getBundleClassPath() { + return this.bundleClassPath; + } + + /** + * Gets the import package installation option. + * + * @return the import package installation option. + */ + public String getImportPackageDeclaration() { + return this.importPackageDeclaration; + } + + /** + * Gets the export package installation option. + * + * @return the export package installation option. + */ + public String getExportPackageDeclaration() { + return this.exportPackageDeclaration; + } + + /** + * Gets the web context path installation option. This option overrides the context path specified in the manifest. + * + * @return the web context path installation option. + */ + public String getWebContextPath() { + return this.webContextPath; + } + + /** + * Gets the JSP extract location installation option. This option controls where the servlet container will extract + * application JSPs. + * + * @return the JSP extract location installation option. + */ + public String getWebJSPExtractLocation() { + return this.webJSPExtractLocation; + } + + /** + * Returns whether Web Application Bundle (WAB) manifest headers should be defaulted or not. This should be + * <code>false</code> for strict compliance with the OSGi Web Container specification and <code>true</code> for + * compatibility with the behaviour shipped with dm Server 2.0.0.RELEASE. + * + * @return the default WAB headers installation option. + */ + public boolean getDefaultWABHeaders() { + return this.defaultWABHeaders; + } + + public void setDefaultWABHeaders(boolean defaultWABHeaders) { + this.defaultWABHeaders = defaultWABHeaders; + } + +} diff --git a/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/core/WebApplication.java b/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/core/WebApplication.java new file mode 100644 index 0000000..542cf17 --- /dev/null +++ b/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/core/WebApplication.java @@ -0,0 +1,62 @@ +/******************************************************************************* + * Copyright (c) 2009, 2010 VMware Inc. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at + * http://www.opensource.org/licenses/apache2.0.php. + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * VMware Inc. - initial contribution + *******************************************************************************/ + +package org.eclipse.gemini.web.core; + +import javax.servlet.ServletContext; + +import org.osgi.framework.Bundle; +import org.osgi.service.event.EventAdmin; + +/** + * Represents a web application managed by a {@link WebContainer}. + * + * <p/> + * + * Web applications are created from valid web bundles using {@link WebContainer#createWebApplication(Bundle)}. + * + * + */ +public interface WebApplication { + + /** + * Gets the {@link ServletContext} associated with this web application. + * + * @return the <code>ServletContext</code>, never <code>null</code>. + */ + ServletContext getServletContext(); + + /** + * Gets the {@link ClassLoader} of this web application. + * + * @return the web application's <code>ClassLoader</code>. + */ + ClassLoader getClassLoader(); + + /** + * Starts this web application under the {@link ServletContext#getContextPath() configured context path}. + * <p/> + * If the application fails to start an {@link EventAdmin} event is emitted to the + * <code>org/osgi/services/web/FAILED</code> topic and a {@link WebApplicationStartFailedException} is thrown. + * @throws WebApplicationStartFailedException + */ + void start() throws WebApplicationStartFailedException; + + /** + * Stops this web application. After stop the web application is no longer available to serve content. + */ + void stop(); +} diff --git a/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/core/WebApplicationStartFailedException.java b/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/core/WebApplicationStartFailedException.java new file mode 100644 index 0000000..2b2cd23 --- /dev/null +++ b/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/core/WebApplicationStartFailedException.java @@ -0,0 +1,42 @@ +/******************************************************************************* + * Copyright (c) 2009, 2010 VMware Inc. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at + * http://www.opensource.org/licenses/apache2.0.php. + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * VMware Inc. - initial contribution + *******************************************************************************/ + +package org.eclipse.gemini.web.core; + + +/** + * Thrown to signal that a {@link WebApplication} has failed to {@link WebApplication#start()}. + + * <p /> + * + * <strong>Concurrent Semantics</strong><br /> + * + * Thread-safe. + * + */ +public final class WebApplicationStartFailedException extends RuntimeException { + + private static final long serialVersionUID = -1722479683094175136L; + + /** + * Creates a new WebApplicationStartFailedException with the supplied cause + * @param cause The cause of the failure + */ + public WebApplicationStartFailedException(Throwable cause) { + super(cause); + } + +} diff --git a/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/core/WebBundleManifestTransformer.java b/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/core/WebBundleManifestTransformer.java new file mode 100644 index 0000000..bab96b4 --- /dev/null +++ b/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/core/WebBundleManifestTransformer.java @@ -0,0 +1,48 @@ +/******************************************************************************* + * Copyright (c) 2009, 2010 VMware Inc. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at + * http://www.opensource.org/licenses/apache2.0.php. + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * VMware Inc. - initial contribution + *******************************************************************************/ + +package org.eclipse.gemini.web.core; + +import java.io.IOException; +import java.net.URL; + +import org.eclipse.virgo.util.osgi.manifest.BundleManifest; + +/** + * Strategy for applying transformations to a web bundle manifest. + * <p/> + * The exact set of transformations performed is implementation-dependent, but it is expected that implementations will + * at least support the transformations mandated by the RFC66 specification. + * <p/> + * Transformations that are not defined by the specification should be disabled by default and enabled using an + * {@link InstallationOptions installation option}. + * + * + */ +public interface WebBundleManifestTransformer { + + /** + * Transforms the supplied {@link BundleManifest} in place. + * + * @param manifest the <code>BundleManifest</code> to transform. + * @param sourceURL the {@link URL} the bundle was installed from. + * @param options the {@link InstallationOptions}. May be <code>null</code>. + * @param webBundle whether or not the bundle is deemed to be a bundle as determined by the + * {@link org.eclipse.gemini.web.internal.WebContainerUtils#isWebApplicationBundle isWebApplicationBundle} specification. + * @throws IOException if transformation fails. + */ + void transform(BundleManifest manifest, URL sourceURL, InstallationOptions options, boolean webBundle) throws IOException; +} diff --git a/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/core/WebContainer.java b/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/core/WebContainer.java new file mode 100644 index 0000000..1dd67f7 --- /dev/null +++ b/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/core/WebContainer.java @@ -0,0 +1,143 @@ +/******************************************************************************* + * Copyright (c) 2009, 2010 VMware Inc. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at + * http://www.opensource.org/licenses/apache2.0.php. + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * VMware Inc. - initial contribution + *******************************************************************************/ + +package org.eclipse.gemini.web.core; + +import javax.servlet.ServletContext; + +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleException; +import org.osgi.service.event.EventAdmin; + +/** + * A <code>WebContainer</code> provides a mechanism for creating {@link WebApplication WebApplications} from + * user-supplied web bundles. + * <p/> + * The <code>WebContainer</code> provides programmatic access to the web bundle deployment functionality that is + * typically accessed via the extender. + * + */ +public interface WebContainer { + + /** + * The {@link ServletContext} attribute under which the {@link BundleContext} is available. + */ + public static final String ATTRIBUTE_BUNDLE_CONTEXT = "osgi-bundlecontext"; + + static final String EVENT_NAME_PREFIX = "org/osgi/service/web/"; + + /** + * The {@link EventAdmin} topic for web bundle <code>DEPLOYING</code> events. + */ + static final String EVENT_DEPLOYING = EVENT_NAME_PREFIX + "DEPLOYING"; + + /** + * The {@link EventAdmin} topic for web bundle <code>DEPLOYED</code> events. + */ + static final String EVENT_DEPLOYED = EVENT_NAME_PREFIX + "DEPLOYED"; + + /** + * The {@link EventAdmin} topic for web bundle <code>UNDEPLOYING</code> events. + */ + static final String EVENT_UNDEPLOYING = EVENT_NAME_PREFIX + "UNDEPLOYING"; + + /** + * The {@link EventAdmin} topic for web bundle <code>UNDEPLOYED</code> events. + */ + static final String EVENT_UNDEPLOYED = EVENT_NAME_PREFIX + "UNDEPLOYED"; + + /** + * The {@link EventAdmin} topic for web bundle <code>FAILED</code> events. + */ + static final String EVENT_FAILED = EVENT_NAME_PREFIX + "FAILED"; + + /** + * The {@link org.osgi.service.event.Event Event} property for the web application bundle's context path. + */ + static final String EVENT_PROPERTY_CONTEXT_PATH = "context.path"; + + /** + * The {@link org.osgi.service.event.Event Event} property for the web application bundle's version. + */ + static final String EVENT_PROPERTY_BUNDLE_VERSION = "bundle.version"; + + /** + * The {@link org.osgi.service.event.Event Event} property for the web container extender bundle. + */ + static final String EVENT_PROPERTY_EXTENDER_BUNDLE = "extender.bundle"; + + /** + * The {@link org.osgi.service.event.Event Event} property for the web container extender bundle's id. + */ + static final String EVENT_PROPERTY_EXTENDER_BUNDLE_ID = "extender.bundle.id"; + + /** + * The {@link org.osgi.service.event.Event Event} property for the web container extender bundle's symbolic name. + */ + static final String EVENT_PROPERTY_EXTENDER_BUNDLE_SYMBOLICNAME = "extender.bundle.symbolicName"; + + /** + * The {@link org.osgi.service.event.Event Event} property for the web container extender bundle's version. + */ + static final String EVENT_PROPERTY_EXTENDER_BUNDLE_VERSION = "extender.bundle.version"; + + /** + * The {@link org.osgi.service.event.Event Event} property containing a Web-ContextPath which is shared by more than one bundle. + */ + static final String EVENT_PROPERTY_COLLISION = "collision"; + + /** + * The {@link org.osgi.service.event.Event Event} property listing, in the case of a collsion, the bundle ids, as a + * <code>Collection<Long></code>, which share the same Web-ContextPath. + */ + static final String EVENT_PROPERTY_COLLISION_BUNDLES = "collision.bundles"; + + /** + * Creates a {@link WebApplication} for the supplied web bundle. Equivalent to calling + * {@link #createWebApplication(Bundle, Bundle) createWebApplication(bundle, null)}. + * + * @param bundle the web bundle + * + * @return the newly created <code>WebApplication</code>. + * @throws BundleException if the <code>WebApplication</code> cannot be created. + */ + WebApplication createWebApplication(Bundle bundle) throws BundleException; + + /** + * Creates a {@link WebApplication} for the supplied web bundle. + * + * @param bundle the web bundle + * @param extender the extender bundle that has trigger the creation of the web application, or <code>null</code> if + * an extender is not involved. + * @return the newly created <code>WebApplication</code>. + * @throws BundleException if the <code>WebApplication</code> cannot be created. + */ + WebApplication createWebApplication(Bundle bundle, Bundle extender) throws BundleException; + + /** + * Checks to see if the supplied {@link Bundle} is a valid web bundle. + * + * @param bundle the bundle to check. + * @return <code>true</code> if the supplied bundle is a valid web bundle; otherwise <code>false</code>. + */ + boolean isWebBundle(Bundle bundle); + + /** + * Stops the web container. + */ + public void halt(); +} diff --git a/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/core/WebContainerProperties.java b/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/core/WebContainerProperties.java new file mode 100644 index 0000000..a2a099c --- /dev/null +++ b/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/core/WebContainerProperties.java @@ -0,0 +1,41 @@ +/******************************************************************************* + * Copyright (c) 2009, 2010 VMware Inc. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at + * http://www.opensource.org/licenses/apache2.0.php. + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * VMware Inc. - initial contribution + *******************************************************************************/ + +package org.eclipse.gemini.web.core; + +import java.util.Set; + +/** + * <p> + * WebContainerProperties allows applications running on this RFC66 + * implementation to obtain the properties used to configure it. + * </p> + * + * <strong>Concurrent Semantics</strong><br /> + * + * thread-safe + * + */ +public interface WebContainerProperties { + + /** + * The port that this webcontainer is listening on. + * + * @return the port number + */ + Set<ConnectorDescriptor> getConnectorDescriptors(); + +} diff --git a/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/core/spi/ContextPathExistsException.java b/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/core/spi/ContextPathExistsException.java new file mode 100644 index 0000000..b089f13 --- /dev/null +++ b/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/core/spi/ContextPathExistsException.java @@ -0,0 +1,38 @@ +/******************************************************************************* + * Copyright (c) 2009, 2010 VMware Inc. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at + * http://www.opensource.org/licenses/apache2.0.php. + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * VMware Inc. - initial contribution + *******************************************************************************/ + +package org.eclipse.gemini.web.core.spi; + +/** + * Exception signalling that a context path referred to by a web application is already in use by another web + * application. + * + */ +public class ContextPathExistsException extends ServletContainerException { + + private static final long serialVersionUID = 1846326281773843365L; + + private final String contextPath; + + public ContextPathExistsException(String contextPath) { + super("Context path '" + contextPath + "' already exists"); + this.contextPath = contextPath; + } + + public String getContextPath() { + return contextPath; + } +} diff --git a/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/core/spi/ServletContainer.java b/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/core/spi/ServletContainer.java new file mode 100644 index 0000000..4656c5a --- /dev/null +++ b/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/core/spi/ServletContainer.java @@ -0,0 +1,50 @@ +/******************************************************************************* + * Copyright (c) 2009, 2010 VMware Inc. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at + * http://www.opensource.org/licenses/apache2.0.php. + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * VMware Inc. - initial contribution + *******************************************************************************/ + +package org.eclipse.gemini.web.core.spi; + +import org.osgi.framework.Bundle; + +public interface ServletContainer { + + /** + * Creates a web application for the supplied {@link Bundle}. + * + * @param contextPath the context path the web application should run under. + * @param bundle the <code>Bundle</code> containing the web application content. + * @return a handle to the web application that can be used to drive lifecycle events. + * @throws ServletContainerException if the web application cannot be created. + */ + WebApplicationHandle createWebApplication(String contextPath, Bundle bundle); + + /** + * Starts the web application referred to by the supplied {@link WebApplicationHandle}. + * + * @param handle the handle to the web application to start. + * @throws ContextPathExistsException if the context path is already in use. + * @throws ServletContainerException if the web application fails to start. + */ + void startWebApplication(WebApplicationHandle handle); + + /** + * Stops the web application referred to by the supplied {@link WebApplicationHandle}. + * + * @param handle the handle to the web application to stop. + * @throws ServletContainerException if the web application fails to stop. + */ + void stopWebApplication(WebApplicationHandle handle); + +} diff --git a/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/core/spi/ServletContainerException.java b/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/core/spi/ServletContainerException.java new file mode 100644 index 0000000..5cf885e --- /dev/null +++ b/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/core/spi/ServletContainerException.java @@ -0,0 +1,40 @@ +/******************************************************************************* + * Copyright (c) 2009, 2010 VMware Inc. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at + * http://www.opensource.org/licenses/apache2.0.php. + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * VMware Inc. - initial contribution + *******************************************************************************/ + +package org.eclipse.gemini.web.core.spi; + + +public class ServletContainerException extends RuntimeException { + + private static final long serialVersionUID = -4955082179218908312L; + + public ServletContainerException() { + } + + public ServletContainerException(String message, Throwable cause) { + super(message, cause); + } + + public ServletContainerException(String message) { + super(message); + } + + public ServletContainerException(Throwable cause) { + super(cause); + } + + +} diff --git a/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/core/spi/WebApplicationHandle.java b/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/core/spi/WebApplicationHandle.java new file mode 100644 index 0000000..ffd89ec --- /dev/null +++ b/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/core/spi/WebApplicationHandle.java @@ -0,0 +1,49 @@ +/******************************************************************************* + * Copyright (c) 2009, 2010 VMware Inc. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at + * http://www.opensource.org/licenses/apache2.0.php. + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * VMware Inc. - initial contribution + *******************************************************************************/ + +package org.eclipse.gemini.web.core.spi; + +import javax.servlet.ServletContext; + +/** + * Handle to a web application deployed in a {@link ServletContainer}. <code>ServletContainer</code> implementations + * will create custom subclasses of this interface and return them from {@link ServletContainer#createWebApplication}. + * The <code>ServletContainer</code> can store any state need during {@link ServletContainer#startWebApplication(WebApplicationHandle) start} and + * {@link ServletContainer#stopWebApplication(WebApplicationHandle) stop} in this custom implementation. + * <p/> + * Client code <strong>must</strong> return the correct handle to the <code>ServletContainer</code> when starting or + * stopping an application. + * + */ +public interface WebApplicationHandle { + + /** + * Gets the {@link ServletContext} of the deployed web application. + * + * @return the <code>ServletContext</code>. + */ + ServletContext getServletContext(); + + /** + * Gets the {@link ClassLoader} of the deployed web application. May be <code>null</code> if the + * web application has not yet been {@link ServletContainer#startWebApplication(WebApplicationHandle) + * started}. + * + * @return the <code>ClassLoader</code>. + */ + ClassLoader getClassLoader(); + +} diff --git a/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/internal/EventManager.java b/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/internal/EventManager.java new file mode 100644 index 0000000..96928fe --- /dev/null +++ b/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/internal/EventManager.java @@ -0,0 +1,156 @@ +/******************************************************************************* + * Copyright (c) 2009, 2010 VMware Inc. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at + * http://www.opensource.org/licenses/apache2.0.php. + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * VMware Inc. - initial contribution + *******************************************************************************/ + +package org.eclipse.gemini.web.internal; + +import static org.eclipse.gemini.web.core.WebContainer.EVENT_DEPLOYED; +import static org.eclipse.gemini.web.core.WebContainer.EVENT_DEPLOYING; +import static org.eclipse.gemini.web.core.WebContainer.EVENT_FAILED; +import static org.eclipse.gemini.web.core.WebContainer.EVENT_PROPERTY_BUNDLE_VERSION; +import static org.eclipse.gemini.web.core.WebContainer.EVENT_PROPERTY_COLLISION; +import static org.eclipse.gemini.web.core.WebContainer.EVENT_PROPERTY_COLLISION_BUNDLES; +import static org.eclipse.gemini.web.core.WebContainer.EVENT_PROPERTY_CONTEXT_PATH; +import static org.eclipse.gemini.web.core.WebContainer.EVENT_PROPERTY_EXTENDER_BUNDLE; +import static org.eclipse.gemini.web.core.WebContainer.EVENT_PROPERTY_EXTENDER_BUNDLE_ID; +import static org.eclipse.gemini.web.core.WebContainer.EVENT_PROPERTY_EXTENDER_BUNDLE_SYMBOLICNAME; +import static org.eclipse.gemini.web.core.WebContainer.EVENT_PROPERTY_EXTENDER_BUNDLE_VERSION; +import static org.eclipse.gemini.web.core.WebContainer.EVENT_UNDEPLOYED; +import static org.eclipse.gemini.web.core.WebContainer.EVENT_UNDEPLOYING; + +import java.util.Collection; +import java.util.Collections; +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.Set; + +import org.eclipse.gemini.web.internal.template.ServiceCallback; +import org.eclipse.gemini.web.internal.template.ServiceTemplate; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.service.event.Event; +import org.osgi.service.event.EventAdmin; +import org.osgi.service.event.EventConstants; + + +/** + * TODO: Need a better implementation of this - abstraction is poor :) + * + */ +final class EventManager { + + private final ServiceTemplate<EventAdmin> template; + + public EventManager(BundleContext context) { + if (isEventAdminAvailable()) { + this.template = new ServiceTemplate<EventAdmin>(context, EventAdmin.class); + } else { + this.template = null; + } + } + + public void start() { + if (this.template != null) { + this.template.start(); + } + } + + public void stop() { + if (this.template != null) { + this.template.stop(); + } + } + + public void sendDeploying(Bundle applicationBundle, Bundle extenderBundle, String contextPath) { + sendEvent(EVENT_DEPLOYING, applicationBundle, extenderBundle, contextPath, null, null, null); + } + + public void sendDeployed(Bundle applicationBundle, Bundle extenderBundle, String contextPath) { + sendEvent(EVENT_DEPLOYED, applicationBundle, extenderBundle, contextPath, null, null, null); + } + + public void sendUndeploying(Bundle applicationBundle, Bundle extenderBundle, String contextPath) { + sendEvent(EVENT_UNDEPLOYING, applicationBundle, extenderBundle, contextPath, null, null, null); + } + + public void sendUndeployed(Bundle applicationBundle, Bundle extenderBundle, String contextPath) { + sendEvent(EVENT_UNDEPLOYED, applicationBundle, extenderBundle, contextPath, null, null, null); + } + + public void sendFailed(Bundle applicationBundle, Bundle extenderBundle, String contextPath, Exception ex, String collidingWebContextPath, + Set<Long> collisionBundles) { + sendEvent(EVENT_FAILED, applicationBundle, extenderBundle, contextPath, ex, collidingWebContextPath, collisionBundles); + } + + private void sendEvent(final String eventName, final Bundle applicationBundle, final Bundle extenderBundle, final String contextPath, + final Throwable ex, final String collidingWebContextPath, final Set<Long> collisionBundles) { + if (this.template != null) { + this.template.executeWithService(new ServiceCallback<EventAdmin, Void>() { + + public Void doWithService(EventAdmin eventAdmin) { + Dictionary<String, Object> props = new Hashtable<String, Object>(); + if (applicationBundle.getSymbolicName() != null) { + props.put(EventConstants.BUNDLE_SYMBOLICNAME, applicationBundle.getSymbolicName()); + } + props.put(EventConstants.BUNDLE_ID, applicationBundle.getBundleId()); + props.put(EventConstants.BUNDLE, applicationBundle); + props.put(EVENT_PROPERTY_BUNDLE_VERSION, applicationBundle.getVersion()); + props.put(EventConstants.TIMESTAMP, System.currentTimeMillis()); + props.put(EVENT_PROPERTY_CONTEXT_PATH, contextPath); + + if (extenderBundle != null) { + props.put(EVENT_PROPERTY_EXTENDER_BUNDLE, extenderBundle); + props.put(EVENT_PROPERTY_EXTENDER_BUNDLE_ID, extenderBundle.getBundleId()); + if (extenderBundle.getSymbolicName() != null) { + props.put(EVENT_PROPERTY_EXTENDER_BUNDLE_SYMBOLICNAME, extenderBundle.getSymbolicName()); + } + props.put(EVENT_PROPERTY_EXTENDER_BUNDLE_VERSION, extenderBundle.getVersion()); + } + + if (ex != null) { + props.put(EventConstants.EXCEPTION, ex); + } + + if (collidingWebContextPath != null) { + props.put(EVENT_PROPERTY_COLLISION, collidingWebContextPath); + + /* + * Prevent event handlers modifying the set of collision bundles. + * + * Note: OSGi specs prefer Collection to Set even when there cannot be duplicates. + */ + Collection<Long> immutableCollisionBundles = Collections.unmodifiableCollection(collisionBundles); + props.put(EVENT_PROPERTY_COLLISION_BUNDLES, immutableCollisionBundles); + } + + eventAdmin.sendEvent(new Event(eventName, props)); + return null; + } + + }); + } + } + + private boolean isEventAdminAvailable() { + try { + getClass().getClassLoader().loadClass(EventAdmin.class.getName()); + return true; + } catch (NoClassDefFoundError ex) { + return false; + } catch (ClassNotFoundException ex) { + return false; + } + } +} diff --git a/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/internal/StandardWebApplication.java b/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/internal/StandardWebApplication.java new file mode 100644 index 0000000..9f88dd1 --- /dev/null +++ b/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/internal/StandardWebApplication.java @@ -0,0 +1,160 @@ +/******************************************************************************* + * Copyright (c) 2009, 2010 VMware Inc. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at + * http://www.opensource.org/licenses/apache2.0.php. + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * VMware Inc. - initial contribution + *******************************************************************************/ + +package org.eclipse.gemini.web.internal; + +import java.util.HashSet; +import java.util.Properties; +import java.util.Set; + +import javax.servlet.ServletContext; + +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +import org.eclipse.gemini.web.core.WebApplication; +import org.eclipse.gemini.web.core.WebApplicationStartFailedException; +import org.eclipse.gemini.web.core.spi.ServletContainer; +import org.eclipse.gemini.web.core.spi.ServletContainerException; +import org.eclipse.gemini.web.core.spi.WebApplicationHandle; +import org.eclipse.virgo.util.osgi.ServiceRegistrationTracker; + +final class StandardWebApplication implements WebApplication { + + private static final Logger LOGGER = LoggerFactory.getLogger(StandardWebApplication.class); + + private final BundleContext bundleContext; + + private final Bundle extender; + + private final WebApplicationHandle handle; + + private final ServletContainer container; + + private final ServiceRegistrationTracker tracker = new ServiceRegistrationTracker(); + + private final EventManager eventManager; + + private boolean started = false; + + private final Object monitor = new Object(); + + private final WebApplicationStartFailureRetryController retryController; + + public StandardWebApplication(BundleContext bundleContext, Bundle extender, WebApplicationHandle handle, ServletContainer container, + EventManager eventManager, WebApplicationStartFailureRetryController retryController) { + this.bundleContext = bundleContext; + this.extender = extender; + this.handle = handle; + this.container = container; + this.eventManager = eventManager; + this.retryController = retryController; + } + + public ServletContext getServletContext() { + return this.handle.getServletContext(); + } + + public ClassLoader getClassLoader() { + return this.handle.getClassLoader(); + } + + public void start() { + boolean localStarted; + + synchronized (this.monitor) { + localStarted = this.started; + } + + if (!localStarted) { + String webContextPath = getContextPath(); + this.eventManager.sendDeploying(getBundle(), this.extender, webContextPath); + + try { + this.container.startWebApplication(this.handle); + publishServletContext(); + + synchronized (this.monitor) { + this.started = true; + } + + this.eventManager.sendDeployed(getBundle(), this.extender, webContextPath); + } catch (ServletContainerException ex) { + if (LOGGER.isErrorEnabled()) { + LOGGER.error("Failed to start web application at bundleContext path '" + this.handle.getServletContext().getContextPath() + "'", ex); + } + this.retryController.recordFailure(this); + Set<Long> webContextPathBundleIds = getWebContextPathBundleIds(webContextPath); + boolean collision = webContextPathBundleIds.size() > 1; + this.eventManager.sendFailed(getBundle(), this.extender, webContextPath, ex, collision ? webContextPath : null, + collision ? webContextPathBundleIds : null); + throw new WebApplicationStartFailedException(ex); + } + } + } + + private Set<Long> getWebContextPathBundleIds(String webContextPath) { + Set<Long> bundleIds = new HashSet<Long>(); + for (Bundle bundle : this.bundleContext.getBundles()) { + if (webContextPath.equals(WebContainerUtils.getContextPath(bundle))) { + bundleIds.add(bundle.getBundleId()); + } + } + return bundleIds; + } + + public void stop() { + boolean localStarted; + + synchronized (this.monitor) { + localStarted = this.started; + this.started = false; + } + + if (localStarted) { + this.eventManager.sendUndeploying(getBundle(), this.extender, getContextPath()); + this.container.stopWebApplication(this.handle); + this.tracker.unregisterAll(); + this.eventManager.sendUndeployed(getBundle(), this.extender, getContextPath()); + } + this.retryController.retryFailures(this); + } + + private void publishServletContext() { + Properties properties = constructServletContextProperties(); + this.tracker.track(this.bundleContext.registerService(ServletContext.class.getName(), getServletContext(), properties)); + } + + String getContextPath() { + return this.handle.getServletContext().getContextPath(); + } + + Bundle getBundle() { + return this.bundleContext.getBundle(); + } + + private Properties constructServletContextProperties() { + Properties properties = new Properties(); + Bundle bundle = getBundle(); + WebContainerUtils.setServletContextBundleProperties(properties, bundle); + properties.setProperty("osgi.web.contextpath", getServletContext().getContextPath()); + return properties; + } + +} diff --git a/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/internal/StandardWebContainer.java b/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/internal/StandardWebContainer.java new file mode 100644 index 0000000..9662423 --- /dev/null +++ b/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/internal/StandardWebContainer.java @@ -0,0 +1,79 @@ +/******************************************************************************* + * Copyright (c) 2009, 2010 VMware Inc. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at + * http://www.opensource.org/licenses/apache2.0.php. + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * VMware Inc. - initial contribution + *******************************************************************************/ + +package org.eclipse.gemini.web.internal; + +import org.eclipse.gemini.web.core.WebApplication; +import org.eclipse.gemini.web.core.WebContainer; +import org.eclipse.gemini.web.core.spi.ServletContainer; +import org.eclipse.gemini.web.core.spi.ServletContainerException; +import org.eclipse.gemini.web.core.spi.WebApplicationHandle; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +/** + * Standard implementation of {@link WebContainer}. + */ +final class StandardWebContainer implements WebContainer { + + private static final Logger LOGGER = LoggerFactory.getLogger(StandardWebContainer.class); + + private final EventManager eventManager; + + private final ServletContainer servletContainer; + + private final WebApplicationStartFailureRetryController retryController = new WebApplicationStartFailureRetryController(); + + public StandardWebContainer(ServletContainer servletContainer, EventManager eventManager) { + this.servletContainer = servletContainer; + this.eventManager = eventManager; + } + + public WebApplication createWebApplication(Bundle bundle) throws BundleException { + return this.createWebApplication(bundle, null); + } + + public WebApplication createWebApplication(Bundle bundle, Bundle extender) throws BundleException { + if (!isWebBundle(bundle)) { + throw new BundleException("Bundle '" + bundle + "' is not a valid web bundle."); + } + try { + WebApplicationHandle handle = this.servletContainer.createWebApplication(WebContainerUtils.getContextPath(bundle), bundle); + handle.getServletContext().setAttribute(ATTRIBUTE_BUNDLE_CONTEXT, bundle.getBundleContext()); + return new StandardWebApplication(bundle.getBundleContext(), extender, handle, this.servletContainer, this.eventManager, this.retryController); + } catch (ServletContainerException ex) { + if (LOGGER.isErrorEnabled()) { + LOGGER.error("Failed to create web application for bundle '" + bundle + "'", ex); + } + throw new BundleException("Failed to create web application for bundle '" + bundle + "'", ex); + } + } + + public boolean isWebBundle(Bundle bundle) { + return WebContainerUtils.isWebBundle(bundle); + } + + /** + * {@inheritDoc} + */ + public void halt() { + this.retryController.clear(); + } + +} diff --git a/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/internal/SystemBundleExportsResolver.java b/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/internal/SystemBundleExportsResolver.java new file mode 100644 index 0000000..9c0ddf1 --- /dev/null +++ b/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/internal/SystemBundleExportsResolver.java @@ -0,0 +1,79 @@ +/******************************************************************************* + * Copyright (c) 2009, 2010 VMware Inc. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at + * http://www.opensource.org/licenses/apache2.0.php. + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * VMware Inc. - initial contribution + *******************************************************************************/ + +package org.eclipse.gemini.web.internal; + +import java.util.HashMap; +import java.util.Map; + +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Version; +import org.osgi.service.packageadmin.ExportedPackage; +import org.osgi.service.packageadmin.PackageAdmin; + +import org.eclipse.gemini.web.internal.template.ServiceCallback; +import org.eclipse.gemini.web.internal.template.ServiceTemplate; +import org.eclipse.virgo.util.osgi.VersionRange; + +final class SystemBundleExportsResolver { + + private final BundleContext bundleContext; + + SystemBundleExportsResolver(BundleContext bundleContext) { + this.bundleContext = bundleContext; + } + + public Map<String, VersionRange> getSystemBundleExports() { + final Bundle systemBundle = bundleContext.getBundle(0); + ServiceTemplate<PackageAdmin> packageAdminTemplate = new ServiceTemplate<PackageAdmin>(bundleContext, PackageAdmin.class); + packageAdminTemplate.start(); + ExportedPackage[] systemBundleExports = packageAdminTemplate.executeWithService(new ServiceCallback<PackageAdmin, ExportedPackage[]>() { + public ExportedPackage[] doWithService(PackageAdmin packageAdmin) { + return packageAdmin.getExportedPackages(systemBundle); + } + }); + packageAdminTemplate.stop(); + return combineDuplicateExports(systemBundleExports); + } + + static Map<String, VersionRange> combineDuplicateExports(ExportedPackage[] allExportedPackages) { + Map<String, VersionRange> exportedPackages = new HashMap<String, VersionRange>(); + for (ExportedPackage exportedPackage : allExportedPackages) { + VersionRange versionRange = exportedPackages.get(exportedPackage.getName()); + if (versionRange == null) { + versionRange = VersionRange.createExactRange(exportedPackage.getVersion()); + } else { + Version version = exportedPackage.getVersion(); + if (!versionRange.includes(version)) { + versionRange = expandVersionRange(version, versionRange); + } + } + exportedPackages.put(exportedPackage.getName(), versionRange); + } + + return exportedPackages; + } + + private static VersionRange expandVersionRange(Version version, VersionRange versionRange) { + Version ceiling = versionRange.getCeiling(); + if (version.compareTo(ceiling) > 0) { + return new VersionRange("[" + versionRange.getFloor() + "," + version + "]"); + } else { + return new VersionRange("[" + version + "," + versionRange.getCeiling() + "]"); + } + } +} diff --git a/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/internal/WebApplicationStartFailureRetryController.java b/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/internal/WebApplicationStartFailureRetryController.java new file mode 100644 index 0000000..ae9fb33 --- /dev/null +++ b/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/internal/WebApplicationStartFailureRetryController.java @@ -0,0 +1,104 @@ +/******************************************************************************* + * Copyright (c) 2009, 2010 VMware Inc. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at + * http://www.opensource.org/licenses/apache2.0.php. + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * VMware Inc. - initial contribution + *******************************************************************************/ + +package org.eclipse.gemini.web.internal; + +import java.util.Comparator; +import java.util.HashSet; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.eclipse.gemini.web.core.WebApplication; +import org.eclipse.gemini.web.core.WebApplicationStartFailedException; + + + +final class WebApplicationStartFailureRetryController { + + private final Object monitor = new Object(); + + private final ConcurrentMap<String, Set<StandardWebApplication>> failures = new ConcurrentHashMap<String, Set<StandardWebApplication>>(); + + void recordFailure(StandardWebApplication failedWebApplication) { + String contextPath = failedWebApplication.getContextPath(); + if (contextPath != null) { + addFailureForWebContextPath(contextPath, failedWebApplication); + } + } + + private void addFailureForWebContextPath(String contextPath, StandardWebApplication failedWebApplication) { + Set<StandardWebApplication> contextFailures = this.failures.get(contextPath); + if (contextFailures == null) { + contextFailures = new HashSet<StandardWebApplication>(); + Set<StandardWebApplication> previousContextFailures = this.failures.putIfAbsent(contextPath, contextFailures); + if (previousContextFailures != null) { + contextFailures = previousContextFailures; + } + } + synchronized (this.monitor) { + contextFailures.add(failedWebApplication); + } + } + + void retryFailures(StandardWebApplication stoppedWebApplication) { + String contextPath = stoppedWebApplication.getContextPath(); + if (contextPath != null) { + Set<StandardWebApplication> contextFailures = removeFailuresForWebContextPath(contextPath); + contextFailures.remove(stoppedWebApplication); + for (WebApplication failedWebApplication : contextFailures) { + try { + failedWebApplication.start(); + } catch (WebApplicationStartFailedException _) { + // ignore as the web application will have been added to the new contextFailures set + } + } + } + } + + private Set<StandardWebApplication> removeFailuresForWebContextPath(String contextPath) { + Set<StandardWebApplication> sortedContextFailures = createSetSortedByBundleId(); + Set<StandardWebApplication> contextFailures = this.failures.remove(contextPath); + if (contextFailures != null) { + sortedContextFailures.addAll(contextFailures); + } + return sortedContextFailures; + + } + + private Set<StandardWebApplication> createSetSortedByBundleId() { + return new TreeSet<StandardWebApplication>(new Comparator<StandardWebApplication>() { + + public int compare(StandardWebApplication wa1, StandardWebApplication wa2) { + long id1 = wa1.getBundle().getBundleId(); + long id2 = wa2.getBundle().getBundleId(); + if (id1 < id2) { + return -1; + } else if (id1 > id2) { + return 1; + } else { + return 0; + } + } + }); + } + + void clear() { + this.failures.clear(); + } + +} diff --git a/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/internal/WebContainerActivator.java b/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/internal/WebContainerActivator.java new file mode 100644 index 0000000..f53e524 --- /dev/null +++ b/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/internal/WebContainerActivator.java @@ -0,0 +1,130 @@ +/******************************************************************************* + * Copyright (c) 2009, 2010 VMware Inc. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at + * http://www.opensource.org/licenses/apache2.0.php. + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * VMware Inc. - initial contribution + *******************************************************************************/ + +package org.eclipse.gemini.web.internal; + +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.Map; + +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.url.URLConstants; +import org.osgi.service.url.URLStreamHandlerService; +import org.osgi.util.tracker.ServiceTracker; +import org.osgi.util.tracker.ServiceTrackerCustomizer; + + +import org.eclipse.gemini.web.core.WebBundleManifestTransformer; +import org.eclipse.gemini.web.core.WebContainer; +import org.eclipse.gemini.web.core.spi.ServletContainer; +import org.eclipse.gemini.web.internal.url.ChainingWebBundleManifestTransformer; +import org.eclipse.gemini.web.internal.url.DefaultsWebBundleManifestTransformer; +import org.eclipse.gemini.web.internal.url.SpecificationWebBundleManifestTransformer; +import org.eclipse.gemini.web.internal.url.SystemBundleExportsImportingWebBundleManifestTransformer; +import org.eclipse.gemini.web.internal.url.WebBundleUrl; +import org.eclipse.gemini.web.internal.url.WebBundleUrlStreamHandlerService; +import org.eclipse.virgo.util.osgi.ServiceRegistrationTracker; +import org.eclipse.virgo.util.osgi.VersionRange; + +public class WebContainerActivator implements BundleActivator { + + private final ServiceRegistrationTracker regTracker = new ServiceRegistrationTracker(); + + private volatile EventManager eventManager; + + private ServiceTracker serviceTracker; + + public void start(BundleContext context) throws Exception { + WebBundleManifestTransformer transformer = registerWebBundleManifestTransformer(context); + + registerUrlStreamHandler(context, transformer); + + this.eventManager = new EventManager(context); + this.eventManager.start(); + + this.serviceTracker = new ServiceTracker(context, ServletContainer.class.getName(), new ServletContainerTracker(context, this.eventManager)); + this.serviceTracker.open(); + } + + public void stop(BundleContext context) throws Exception { + this.serviceTracker.close(); + this.regTracker.unregisterAll(); + this.eventManager.stop(); + } + + private WebBundleManifestTransformer registerWebBundleManifestTransformer(BundleContext context) { + + WebBundleManifestTransformer specTransformer = new SpecificationWebBundleManifestTransformer(); + WebBundleManifestTransformer defaultsTransformer = new DefaultsWebBundleManifestTransformer(); + Map<String, VersionRange> systemBundleExports = new SystemBundleExportsResolver(context).getSystemBundleExports(); + WebBundleManifestTransformer systemBundleExportImportingWebBundleManifestTransformer = new SystemBundleExportsImportingWebBundleManifestTransformer(systemBundleExports); + + WebBundleManifestTransformer chainingTransformer = new ChainingWebBundleManifestTransformer(specTransformer, defaultsTransformer, systemBundleExportImportingWebBundleManifestTransformer); + + ServiceRegistration reg = context.registerService(WebBundleManifestTransformer.class.getName(), chainingTransformer, null); + this.regTracker.track(reg); + + return chainingTransformer; + } + + + private void registerUrlStreamHandler(BundleContext context, WebBundleManifestTransformer transformer) { + Dictionary<String, Object> props = new Hashtable<String, Object>(); + props.put(URLConstants.URL_HANDLER_PROTOCOL, WebBundleUrl.SCHEME); + + ServiceRegistration reg = context.registerService(URLStreamHandlerService.class.getName(), new WebBundleUrlStreamHandlerService(transformer), props); + this.regTracker.track(reg); + } + + private static final class ServletContainerTracker implements ServiceTrackerCustomizer { + + private final ServiceRegistrationTracker regTracker = new ServiceRegistrationTracker(); + + private final BundleContext context; + + private final EventManager eventManager; + + public ServletContainerTracker(BundleContext context, EventManager eventManager) { + this.context = context; + this.eventManager = eventManager; + } + + public Object addingService(ServiceReference reference) { + ServletContainer container = (ServletContainer) this.context.getService(reference); + + WebContainer webContainer = new StandardWebContainer(container, this.eventManager); + + ServiceRegistration reg = context.registerService(WebContainer.class.getName(), webContainer, null); + this.regTracker.track(reg); + + return webContainer; + } + + public void modifiedService(ServiceReference reference, Object service) { + } + + public void removedService(ServiceReference reference, Object service) { + this.regTracker.unregisterAll(); + if (service instanceof WebContainer) { + ((WebContainer)service).halt(); + } + } + + } +} diff --git a/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/internal/WebContainerUtils.java b/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/internal/WebContainerUtils.java new file mode 100644 index 0000000..f114467 --- /dev/null +++ b/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/internal/WebContainerUtils.java @@ -0,0 +1,207 @@ +/******************************************************************************* + * Copyright (c) 2009, 2010 VMware Inc. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at + * http://www.opensource.org/licenses/apache2.0.php. + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * VMware Inc. - initial contribution + *******************************************************************************/ + +package org.eclipse.gemini.web.internal; + +import java.net.URL; +import java.util.Locale; +import java.util.Properties; + +import org.osgi.framework.Bundle; +import org.osgi.framework.Constants; + +import org.eclipse.virgo.util.osgi.manifest.BundleManifest; + +public final class WebContainerUtils { + + public static final String WEB_BUNDLE_SCHEME = "webbundle"; + + /** + * Constant for the <code>Web-ContextPath</code> manifest header. + */ + public static final String HEADER_WEB_CONTEXT_PATH = "Web-ContextPath"; + + /** + * Constant for the <code>SpringSource-SystemPackages</code> manifest header. + */ + public static final String HEADER_SPRINGSOURCE_DEFAULT_WAB_HEADERS = "SpringSource-DefaultWABHeaders"; + + /** + * Constant for the <code>Web-JSPExtractLocation</code> manifest header. + */ + public static final String HEADER_WEB_JSP_EXTRACT_LOCATION = "Web-JSPExtractLocation"; + + static final String ENTRY_WEB_XML = "/WEB-INF/web.xml"; + + static final String WAR_EXTENSION = ".war"; + + static final String OSGI_WEB_VERSION = "osgi.web.version"; + + static final String OSGI_WEB_SYMBOLICNAME = "osgi.web.symbolicname"; + + static final String BUNDLE_VERSION_HEADER = "bundle-version"; + + private WebContainerUtils() { + } + + public static boolean isWebBundle(Bundle bundle) { + return hasWarExtension(bundle) || hasWarScheme(bundle) || hasWebContextPath(bundle) || hasWebXml(bundle); + } + + private static boolean hasWarExtension(Bundle bundle) { + String lowerCaseLocation = bundle.getLocation().toLowerCase(Locale.ENGLISH); + while (lowerCaseLocation.endsWith("/")) { + lowerCaseLocation = lowerCaseLocation.substring(0, lowerCaseLocation.length() - 1); + } + return lowerCaseLocation.endsWith(WAR_EXTENSION); + } + + private static boolean hasWarScheme(Bundle bundle) { + return bundle.getLocation().startsWith(WEB_BUNDLE_SCHEME); + } + + private static boolean hasWebContextPath(Bundle bundle) { + return getWebContextPathHeader(bundle) != null; + } + + private static String getWebContextPathHeader(Bundle bundle) { + return (String) bundle.getHeaders().get(HEADER_WEB_CONTEXT_PATH); + } + + private static boolean hasWebXml(Bundle bundle) { + return bundle.getEntry(ENTRY_WEB_XML) != null; + } + + public static String getContextPath(Bundle bundle) { + String contextPath = getWebContextPathHeader(bundle); + + if (contextPath == null) { + contextPath = getBaseName(bundle.getLocation()); + } + + return contextPath; + } + + public static String createDefaultBundleSymbolicName(URL source) { + return getBaseName(source.getPath()); + } + + static String getBaseName(String path) { + String base = path; + base = unifySeparators(base); + if (base.endsWith("/")) { + base = base.substring(0, base.length() - 1); + } + + base = stripQuery(base); + base = stripSchemeAndDrive(base); + base = stripLeadingPathElements(base); + base = stripExtension(base); + return base; + } + + private static String unifySeparators(String base) { + return base.replaceAll("\\\\", "/"); + } + + private static String stripExtension(String base) { + int index; + index = base.lastIndexOf("."); + if (index > -1) { + base = base.substring(0, index); + } + return base; + } + + private static String stripLeadingPathElements(String base) { + int index = base.lastIndexOf("/"); + if (index > -1) { + base = base.substring(index + 1); + } + return base; + } + + private static String stripQuery(String path) { + String result = path; + int index = result.lastIndexOf("?"); + if (index > -1) { + result = result.substring(0, index); + } + return result; + } + + private static String stripSchemeAndDrive(String path) { + String result = path; + int index = result.indexOf(":"); + while (index > -1 && index < result.length()) { + result = result.substring(index + 1); + index = result.indexOf(":"); + } + return result; + } + + public static void setServletContextBundleProperties(Properties properties, Bundle bundle) { + setServletContextOsgiWebSymbolicNameProperty(properties, bundle); + setServletContextOsgiWebVersionProperty(properties, bundle); + } + + private static void setServletContextOsgiWebVersionProperty(Properties properties, Bundle bundle) { + if (bundle.getHeaders().get(BUNDLE_VERSION_HEADER) != null) { + properties.setProperty(OSGI_WEB_VERSION, bundle.getVersion().toString()); + } + } + + private static void setServletContextOsgiWebSymbolicNameProperty(Properties properties, Bundle bundle) { + String symbolicName = bundle.getSymbolicName(); + if (symbolicName != null) { + properties.setProperty(OSGI_WEB_SYMBOLICNAME, symbolicName); + } + } + + /** + * Determines whether the given manifest represents a web application bundle. According to the R4.2 Enterprise + * Specification, this is true if and only if the manifest contains any of the headers in Table 128.3: + * Bundle-SymbolicName, Bundle-Version, Bundle-ManifestVersion, Import-Package, Web-ContextPath. Note: there is no + * need to validate the manifest as if it is invalid it will cause an error later. + * + * @param manifest the bundle manifest + * @return <code>true</code> if and only if the given manifest represents a web application bundle + */ + public static boolean isWebApplicationBundle(BundleManifest manifest) { + return specifiesBundleSymbolicName(manifest) || specifiesBundleVersion(manifest) || specifiesBundleManifestVersion(manifest) + || specifiesImportPackage(manifest) || specifiesWebContextPath(manifest); + } + + private static boolean specifiesBundleSymbolicName(BundleManifest manifest) { + return manifest.getBundleSymbolicName().getSymbolicName() != null; + } + + private static boolean specifiesBundleVersion(BundleManifest manifest) { + return manifest.getHeader(Constants.BUNDLE_VERSION) != null; + } + + private static boolean specifiesBundleManifestVersion(BundleManifest manifest) { + return manifest.getBundleManifestVersion() != 1; + } + + private static boolean specifiesImportPackage(BundleManifest manifest) { + return !manifest.getImportPackage().getImportedPackages().isEmpty(); + } + + private static boolean specifiesWebContextPath(BundleManifest manifest) { + return manifest.getHeader(WebContainerUtils.HEADER_WEB_CONTEXT_PATH) != null; + } +} diff --git a/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/internal/template/ServiceCallback.java b/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/internal/template/ServiceCallback.java new file mode 100644 index 0000000..97ff729 --- /dev/null +++ b/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/internal/template/ServiceCallback.java @@ -0,0 +1,21 @@ +/******************************************************************************* + * Copyright (c) 2009, 2010 VMware Inc. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at + * http://www.opensource.org/licenses/apache2.0.php. + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * VMware Inc. - initial contribution + *******************************************************************************/ + +package org.eclipse.gemini.web.internal.template; + +public interface ServiceCallback<S, T> { + T doWithService(S service); +} diff --git a/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/internal/template/ServiceTemplate.java b/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/internal/template/ServiceTemplate.java new file mode 100644 index 0000000..831ae29 --- /dev/null +++ b/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/internal/template/ServiceTemplate.java @@ -0,0 +1,70 @@ +/******************************************************************************* + * Copyright (c) 2009, 2010 VMware Inc. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at + * http://www.opensource.org/licenses/apache2.0.php. + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * VMware Inc. - initial contribution + *******************************************************************************/ + +package org.eclipse.gemini.web.internal.template; + +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; +import org.osgi.util.tracker.ServiceTracker; +import org.osgi.util.tracker.ServiceTrackerCustomizer; + + +public class ServiceTemplate<S> { + + private final ServiceTracker tracker; + + public ServiceTemplate(BundleContext context, Class<S> clazz) { + this.tracker = new ServiceTracker(context, clazz.getName(), new ServiceTemplateCustomizer(context)); + } + + public void start() { + this.tracker.open(); + } + + public void stop() { + this.tracker.close(); + } + + @SuppressWarnings("unchecked") + public <T> T executeWithService(ServiceCallback<S, T> callback) { + Object service = this.tracker.getService(); + if(service != null) { + return callback.doWithService((S)service); + } + return null; + } + + private static final class ServiceTemplateCustomizer implements ServiceTrackerCustomizer { + + private final BundleContext context; + + public ServiceTemplateCustomizer(BundleContext context) { + this.context = context; + } + + public Object addingService(ServiceReference reference) { + return this.context.getService(reference); + } + + public void modifiedService(ServiceReference reference, Object service) { + } + + public void removedService(ServiceReference reference, Object service) { + this.context.ungetService(reference); + } + + } +} diff --git a/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/internal/template/ServiceUnavailableException.java b/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/internal/template/ServiceUnavailableException.java new file mode 100644 index 0000000..394c7b9 --- /dev/null +++ b/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/internal/template/ServiceUnavailableException.java @@ -0,0 +1,42 @@ +/******************************************************************************* + * Copyright (c) 2009, 2010 VMware Inc. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at + * http://www.opensource.org/licenses/apache2.0.php. + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * VMware Inc. - initial contribution + *******************************************************************************/ + +package org.eclipse.gemini.web.internal.template; + +public class ServiceUnavailableException extends RuntimeException { + + /** + * + */ + private static final long serialVersionUID = -4800943191504596869L; + + public ServiceUnavailableException() { + super(); + } + + public ServiceUnavailableException(String message, Throwable cause) { + super(message, cause); + } + + public ServiceUnavailableException(String message) { + super(message); + } + + public ServiceUnavailableException(Throwable cause) { + super(cause); + } + +} diff --git a/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/internal/url/ChainingWebBundleManifestTransformer.java b/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/internal/url/ChainingWebBundleManifestTransformer.java new file mode 100644 index 0000000..6d0bc9b --- /dev/null +++ b/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/internal/url/ChainingWebBundleManifestTransformer.java @@ -0,0 +1,44 @@ +/******************************************************************************* + * Copyright (c) 2009, 2010 VMware Inc. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at + * http://www.opensource.org/licenses/apache2.0.php. + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * VMware Inc. - initial contribution + *******************************************************************************/ + +package org.eclipse.gemini.web.internal.url; + +import java.io.IOException; +import java.net.URL; +import java.util.Collections; + +import org.eclipse.gemini.web.core.InstallationOptions; +import org.eclipse.gemini.web.core.WebBundleManifestTransformer; +import org.eclipse.virgo.util.osgi.manifest.BundleManifest; + + +public class ChainingWebBundleManifestTransformer implements WebBundleManifestTransformer { + + private final WebBundleManifestTransformer[] manifestTransformers; + + public ChainingWebBundleManifestTransformer(WebBundleManifestTransformer... transformers) { + this.manifestTransformers = transformers; + } + + public void transform(BundleManifest manifest, URL sourceURL, InstallationOptions options, boolean webBundle) throws IOException { + if (options == null) { + options = new InstallationOptions(Collections.<String, String> emptyMap()); + } + for (WebBundleManifestTransformer manifestTransformer : this.manifestTransformers) { + manifestTransformer.transform(manifest, sourceURL, options, webBundle); + } + } +} diff --git a/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/internal/url/DefaultsWebBundleManifestTransformer.java b/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/internal/url/DefaultsWebBundleManifestTransformer.java new file mode 100644 index 0000000..edf06ac --- /dev/null +++ b/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/internal/url/DefaultsWebBundleManifestTransformer.java @@ -0,0 +1,108 @@ +/******************************************************************************* + * Copyright (c) 2009, 2010 VMware Inc. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at + * http://www.opensource.org/licenses/apache2.0.php. + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * VMware Inc. - initial contribution + *******************************************************************************/ + +package org.eclipse.gemini.web.internal.url; + +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; + +import org.osgi.framework.Constants; +import org.osgi.framework.Version; + + +import org.eclipse.gemini.web.core.InstallationOptions; +import org.eclipse.gemini.web.core.WebBundleManifestTransformer; +import org.eclipse.gemini.web.internal.WebContainerUtils; +import org.eclipse.virgo.util.osgi.manifest.BundleManifest; +import org.eclipse.virgo.util.osgi.manifest.BundleSymbolicName; +import org.eclipse.virgo.util.osgi.manifest.ImportedPackage; + +public final class DefaultsWebBundleManifestTransformer implements WebBundleManifestTransformer { + + private static final int MINIMUM_BUNDLE_MANIFEST_VERSION = 2; + + private static final String WEB_INF_CLASSES = "WEB-INF/classes"; + + public void transform(BundleManifest manifest, URL sourceURL, InstallationOptions options, boolean webBundle) throws IOException { + if (!webBundle || options.getDefaultWABHeaders()) { + applyDefaultBundleSymbolicName(sourceURL, manifest); + applyDefaultBundleManifestVersion(manifest); + applyBundleClassPath(sourceURL, manifest); + applyImportPackage(manifest); + } + } + + private void applyImportPackage(BundleManifest manifest) { + addImportInNecessary("javax.servlet", new Version("2.5"), manifest); + addImportInNecessary("javax.servlet.http", new Version("2.5"), manifest); + addImportInNecessary("javax.servlet.jsp", new Version("2.1"), manifest); + addImportInNecessary("javax.servlet.jsp.el", new Version("2.1"), manifest); + addImportInNecessary("javax.servlet.jsp.tagext", new Version("2.1"), manifest); + addImportInNecessary("javax.el", new Version("1.0"), manifest); + } + + private void addImportInNecessary(String packageName, Version version, BundleManifest manifest) { + List<ImportedPackage> pkgs = manifest.getImportPackage().getImportedPackages(); + for (ImportedPackage pkg : pkgs) { + if (pkg.getPackageName().equals(packageName)) { + return; + } + } + ImportedPackage packageImport = manifest.getImportPackage().addImportedPackage(packageName); + packageImport.getAttributes().put(Constants.VERSION_ATTRIBUTE, version.toString()); + } + + private void applyBundleClassPath(URL source, BundleManifest manifest) throws IOException { + List<String> bundleClassPath = manifest.getBundleClasspath(); + + if (!bundleClassPath.contains(WEB_INF_CLASSES)) { + bundleClassPath.add(0, WEB_INF_CLASSES); + } + + final List<String> entries = new ArrayList<String>(); + WebBundleScanner scanner = new WebBundleScanner(source, new WebBundleScannerCallback() { + + public void classFound(String entry) { + } + + public void jarFound(String entry) { + entries.add(entry); + } + }); + scanner.scanWar(); + + for (String entry : entries) { + if (!bundleClassPath.contains(entry)) { + bundleClassPath.add(entry); + } + } + } + + private void applyDefaultBundleManifestVersion(BundleManifest manifest) { + if (manifest.getBundleManifestVersion() < MINIMUM_BUNDLE_MANIFEST_VERSION) { + manifest.setBundleManifestVersion(MINIMUM_BUNDLE_MANIFEST_VERSION); + } + } + + private void applyDefaultBundleSymbolicName(URL source, BundleManifest manifest) { + BundleSymbolicName bsn = manifest.getBundleSymbolicName(); + if (bsn.getSymbolicName() == null) { + bsn.setSymbolicName(WebContainerUtils.createDefaultBundleSymbolicName(source)); + } + } +} diff --git a/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/internal/url/PackageMergeUtils.java b/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/internal/url/PackageMergeUtils.java new file mode 100644 index 0000000..6093150 --- /dev/null +++ b/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/internal/url/PackageMergeUtils.java @@ -0,0 +1,81 @@ +/******************************************************************************* + * Copyright (c) 2009, 2010 VMware Inc. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at + * http://www.opensource.org/licenses/apache2.0.php. + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * VMware Inc. - initial contribution + *******************************************************************************/ + +package org.eclipse.gemini.web.internal.url; + +import java.util.List; +import java.util.Map; + +import org.osgi.framework.Constants; +import org.osgi.framework.Version; + +import org.eclipse.virgo.util.osgi.manifest.BundleManifest; +import org.eclipse.virgo.util.osgi.manifest.ExportedPackage; +import org.eclipse.virgo.util.osgi.manifest.ImportedPackage; + +public class PackageMergeUtils { + + public static void mergeImportPackage(BundleManifest manifest, String name, Map<String, String> attributes, Map<String, String> directives) { + ImportedPackage packageImport = findImportedPackage(manifest, name); + + if (packageImport == null) { + packageImport = manifest.getImportPackage().addImportedPackage(name); + } + + packageImport.getAttributes().clear(); + packageImport.getAttributes().putAll(attributes); + + packageImport.getDirectives().clear(); + packageImport.getDirectives().putAll(directives); + } + + public static void mergeExportPackage(BundleManifest manifest, String name, Map<String, String> attributes, Map<String, String> directives) { + String versionAttribute = attributes.get(Constants.VERSION_ATTRIBUTE); + Version version = (versionAttribute == null ? Version.emptyVersion : new Version(versionAttribute)); + + ExportedPackage packageExport = findExportedPackage(manifest, name, version); + + if (packageExport == null) { + packageExport = manifest.getExportPackage().addExportedPackage(name); + } + + packageExport.getAttributes().clear(); + packageExport.getAttributes().putAll(attributes); + + packageExport.getDirectives().clear(); + packageExport.getDirectives().putAll(directives); + } + + private static final ExportedPackage findExportedPackage(BundleManifest manifest, String packageName, Version version) { + List<ExportedPackage> exportedPackages = manifest.getExportPackage().getExportedPackages(); + for (ExportedPackage exportedPackage : exportedPackages) { + if (packageName.equals(exportedPackage.getPackageName()) && version.equals(exportedPackage.getVersion())) { + return exportedPackage; + } + } + return null; + } + + static final ImportedPackage findImportedPackage(BundleManifest manifest, String packageName) { + List<ImportedPackage> importedPackages = manifest.getImportPackage().getImportedPackages(); + for (ImportedPackage importedPackage : importedPackages) { + if (packageName.equals(importedPackage.getPackageName())) { + return importedPackage; + } + } + return null; + } +} diff --git a/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/internal/url/PackagesInWarScanner.java b/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/internal/url/PackagesInWarScanner.java new file mode 100644 index 0000000..9bcd17a --- /dev/null +++ b/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/internal/url/PackagesInWarScanner.java @@ -0,0 +1,46 @@ +/******************************************************************************* + * Copyright (c) 2009, 2010 VMware Inc. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at + * http://www.opensource.org/licenses/apache2.0.php. + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * VMware Inc. - initial contribution + *******************************************************************************/ + +package org.eclipse.gemini.web.internal.url; + +import java.io.IOException; +import java.net.URL; +import java.util.HashSet; +import java.util.Set; + +class PackagesInWarScanner { + + Set<String> getPackagesContainedInWar(URL warURL) throws IOException { + final Set<String> packagesInWar = new HashSet<String>(); + if (warURL != null) { + WebBundleScanner scanner = new WebBundleScanner(warURL, new WebBundleScannerCallback() { + + public void classFound(String entry) { + int lastSlashIndex = entry.lastIndexOf('/'); + if (lastSlashIndex >= 0) { + packagesInWar.add(entry.substring(0, lastSlashIndex).replace('/', '.')); + } + } + + public void jarFound(String entry) { + } + }, true); + scanner.scanWar(); + } + + return packagesInWar; + } +} diff --git a/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/internal/url/Path.java b/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/internal/url/Path.java new file mode 100644 index 0000000..32f753c --- /dev/null +++ b/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/internal/url/Path.java @@ -0,0 +1,165 @@ +/* + * Copyright SpringSource Inc 2010 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.eclipse.gemini.web.internal.url; + +import java.util.Arrays; + +final class Path { + + private static final String BACK_SLASH = "\\"; + + private static final String PATH_SEPARATOR = "/"; + + private static final String DOUBLE_PATH_SEPARATOR = PATH_SEPARATOR + PATH_SEPARATOR; + + private static final String PATH_HERE = "."; + + private static final String PATH_UP = ".."; + + private final String[] baseComponents; + + Path(String basePath) { + validatePath(basePath); + String[] comps = basePath.split(PATH_SEPARATOR); + while (comps.length > 0 && PATH_HERE.equals(comps[0])) { + String[] c2 = new String[comps.length - 1]; + System.arraycopy(comps, 1, c2, 0, c2.length); + comps = c2; + } + this.baseComponents = comps; + } + + private Path(String[] comps) { + this.baseComponents = comps; + } + + private String head() { + if (!isEmpty()) { + return baseComponents[0]; + } else { + throw new IllegalStateException("head not applicable to an empty path"); + } + } + + private Path tail() { + if (!isEmpty()) { + String[] c = new String[this.baseComponents.length - 1]; + System.arraycopy(this.baseComponents, 1, c, 0, c.length); + return new Path(c); + } else { + throw new IllegalStateException("tail not applicable to an empty path"); + } + } + + private Path front() { + if (!isEmpty()) { + String[] c = new String[this.baseComponents.length - 1]; + System.arraycopy(this.baseComponents, 0, c, 0, c.length); + return new Path(c); + } else { + throw new IllegalStateException("front not applicable to an empty path"); + } + } + + private static void validatePath(String basePath) { + if (basePath == null) { + throw new IllegalArgumentException("path must not be null"); + } + if (basePath.contains(BACK_SLASH)) { + throw new IllegalArgumentException("path must not contain '" + BACK_SLASH + "'"); + } + if (basePath.endsWith(PATH_SEPARATOR)) { + throw new IllegalArgumentException("path must not end in '" + PATH_SEPARATOR + "'"); + } + if (basePath.contains(DOUBLE_PATH_SEPARATOR)) { + throw new IllegalArgumentException("path must not contain '" + DOUBLE_PATH_SEPARATOR + "'"); + } + } + + public Path() { + this.baseComponents = new String[0]; + } + + public Path applyRelativePath(Path relativePath) { + try { + Path b = this; + Path r = relativePath; + while (r.isUp()) { + r = r.tail(); + b = b.front(); + } + return b.append(r); + } catch (IllegalStateException s) { + throw new IllegalArgumentException("relative path cannot be applied", s); + } + } + + private Path append(Path r) { + if (isEmpty()) { + return r; + } else if (r.isEmpty()) { + return this; + } + return new Path(toString() + PATH_SEPARATOR + r.toString()); + } + + private boolean isUp() { + return !isEmpty() && PATH_UP.equals(head()); + } + + private boolean isEmpty() { + return this.baseComponents.length == 0; + } + + @Override + public String toString() { + StringBuffer s = new StringBuffer(); + boolean first = true; + for (String c : baseComponents) { + if (!first) { + s.append(PATH_SEPARATOR); + } + first = false; + s.append(c); + } + + return s.toString(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + Arrays.hashCode(baseComponents); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Path other = (Path) obj; + if (!Arrays.equals(baseComponents, other.baseComponents)) + return false; + return true; + } + +} diff --git a/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/internal/url/SpecificationWebBundleManifestTransformer.java b/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/internal/url/SpecificationWebBundleManifestTransformer.java new file mode 100644 index 0000000..e0729fc --- /dev/null +++ b/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/internal/url/SpecificationWebBundleManifestTransformer.java @@ -0,0 +1,218 @@ +/******************************************************************************* + * Copyright (c) 2009, 2010 VMware Inc. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at + * http://www.opensource.org/licenses/apache2.0.php. + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * VMware Inc. - initial contribution + *******************************************************************************/ + +package org.eclipse.gemini.web.internal.url; + +import java.io.IOException; +import java.net.URL; +import java.util.Collections; +import java.util.List; + +import org.osgi.framework.Constants; +import org.osgi.framework.Version; + + +import org.eclipse.gemini.web.core.InstallationOptions; +import org.eclipse.gemini.web.core.WebBundleManifestTransformer; +import org.eclipse.gemini.web.internal.WebContainerUtils; +import org.eclipse.virgo.util.osgi.manifest.BundleManifest; +import org.eclipse.virgo.util.osgi.manifest.parse.HeaderDeclaration; +import org.eclipse.virgo.util.osgi.manifest.parse.HeaderParser; +import org.eclipse.virgo.util.osgi.manifest.parse.HeaderParserFactory; +import org.eclipse.virgo.util.osgi.manifest.parse.ParserLogger; + +/** + * Applies user installation options onto a {@link BundleManifest}. + * + * + */ +public final class SpecificationWebBundleManifestTransformer implements WebBundleManifestTransformer { + + private static final int MINIMUM_VALID_BUNDLE_MANIFEST_VERSION = 2; + + public void transform(BundleManifest manifest, URL sourceURL, InstallationOptions options, boolean webBundle) throws IOException { + if (options == null) { + options = new InstallationOptions(Collections.<String, String> emptyMap()); + } + + transformBundleSymbolicName(manifest, options, webBundle); + transformBundleVersion(manifest, options, webBundle); + transformBundleManifestVersion(manifest, options, webBundle); + transformBundleClassPath(manifest, options, webBundle); + transformImportPackage(manifest, options, webBundle); + transformExportPackage(manifest, options, webBundle); + transformWebContextPath(manifest, options, webBundle); + } + + private void transformExportPackage(BundleManifest manifest, InstallationOptions options, boolean isWebApplicationBundle) { + String epd = options.getExportPackageDeclaration(); + if (epd != null) { + if (isWebApplicationBundle) { + throw new IllegalArgumentException("Export-Package URL parameter cannot modify a Web Application Bundle"); + } + HeaderParser parser = HeaderParserFactory.newHeaderParser(new TransformerParserLogger()); + List<HeaderDeclaration> packageHeader = parser.parsePackageHeader(epd, Constants.EXPORT_PACKAGE); + for (HeaderDeclaration headerDeclaration : packageHeader) { + for (String name : headerDeclaration.getNames()) { + PackageMergeUtils.mergeExportPackage(manifest, name, headerDeclaration.getAttributes(), headerDeclaration.getDirectives()); + } + } + } + } + + private void transformImportPackage(BundleManifest manifest, InstallationOptions options, boolean isWebApplicationBundle) { + String ipd = options.getImportPackageDeclaration(); + if (ipd != null) { + if (isWebApplicationBundle) { + throw new IllegalArgumentException("Import-Package URL parameter cannot modify a Web Application Bundle"); + } + HeaderParser parser = HeaderParserFactory.newHeaderParser(new TransformerParserLogger()); + List<HeaderDeclaration> packageHeader = parser.parsePackageHeader(ipd, Constants.IMPORT_PACKAGE); + for (HeaderDeclaration headerDeclaration : packageHeader) { + for (String name : headerDeclaration.getNames()) { + PackageMergeUtils.mergeImportPackage(manifest, name, headerDeclaration.getAttributes(), headerDeclaration.getDirectives()); + } + } + } + } + + private void transformBundleSymbolicName(BundleManifest manifest, InstallationOptions options, boolean isWebApplicationBundle) { + if (options.getBundleSymbolicName() != null) { + if (isWebApplicationBundle) { + throw new IllegalArgumentException("Bundle-SymbolicName URL parameter cannot modify a Web Application Bundle"); + } + manifest.getBundleSymbolicName().setSymbolicName(options.getBundleSymbolicName()); + } + } + + private void transformBundleManifestVersion(BundleManifest manifest, InstallationOptions options, boolean isWebApplicationBundle) { + if (options.getBundleManifestVersion() != null) { + if (isWebApplicationBundle) { + throw new IllegalArgumentException("Bundle-ManifestVersion URL parameter cannot modify a Web Application Bundle"); + } + manifest.setBundleManifestVersion(parseBundleManifestVersion(options.getBundleManifestVersion())); + } + } + + private int parseBundleManifestVersion(String bundleManifestVersion) { + int result = MINIMUM_VALID_BUNDLE_MANIFEST_VERSION; + if (bundleManifestVersion != null) { + try { + result = Integer.parseInt(bundleManifestVersion); + if (result < MINIMUM_VALID_BUNDLE_MANIFEST_VERSION) { + throw new IllegalArgumentException(Constants.BUNDLE_MANIFESTVERSION + " " + result + " is less than the smallest valid value of " + + MINIMUM_VALID_BUNDLE_MANIFEST_VERSION); + } + } catch (NumberFormatException ex) { + throw new IllegalArgumentException(Constants.BUNDLE_MANIFESTVERSION + " is not a valid integer.", ex); + } + } + return result; + } + + private void transformBundleVersion(BundleManifest manifest, InstallationOptions options, boolean isWebApplicationBundle) { + if (options.getBundleVersion() != null) { + if (isWebApplicationBundle) { + throw new IllegalArgumentException("Bundle-Version URL parameter cannot modify a Web Application Bundle"); + } + manifest.setBundleVersion(new Version(options.getBundleVersion())); + } + } + + private void transformBundleClassPath(BundleManifest manifest, InstallationOptions options, boolean isWebApplicationBundle) { + List<String> bundleClassPath = manifest.getBundleClasspath(); + String bundleClassPathOption = options.getBundleClassPath(); + if (bundleClassPathOption != null) { + if (isWebApplicationBundle) { + throw new IllegalArgumentException("Bundle-ClassPath URL parameter cannot modify a Web Application Bundle"); + } + for (String entry : parseBundleClassPath(bundleClassPathOption)) { + if (!bundleClassPath.contains(entry)) { + bundleClassPath.add(entry); + } + } + } + } + + private static String[] parseBundleClassPath(String bundleClassPath) { + String[] bundleClassPathEntries = bundleClassPath.split(","); + minimallyValidateBundleClassPathEntries(bundleClassPathEntries, bundleClassPath); + return bundleClassPathEntries; + } + + /** + * Validates the given bundle class path entries. + * <p> + * Trailing slashes are tolerated and removed so the resultant class path entries are more likely to conform + * strictly to the OSGi specification. + */ + private static void minimallyValidateBundleClassPathEntries(String[] bundleClassPathEntries, String bundleClassPath) { + for (int i = 0; i < bundleClassPathEntries.length; i++) { + String entry = bundleClassPathEntries[i]; + if (entry.length() == 0) { + diagnoseInvalidEntry(entry, bundleClassPath); + } + if (entry.endsWith("/")) { + if (entry.length() == 1) { + diagnoseInvalidEntry(entry, bundleClassPath); + } + bundleClassPathEntries[i] = entry.substring(0, entry.length() - 1); + } + } + } + + private static void diagnoseInvalidEntry(String entry, String bundleClassPath) { + throw new IllegalArgumentException(Constants.BUNDLE_CLASSPATH + "'" + bundleClassPath + "' contains an invalid entry '" + entry + "'"); + } + + /** + * @param isWebApplicationBundle + */ + private void transformWebContextPath(BundleManifest manifest, InstallationOptions options, boolean isWebApplicationBundle) { + String webContextPathOption = options.getWebContextPath(); + if (webContextPathOption != null) { + manifest.setHeader(WebContainerUtils.HEADER_WEB_CONTEXT_PATH, validateWebContextPath(webContextPathOption)); + } else if (!options.getDefaultWABHeaders()) { + String webContextPathHeader = manifest.getHeader(WebContainerUtils.HEADER_WEB_CONTEXT_PATH); + if (webContextPathHeader == null || webContextPathHeader.trim().length() == 0) { + throw new IllegalArgumentException(WebContainerUtils.HEADER_WEB_CONTEXT_PATH + " is missing"); + } + } + } + + private String validateWebContextPath(String webContextPathOption) { + String trimmedWebContextPathOption = webContextPathOption.trim(); + if (trimmedWebContextPathOption.length() == 0) { + throw new IllegalArgumentException(WebContainerUtils.HEADER_WEB_CONTEXT_PATH + " URL parameter value is missing"); + } + if (trimmedWebContextPathOption.startsWith("/")) { + return trimmedWebContextPathOption; + } else { + return "/" + trimmedWebContextPathOption; + } + } + + private static class TransformerParserLogger implements ParserLogger { + + public String[] errorReports() { + return null; + } + + public void outputErrorMsg(Exception re, String item) { + + } + } +} diff --git a/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/internal/url/SystemBundleExportsImportingWebBundleManifestTransformer.java b/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/internal/url/SystemBundleExportsImportingWebBundleManifestTransformer.java new file mode 100644 index 0000000..6a56bfb --- /dev/null +++ b/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/internal/url/SystemBundleExportsImportingWebBundleManifestTransformer.java @@ -0,0 +1,55 @@ +/******************************************************************************* + * Copyright (c) 2009, 2010 VMware Inc. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at + * http://www.opensource.org/licenses/apache2.0.php. + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * VMware Inc. - initial contribution + *******************************************************************************/ + +package org.eclipse.gemini.web.internal.url; + +import java.io.IOException; +import java.net.URL; +import java.util.Map; +import java.util.Set; +import java.util.Map.Entry; + +import org.eclipse.gemini.web.core.InstallationOptions; +import org.eclipse.gemini.web.core.WebBundleManifestTransformer; +import org.eclipse.virgo.util.osgi.VersionRange; +import org.eclipse.virgo.util.osgi.manifest.BundleManifest; + + +public final class SystemBundleExportsImportingWebBundleManifestTransformer implements WebBundleManifestTransformer { + + private final Map<String, VersionRange> systemBundleExports; + + private final PackagesInWarScanner warPackagesScanner = new PackagesInWarScanner(); + + public SystemBundleExportsImportingWebBundleManifestTransformer(Map<String, VersionRange> systemBundleExports) { + this.systemBundleExports = systemBundleExports; + } + + public void transform(BundleManifest manifest, URL sourceURL, InstallationOptions options, boolean webBundle) throws IOException { + if (!webBundle || options.getDefaultWABHeaders()) { + addImportsForSystemBundleExports(manifest, this.warPackagesScanner.getPackagesContainedInWar(sourceURL)); + } + } + + protected void addImportsForSystemBundleExports(BundleManifest bundleManifest, Set<String> packagesInWar) { + for (Entry<String, VersionRange> exportedPackage : this.systemBundleExports.entrySet()) { + String packageName = exportedPackage.getKey(); + if (!packagesInWar.contains(packageName) && PackageMergeUtils.findImportedPackage(bundleManifest, packageName) == null) { + bundleManifest.getImportPackage().addImportedPackage(packageName).setVersion(exportedPackage.getValue()); + } + } + } +} diff --git a/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/internal/url/WebBundleScanner.java b/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/internal/url/WebBundleScanner.java new file mode 100644 index 0000000..bbdb962 --- /dev/null +++ b/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/internal/url/WebBundleScanner.java @@ -0,0 +1,298 @@ +/******************************************************************************* + * Copyright (c) 2009, 2010 VMware Inc. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at + * http://www.opensource.org/licenses/apache2.0.php. + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * VMware Inc. - initial contribution + *******************************************************************************/ + +package org.eclipse.gemini.web.internal.url; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.HashSet; +import java.util.Set; +import java.util.jar.Attributes; +import java.util.jar.JarEntry; +import java.util.jar.JarInputStream; +import java.util.jar.Manifest; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.eclipse.virgo.util.io.IOUtils; + +final class WebBundleScanner { + + private static final Logger LOGGER = LoggerFactory.getLogger(WebBundleScanner.class); + + private static final String LIB_DIR_SUFFIX = File.separator + "WEB-INF" + File.separator + "lib"; + + private static final String CLASSES_DIR_SUFFIX = File.separator + "WEB-INF" + File.separator + "classes"; + + private static final String FILE_SCHEME = "file"; + + private static final String JAR_SUFFIX = ".jar"; + + private static final String CLASS_SUFFIX = ".class"; + + private static final String LIB_ENTRY_PREFIX = "WEB-INF/lib/"; + + private static final String CLASSES_ENTRY_PREFIX = "WEB-INF/classes/"; + + private static final String CLASS_PATH_ATTRIBUTE_NAME = "Class-Path"; + + private static final String CLASS_PATH_SEPARATOR = " "; + + private final Object monitor = new Object(); + + private final URL source; + + private final WebBundleScannerCallback callBack; + + private final boolean findClassesInNestedJars; + + private final Set<String> scannedJars = new HashSet<String>(); + + WebBundleScanner(URL source, WebBundleScannerCallback callBack) { + this(source, callBack, false); + } + + /** + * Creates a WebBundleScanner for a given WAR with a given callBack. + * + * @param source the WAR content + * @param callBack The callBack to notify of entries in the WAR + * @param findClassesInNestedJars + */ + WebBundleScanner(URL source, WebBundleScannerCallback callBack, boolean findClassesInNestedJars) { + this.source = source; + this.callBack = callBack; + this.findClassesInNestedJars = findClassesInNestedJars; + } + + /** + * Scans the WAR content from <code>source</code>, notifying the callBack of .class entries in WEB-INF/classes and + * its sub-directories, and .jar entries in WEB-INF/lib. + * + * @throws IOException if the WAR cannot be scanned + */ + void scanWar() throws IOException { + synchronized (this.monitor) { + this.scannedJars.clear(); + if (isDirectory()) { + scanWarDirectory(); + } else { + scanWarFile(); + } + } + } + + private void scanWarDirectory() throws IOException { + try { + File bundleDir = sourceAsFile(); + File libDir = new File(bundleDir, LIB_DIR_SUFFIX); + if (libDir.isDirectory()) { + doScanLibDirectory(libDir); + } + File classesDir = new File(bundleDir, CLASSES_DIR_SUFFIX); + if (classesDir.isDirectory()) { + doScanClassesDirectory(classesDir); + } + } catch (URISyntaxException e) { + throw new IllegalStateException("Unexpected URISyntaxException.", e); + } + } + + private void doScanLibDirectory(File libDir) throws IOException { + File[] files = libDir.listFiles(); + if (files != null) { + for (File file : files) { + if (file.isFile() && file.getName().endsWith(JAR_SUFFIX)) { + String pathToJar = LIB_ENTRY_PREFIX + file.getName(); + if (driveCallBackIfNewJarFound(pathToJar)) { + doScanNestedJar(file); + } + } + } + } + } + + private boolean driveCallBackIfNewJarFound(String pathToJar) { + // Prevent infinite recursion. + if (this.scannedJars.contains(pathToJar)) { + return false; + } + this.scannedJars.add(pathToJar); + this.callBack.jarFound(pathToJar); + return true; + } + + private void doScanNestedJar(File file) throws IOException { + JarInputStream jis = null; + + try { + jis = new JarInputStream(new FileInputStream(file)); + doScanNestedJar(file.getAbsolutePath(), jis); + } finally { + if (jis != null) { + IOUtils.closeQuietly(jis); + } + } + } + + private void doScanClassesDirectory(File classesDir) { + File[] files = classesDir.listFiles(); + if (files != null) { + for (File file : files) { + if (file.isDirectory()) { + doScanClassesDirectory(file); + } else if (file.isFile() && file.getName().endsWith(CLASS_SUFFIX)) { + String path = normalizePath(file.getPath()); + this.callBack.classFound(path.substring(path.lastIndexOf(CLASSES_ENTRY_PREFIX) + CLASSES_ENTRY_PREFIX.length())); + } + } + } + } + + private void scanWarFile() throws IOException { + JarInputStream jis = new JarInputStream(this.source.openStream()); + try { + JarEntry entry; + while ((entry = jis.getNextJarEntry()) != null) { + String entryName = entry.getName(); + if (entryName.startsWith(LIB_ENTRY_PREFIX) && entryName.endsWith(JAR_SUFFIX)) { + if (driveCallBackIfNewJarFound(entryName)) { + JarInputStream nestedJis = new JarInputStream(jis); + doScanNestedJar(entryName, nestedJis); + } + } else if (entryName.startsWith(CLASSES_ENTRY_PREFIX) && entryName.endsWith(CLASS_SUFFIX)) { + this.callBack.classFound(entry.getName().substring(CLASSES_ENTRY_PREFIX.length())); + } + } + } finally { + IOUtils.closeQuietly(jis); + } + } + + private void doScanNestedJar(String jarEntryName, JarInputStream jis) throws IOException { + Manifest manifest = jis.getManifest(); + if (manifest != null) { + Attributes mainAttributes = manifest.getMainAttributes(); + if (mainAttributes != null) { + String classPath = mainAttributes.getValue(CLASS_PATH_ATTRIBUTE_NAME); + if (classPath != null) { + Path jarPathx = getNormalisedDirectoryPath(jarEntryName); + + String[] classPathItems = classPath.split(CLASS_PATH_SEPARATOR); + for (String classPathItem : classPathItems) { + try { + Path entryPath = jarPathx.applyRelativePath(new Path(classPathItem)); + scanNestedJarInWar(entryPath.toString()); + } catch (IllegalArgumentException _) { + // skip invalid relative paths which try to escape the WAR + } + } + } + } + } + + JarEntry entry; + while ((entry = jis.getNextJarEntry()) != null) { + String entryName = entry.getName(); + if (entryName.endsWith(CLASS_SUFFIX)) { + notifyClassFound(entryName); + } + } + } + + private static Path getNormalisedDirectoryPath(String jarEntryName) { + String jarPath = normalizePath(jarEntryName); + int lastDirectoryIndex = jarPath.lastIndexOf("/"); + return lastDirectoryIndex == -1 ? new Path() : new Path(jarPath.substring(0, lastDirectoryIndex)); + } + + private void scanNestedJarInWar(String jarPath) throws IOException { + if (isDirectory()) { + scanNestedJarInWarDirectory(jarPath); + } else { + scanNestedJarInWarFile(jarPath); + } + } + + private void scanNestedJarInWarDirectory(String jarPath) throws IOException { + try { + File bundleDir = sourceAsFile(); + File nestedJar = new File(bundleDir, "/" + jarPath); + if (nestedJar.isFile()) { + String pathToJar = "/" + nestedJar.getName(); + if (driveCallBackIfNewJarFound(pathToJar)) { + doScanNestedJar(nestedJar); + } + } + } catch (URISyntaxException e) { + throw new IllegalStateException("Unexpected URISyntaxException.", e); + } + } + + private void scanNestedJarInWarFile(String jarPath) throws IOException { + JarInputStream jis = new JarInputStream(this.source.openStream()); + try { + JarEntry entry; + while ((entry = jis.getNextJarEntry()) != null) { + String entryName = entry.getName(); + if (jarPath.endsWith(entryName)) { + if (driveCallBackIfNewJarFound(entryName)) { + JarInputStream nestedJis = new JarInputStream(jis); + doScanNestedJar(entryName, nestedJis); + } + } + } + } finally { + IOUtils.closeQuietly(jis); + } + + } + + private void notifyClassFound(String entryName) { + if (this.findClassesInNestedJars) { + this.callBack.classFound(entryName); + } + } + + private boolean isDirectory() { + if (FILE_SCHEME.equals(this.source.getProtocol())) { + try { + return sourceAsFile().isDirectory(); + } catch (URISyntaxException e) { + LOGGER.warn("Unable to determine if bundle '" + this.source + "'is a directory.", e); + } + } + return false; + } + + private File sourceAsFile() throws URISyntaxException { + URI uri = this.source.toURI(); + if (uri.isOpaque()) { + return new File(uri.getSchemeSpecificPart()); + } else { + return new File(uri); + } + } + + private static String normalizePath(String path) { + return path.replace('\\', '/'); + } +} diff --git a/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/internal/url/WebBundleScannerCallback.java b/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/internal/url/WebBundleScannerCallback.java new file mode 100644 index 0000000..d0eb225 --- /dev/null +++ b/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/internal/url/WebBundleScannerCallback.java @@ -0,0 +1,24 @@ +/******************************************************************************* + * Copyright (c) 2009, 2010 VMware Inc. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at + * http://www.opensource.org/licenses/apache2.0.php. + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * VMware Inc. - initial contribution + *******************************************************************************/ + +package org.eclipse.gemini.web.internal.url; + +interface WebBundleScannerCallback { + + void jarFound(String pathToJar); + + void classFound(String className); +} diff --git a/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/internal/url/WebBundleUrl.java b/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/internal/url/WebBundleUrl.java new file mode 100644 index 0000000..3145c61 --- /dev/null +++ b/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/internal/url/WebBundleUrl.java @@ -0,0 +1,130 @@ +/******************************************************************************* + * Copyright (c) 2009, 2010 VMware Inc. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at + * http://www.opensource.org/licenses/apache2.0.php. + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * VMware Inc. - initial contribution + *******************************************************************************/ + +package org.eclipse.gemini.web.internal.url; + +import static java.util.Collections.unmodifiableMap; + +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLStreamHandler; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.gemini.web.internal.WebContainerUtils; + + +/** + * Encapsulates the state of a <code>war:</code> URL. + */ +public class WebBundleUrl { + + public static final String SCHEME = WebContainerUtils.WEB_BUNDLE_SCHEME; + + public final Object monitor = new Object(); + + private final URL url; + + private final String location; + + private Map<String, String> options; + + public WebBundleUrl(String location, Map<String, String> options) throws MalformedURLException { + this.url = createURL(location, options); + this.location = location; + this.options = (options == null ? Collections.<String, String> emptyMap() : unmodifiableMap(new HashMap<String, String>(options))); + } + + public WebBundleUrl(URL url) { + String protocol = url.getProtocol(); + if (!SCHEME.equals(protocol)) { + throw new IllegalArgumentException("URL '" + url + "' is not a valid WAR URL"); + } + this.url = url; + this.location = url.getPath(); + } + + public final String getLocation() { + return this.location; + } + + /** + * Gets the query options from the URL. Note that validation is deferred until this point as doing it earlier does + * not produce the necessary BundleException when driven under {@link org.osgi.framework.BundleContext#installBundle(String) installBundle(String)}. + * @return the options in a String->String map + */ + public final Map<String, String> getOptions() { + synchronized (this.monitor) { + if (this.options == null) { + this.options = parseQueryString(url.getQuery()); + } + } + return this.options; + } + + private String toPathString(String location, Map<?, ?> options) { + StringBuilder sb = new StringBuilder(); + sb.append(location); + appendQueryStringIfNecessary(options, sb); + return sb.toString(); + } + + public final String toString() { + return this.url.toString(); + } + + public final URL toURL() { + return this.url; + } + + /** + * Hook method to use in unit testing. Allows for a specific {@link URLStreamHandler} to be added to {@link URL} + * instances created internally. + */ + protected URLStreamHandler createURLStreamHandler() { + return null; + } + + private URL createURL(String location, Map<?, ?> options) throws MalformedURLException { + return new URL(SCHEME, null, -1, toPathString(location, options), createURLStreamHandler()); + } + + private static Map<String, String> parseQueryString(String query) { + Map<String, String> options = new HashMap<String, String>(); + if (query != null) { + String[] parms = query.split("&"); + for (String parm : parms) { + int equals = parm.indexOf("="); + if (equals == -1) { + throw new IllegalArgumentException("Missing '=' in URL parameter '" + parm + "'"); + } + options.put(parm.substring(0, equals), parm.substring(equals + 1)); + } + } + return unmodifiableMap(options); + } + + private static void appendQueryStringIfNecessary(Map<?, ?> options, StringBuilder sb) { + if (options != null && !options.isEmpty()) { + sb.append("?"); + for (Map.Entry<?, ?> entry : options.entrySet()) { + sb.append(entry.getKey()).append("=").append(entry.getValue()).append("&"); + } + sb.deleteCharAt(sb.length() - 1); + } + } +} diff --git a/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/internal/url/WebBundleUrlStreamHandlerService.java b/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/internal/url/WebBundleUrlStreamHandlerService.java new file mode 100644 index 0000000..4743c7e --- /dev/null +++ b/org.eclipse.gemini.web.core/src/main/java/org/eclipse/gemini/web/internal/url/WebBundleUrlStreamHandlerService.java @@ -0,0 +1,134 @@ +/******************************************************************************* + * Copyright (c) 2009, 2010 VMware Inc. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at + * http://www.opensource.org/licenses/apache2.0.php. + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * VMware Inc. - initial contribution + *******************************************************************************/ + +package org.eclipse.gemini.web.internal.url; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URL; +import java.net.URLConnection; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.jar.Attributes; +import java.util.jar.JarFile; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; +import java.util.zip.ZipEntry; + +import org.osgi.service.url.AbstractURLStreamHandlerService; +import org.osgi.service.url.URLStreamHandlerService; + + +import org.eclipse.gemini.web.core.InstallationOptions; +import org.eclipse.gemini.web.core.WebBundleManifestTransformer; +import org.eclipse.gemini.web.internal.WebContainerUtils; +import org.eclipse.virgo.util.io.JarTransformer; +import org.eclipse.virgo.util.io.JarTransformingURLConnection; +import org.eclipse.virgo.util.io.JarTransformer.JarTransformerCallback; +import org.eclipse.virgo.util.osgi.manifest.BundleManifest; +import org.eclipse.virgo.util.osgi.manifest.BundleManifestFactory; + +/** + * {@link URLStreamHandlerService} that transforms bundles installed with the <code>war:</code> protocol. + * <p/> + * Transformations are applied using the {@link WebBundleManifestTransformer}. + * + * @see WebBundleManifestTransformer + */ +public final class WebBundleUrlStreamHandlerService extends AbstractURLStreamHandlerService { + + private final WebBundleManifestTransformer transformer; + + public WebBundleUrlStreamHandlerService(WebBundleManifestTransformer transformer) { + this.transformer = transformer; + } + + @Override + public URLConnection openConnection(URL u) throws IOException { + WebBundleUrl url = new WebBundleUrl(u); + URL actualUrl = new URL(url.getLocation()); + + JarTransformer jarTransformer = new JarTransformer(new Callback(actualUrl, url, this.transformer)); + return new JarTransformingURLConnection(actualUrl, jarTransformer, true); + } + + private static final class Callback implements JarTransformerCallback { + + private final WebBundleManifestTransformer transformer; + + private final URL sourceURL; + + private final WebBundleUrl webBundleUrl; + + public Callback(URL sourceURL, WebBundleUrl url, WebBundleManifestTransformer transformer) { + this.sourceURL = sourceURL; + this.webBundleUrl = url; + this.transformer = transformer; + } + + public boolean transformEntry(String entryName, InputStream is, JarOutputStream jos) throws IOException { + if (JarFile.MANIFEST_NAME.equals(entryName)) { + jos.putNextEntry(new ZipEntry(entryName)); + InputStreamReader reader = new InputStreamReader(is); + BundleManifest manifest = BundleManifestFactory.createBundleManifest(reader); + InstallationOptions options = new InstallationOptions(this.webBundleUrl.getOptions()); + if (manifest.getHeader(WebContainerUtils.HEADER_SPRINGSOURCE_DEFAULT_WAB_HEADERS) != null) { + options.setDefaultWABHeaders(true); + } + + boolean webBundle = WebContainerUtils.isWebApplicationBundle(manifest); + this.transformer.transform(manifest, sourceURL, options, webBundle); + + toManifest(manifest.toDictionary()).write(jos); + jos.closeEntry(); + return true; + } + + // Delete signature files. Should be generalised into another transformer type. + return isSignatureFile(entryName); + } + + private boolean isSignatureFile(String entryName) { + String[] entryNameComponents = entryName.split("/"); + if (entryNameComponents.length == 2) { + if ("META-INF".equals(entryNameComponents[0])) { + String entryFileName = entryNameComponents[1]; + if (entryFileName.endsWith(".SF") || entryFileName.endsWith(".DSA") || entryFileName.endsWith(".RSA")) { + return true; + } + } + } + return false; + } + + private static Manifest toManifest(Dictionary<String, String> headers) { + Manifest manifest = new Manifest(); + Attributes attributes = manifest.getMainAttributes(); + Enumeration<String> names = headers.keys(); + + while (names.hasMoreElements()) { + String name = names.nextElement(); + String value = headers.get(name); + + attributes.putValue(name, value); + } + return manifest; + } + + } + +} |