
org.apache.jackrabbit.j2ee.RepositoryStartupServlet Maven / Gradle / Ivy
Show all versions of jackrabbit-webapp Show documentation
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.jackrabbit.j2ee;
import org.apache.jackrabbit.api.JackrabbitRepository;
import org.apache.jackrabbit.commons.repository.RepositoryFactory;
import org.apache.jackrabbit.core.RepositoryImpl;
import org.apache.jackrabbit.core.config.RepositoryConfig;
import org.apache.jackrabbit.oak.jcr.Jcr;
import org.apache.jackrabbit.oak.plugins.segment.SegmentNodeStore;
import org.apache.jackrabbit.oak.plugins.segment.SegmentStore;
import org.apache.jackrabbit.oak.plugins.segment.file.FileStore;
import org.apache.jackrabbit.rmi.server.RemoteAdapterFactory;
import org.apache.jackrabbit.rmi.server.ServerAdapterFactory;
import org.apache.jackrabbit.servlet.AbstractRepositoryServlet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.InputSource;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.UnknownHostException;
import java.rmi.AlreadyBoundException;
import java.rmi.Naming;
import java.rmi.NoSuchObjectException;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.RMIServerSocketFactory;
import java.rmi.server.UnicastRemoteObject;
import java.util.Properties;
import javax.jcr.Repository;
import javax.jcr.RepositoryException;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* The RepositoryStartupServlet starts a jackrabbit repository and registers it
* to the JNDI environment and optional to the RMI registry.
*
* Registration with RMI
*
* Upon successfull creation of the repository in the {@link #init()} method,
* the repository is registered with an RMI registry if the web application is
* so configured. To register with RMI, the following web application
* init-params
are considered: rmi-port
designating
* the port on which the RMI registry is listening, rmi-host
* designating the interface on the local host on which the RMI registry is
* active, repository-name
designating the name to which the
* repository is to be bound in the registry, and rmi-uri
* designating an RMI URI complete with host, optional port and name to which
* the object is bound.
*
* If the rmi-uri
parameter is configured with a non-empty value,
* the rmi-port
and rmi-host
parameters are ignored.
* The repository-name
parameter is only considered if a non-empty
* rmi-uri
parameter is configured if the latter does not contain
* a name to which to bind the repository.
*
* This is the algorithm used to find out the host, port and name for RMI
* registration:
*
* - If neither a
rmi-uri
nor a rmi-host
nor a
* rmi-port
parameter is configured, the repository is not
* registered with any RMI registry.
* - If a non-empty
rmi-uri
parameter is configured extract the
* host name (or IP address), port number and name to bind to from the
* URI. If the URI is not valid, host defaults to 0.0.0.0
* meaning all interfaces on the local host, port defaults to the RMI
* default port (1099
) and the name defaults to the value
* of the repository-name
parameter.
* - If a non-empty
rmi-uri
is not configured, the host is taken
* from the rmi-host
parameter, the port from the
* rmi-port
parameter and the name to bind the repository to
* from the repository-name
parameter. If the
* rmi-host
parameter is empty or not configured, the host
* defaults to 0.0.0.0
meaning all interfaces on the local
* host. If the rmi-port
parameter is empty, not configured,
* zero or a negative value, the default port for the RMI registry
* (1099
) is used.
*
*
* After finding the host and port of the registry, the RMI registry itself
* is acquired. It is assumed, that host and port primarily designate an RMI
* registry, which should be active on the local host but has not been started
* yet. In this case, the LocateRegistry.createRegistry
method is
* called to create a registry on the local host listening on the host and port
* configured. If creation fails, the LocateRegistry.getRegistry
* method is called to get a remote instance of the registry. Note, that
* getRegistry
does not create an actual registry on the given
* host/port nor does it check, whether an RMI registry is active.
*
* When the registry has been retrieved, either by creation or by just creating
* a remote instance, the repository is bound to the configured name in the
* registry.
*
* Possible causes for registration failures include:
*
* - The web application is not configured to register with an RMI registry at
* all.
*
- The registry is expected to be running on a remote host but does not.
*
- The registry is expected to be running on the local host but cannot be
* accessed. Reasons include another application which does not act as an
* RMI registry is running on the configured port and thus blocks creation
* of a new RMI registry.
*
- An object may already be bound to the same name as is configured to be
* used for the repository.
*
*
* Note: if a bootstrap-config
init parameter is specified the
* servlet tries to read the respective resource, either as context resource or
* as file. The properties specified in this file override the init params
* specified in the web.xml
.
*
*
* Setup Wizard Functionality
* When using the first time, the configuraition can miss the relevant
* repository parameters in the web.xml. if so, it must contain a
* bootstrap-config
parameter that referrs to a propertiy file.
* This file must exsit for proper working. If not, the repository is not
* started.
* If the servlet is not configured correctly and accessed via http, it will
* provide a simple wizard for the first time configuration. It propmpts for
* a new (or existing) repository home and will copy the templates of the
* repository.xml and bootstrap.properties to the respective location.
*/
public class RepositoryStartupServlet extends AbstractRepositoryServlet {
/**
* the default logger
*/
private static final Logger log = LoggerFactory.getLogger(RepositoryStartupServlet.class);
/**
* the context attribute name foe 'this' instance.
*/
private final static String CTX_PARAM_THIS = "repository.startup.servet";
/**
* initial param name for the bootstrap config location
*/
public final static String INIT_PARAM_BOOTSTRAP_CONFIG = "bootstrap-config";
/**
* Ugly hack to override the bootstrap file location in the test cases
*/
static String bootstrapOverride = null;
/**
* the TarMK segment store
*/
private SegmentStore store;
/**
* the registered repository
*/
private Repository repository;
/**
* the jndi context; created based on configuration
*/
private InitialContext jndiContext;
private Registry rmiRegistry = null;
/**
* Keeps a strong reference to the server side RMI repository instance to
* prevent the RMI distributed Garbage Collector from collecting the
* instance making the repository unaccessible though it should still be.
* This field is only set to a non-null
value, if registration
* of the repository to an RMI registry succeeded in the
* {@link #registerRMI()} method.
*
* @see #registerRMI()
* @see #unregisterRMI()
*/
private Remote rmiRepository;
/**
* the file to the bootstrap config
*/
private File bootstrapConfigFile;
/**
* The bootstrap configuration
*/
private BootstrapConfig config;
/**
* Initializes the servlet.
* Please note that only one repository startup servlet may exist per
* webapp. it registers itself as context attribute and acts as singleton.
*
* @throws ServletException if a same servlet is already registered or of
* another initialization error occurs.
*/
public void init() throws ServletException {
super.init();
// check if servlet is defined twice
if (getServletContext().getAttribute(CTX_PARAM_THIS) != null) {
throw new ServletException("Only one repository startup servlet allowed per web-app.");
}
getServletContext().setAttribute(CTX_PARAM_THIS, this);
startup();
}
/**
* Returns an instance of this servlet. Please note, that only 1
* repository startup servlet can exist per webapp.
*
* @param context the servlet context
* @return this servlet
*/
public static RepositoryStartupServlet getInstance(ServletContext context) {
return (RepositoryStartupServlet) context.getAttribute(CTX_PARAM_THIS);
}
/**
* Configures and starts the repository. It registers it then to the
* RMI registry and bind is to the JNDI context if so configured.
* @throws ServletException if an error occurs.
*/
public void startup() throws ServletException {
if (repository != null) {
log.error("Startup: Repository already running.");
throw new ServletException("Repository already running.");
}
log.info("RepositoryStartupServlet initializing...");
try {
if (configure()) {
initRepository();
registerRMI();
registerJNDI();
}
log.info("RepositoryStartupServlet initialized.");
} catch (ServletException e) {
// shutdown repository
shutdownRepository();
log.error("RepositoryStartupServlet initializing failed: " + e, e);
}
}
/**
* Does a shutdown of the repository and deregisters it from the RMI
* registry and unbinds if from the JNDI context if so configured.
*/
public void shutdown() {
if (repository == null) {
log.info("Shutdown: Repository already stopped.");
} else {
log.info("RepositoryStartupServlet shutting down...");
shutdownRepository();
unregisterRMI();
unregisterJNDI();
log.info("RepositoryStartupServlet shut down.");
}
}
/**
* Restarts the repository.
* @throws ServletException if an error occurs.
* @see #shutdown()
* @see #startup()
*/
public void restart() throws ServletException {
if (repository != null) {
shutdown();
}
startup();
}
/**
* destroy the servlet
*/
public void destroy() {
super.destroy();
shutdown();
}
/**
* Returns the started repository or null
if not started
* yet.
* @return the JCR repository
*/
public Repository getRepository() {
return repository;
}
/**
* Returns a repository factory that returns the repository if available
* or throws an exception if not.
*
* @return repository factory
*/
public RepositoryFactory getRepositoryFactory() {
return new RepositoryFactory() {
public Repository getRepository() throws RepositoryException {
Repository r = repository;
if (r != null) {
return repository;
} else {
throw new RepositoryException("Repository not available");
}
}
};
}
/**
* Reads the configuration and initializes the {@link #config} field if
* successful.
* @throws ServletException if an error occurs.
*/
private boolean configure() throws ServletException {
// check if there is a loadable bootstrap config
Properties bootstrapProps = new Properties();
String bstrp = bootstrapOverride;
if (bstrp == null) {
bstrp = getServletConfig().getInitParameter(INIT_PARAM_BOOTSTRAP_CONFIG);
}
if (bstrp != null) {
// check if it's a web-resource
InputStream in = getServletContext().getResourceAsStream(bstrp);
if (in == null) {
// check if it's a file
bootstrapConfigFile = new File(bstrp);
if (bootstrapConfigFile.canRead()) {
try {
in = new FileInputStream(bootstrapConfigFile);
} catch (FileNotFoundException e) {
throw new ServletExceptionWithCause(
"Bootstrap configuration not found: " + bstrp, e);
}
}
}
if (in != null) {
try {
bootstrapProps.load(in);
} catch (IOException e) {
throw new ServletException(
"Bootstrap configuration failure: " + bstrp, e);
} finally {
try {
in.close();
} catch (IOException e) {
// ignore
}
}
}
}
// read bootstrap config
config = new BootstrapConfig();
config.init(getServletConfig());
config.init(bootstrapProps);
config.validate();
if (!config.isValid()
|| config.getRepositoryHome() == null) {
if (bstrp == null) {
log.error("Repository startup configuration is not valid.");
} else {
log.error("Repository startup configuration is not valid but a bootstrap config is specified.");
log.error("Either create the {} file or", bstrp);
log.error("use the '/config/index.jsp' for easy configuration.");
}
return false;
} else {
config.logInfos();
return true;
}
}
/**
* Creates a new Repository based on the configuration and initializes the
* {@link #repository} field if successful.
*
* @throws ServletException if an error occurs
*/
private void initRepository() throws ServletException {
// get repository config
File repHome;
try {
repHome = new File(config.getRepositoryHome()).getCanonicalFile();
} catch (IOException e) {
throw new ServletExceptionWithCause(
"Repository configuration failure: " + config.getRepositoryHome(), e);
}
String repConfig = config.getRepositoryConfig();
if (repConfig != null) { // Jackrabbit Classic
InputStream in = getServletContext().getResourceAsStream(repConfig);
if (in == null) {
try {
in = new FileInputStream(new File(repConfig));
} catch (FileNotFoundException e) {
// fallback to old config
try {
in = new FileInputStream(new File(repHome, repConfig));
} catch (FileNotFoundException e1) {
throw new ServletExceptionWithCause(
"Repository configuration not found: " + repConfig, e);
}
}
}
try {
repository = createRepository(new InputSource(in), repHome);
} catch (RepositoryException e) {
throw new ServletExceptionWithCause("Error while creating repository", e);
}
} else { // Jackrabbit Oak
try {
String model = System.getProperty("sun.arch.data.model", "32");
store = new FileStore(repHome, 256, "64".equals(model));
repository = new Jcr(new SegmentNodeStore(store)).createRepository();
} catch (IOException e) {
throw new ServletExceptionWithCause("Error while creating repository", e);
}
}
}
/**
* Shuts down the repository. If the repository is an instanceof
* {@link JackrabbitRepository} it's {@link JackrabbitRepository#shutdown()}
* method is called. in any case, the {@link #repository} field is
* nulled
.
*/
private void shutdownRepository() {
if (store != null) {
store.close();
store = null;
} else if (repository instanceof JackrabbitRepository) {
((JackrabbitRepository) repository).shutdown();
}
repository = null;
}
/**
* Creates the repository instance for the given config and homedir.
* Subclasses may override this method of providing own implementations of
* a {@link Repository}.
*
* @param is input source of the repository config
* @param homedir the repository home directory
* @return a new jcr repository.
* @throws RepositoryException if an error during creation occurs.
*/
protected Repository createRepository(InputSource is, File homedir)
throws RepositoryException {
RepositoryConfig config = RepositoryConfig.create(is, homedir.getAbsolutePath());
return RepositoryImpl.create(config);
}
/**
* Binds the repository to the JNDI context
* @throws ServletException if an error occurs.
*/
private void registerJNDI() throws ServletException {
JNDIConfig jc = config.getJndiConfig();
if (jc.isValid() && jc.enabled()) {
try {
jndiContext = new InitialContext(jc.getJndiEnv());
jndiContext.bind(jc.getJndiName(), repository);
log.info("Repository bound to JNDI with name: " + jc.getJndiName());
} catch (NamingException e) {
throw new ServletExceptionWithCause(
"Unable to bind repository using JNDI: " + jc.getJndiName(), e);
}
}
}
/**
* Unbinds the repository from the JNDI context.
*/
private void unregisterJNDI() {
if (jndiContext != null) {
try {
jndiContext.unbind(config.getJndiConfig().getJndiName());
} catch (NamingException e) {
log("Error while unbinding repository from JNDI: " + e);
}
}
}
/**
* Registers the repository to an RMI registry configured in the web
* application. See Registration with RMI in the
* class documentation for a description of the algorithms used to register
* the repository with an RMI registry.
* @throws ServletException if an error occurs.
*/
private void registerRMI() {
RMIConfig rc = config.getRmiConfig();
if (!rc.isValid() || !rc.enabled()) {
return;
}
// try to create remote repository
Remote remote;
try {
Class> clazz = Class.forName(getRemoteFactoryDelegaterClass());
RemoteFactoryDelegater rmf = (RemoteFactoryDelegater) clazz.newInstance();
remote = rmf.createRemoteRepository(repository);
} catch (RemoteException e) {
log.warn("Unable to create RMI repository.", e);
return;
} catch (Throwable t) {
log.warn("Unable to create RMI repository."
+ " The jcr-rmi jar might be missing.", t);
return;
}
try {
System.setProperty("java.rmi.server.useCodebaseOnly", "true");
Registry reg = null;
// first try to create the registry, which will fail if another
// application is already running on the configured host/port
// or if the rmiHost is not local
try {
// find the server socket factory: use the default if the
// rmiHost is not configured
RMIServerSocketFactory sf;
if (rc.getRmiHost().length() > 0) {
log.debug("Creating RMIServerSocketFactory for host " + rc.getRmiHost());
InetAddress hostAddress = InetAddress.getByName(rc.getRmiHost());
sf = getRMIServerSocketFactory(hostAddress);
} else {
// have the RMI implementation decide which factory is the
// default actually
log.debug("Using default RMIServerSocketFactory");
sf = null;
}
// create a registry using the default client socket factory
// and the server socket factory retrieved above. This also
// binds to the server socket to the rmiHost:rmiPort.
reg = LocateRegistry.createRegistry(rc.rmiPort(), null, sf);
rmiRegistry = reg;
} catch (UnknownHostException uhe) {
// thrown if the rmiHost cannot be resolved into an IP-Address
// by getRMIServerSocketFactory
log.info("Cannot create Registry", uhe);
} catch (RemoteException e) {
// thrown by createRegistry if binding to the rmiHost:rmiPort
// fails, for example due to rmiHost being remote or another
// application already being bound to the port
log.info("Cannot create Registry", e);
}
// if creation of the registry failed, we try to access an
// potentially active registry. We do not check yet, whether the
// registry is actually accessible.
if (reg == null) {
log.debug("Trying to access existing registry at " + rc.getRmiHost()
+ ":" + rc.getRmiPort());
try {
reg = LocateRegistry.getRegistry(rc.getRmiHost(), rc.rmiPort());
} catch (RemoteException re) {
log.warn("Cannot create the reference to the registry at "
+ rc.getRmiHost() + ":" + rc.getRmiPort(), re);
}
}
// if we finally have a registry, register the repository with the
// rmiName
if (reg != null) {
log.debug("Registering repository as " + rc.getRmiName()
+ " to registry " + reg);
reg.bind(rc.getRmiName(), remote);
// when successfull, keep references
this.rmiRepository = remote;
log.info("Repository bound via RMI with name: " + rc.getRmiUri());
} else {
log.info("RMI registry missing, cannot bind repository via RMI");
}
} catch (RemoteException e) {
log.warn("Unable to bind repository via RMI: " + rc.getRmiUri(), e);
} catch (AlreadyBoundException e) {
log.warn("Unable to bind repository via RMI: " + rc.getRmiUri(), e);
}
}
/**
* Unregisters the repository from the RMI registry, if it has previously
* been registered.
*/
private void unregisterRMI() {
if (rmiRepository != null) {
// Forcibly unexport the repository;
try {
UnicastRemoteObject.unexportObject(rmiRepository, true);
} catch (NoSuchObjectException e) {
log.warn("Odd, the RMI repository was not exported", e);
}
// drop strong reference to remote repository
rmiRepository = null;
// unregister repository
try {
Naming.unbind(config.getRmiConfig().getRmiUri());
} catch (Exception e) {
log("Error while unbinding repository from JNDI: " + e);
}
}
if (rmiRegistry != null) {
try {
UnicastRemoteObject.unexportObject(rmiRegistry, true);
} catch (NoSuchObjectException e) {
log.warn("Odd, the RMI registry was not exported", e);
}
rmiRegistry = null;
}
}
/**
* Returns the config that was used to bootstrap this servlet.
* @return the bootstrap config or null
.
*/
public BootstrapConfig getBootstrapConfig() {
return config;
}
/**
* Return the fully qualified name of the class providing the remote
* repository. The class whose name is returned must implement the
* {@link RemoteFactoryDelegater} interface.
*
* Subclasses may override this method for providing a name of a own
* implementation.
*
* @return getClass().getName() + "$RMIRemoteFactoryDelegater"
*/
protected String getRemoteFactoryDelegaterClass() {
return getClass().getName() + "$RMIRemoteFactoryDelegater";
}
/**
* Returns an RMIServerSocketFactory
used to create the server
* socket for a locally created RMI registry.
*
* This implementation returns a new instance of a simple
* RMIServerSocketFactory
which just creates instances of
* the java.net.ServerSocket
class bound to the given
* hostAddress
. Implementations may overwrite this method to
* provide factory instances, which provide more elaborate server socket
* creation, such as SSL server sockets.
*
* @param hostAddress The InetAddress
instance representing the
* the interface on the local host to which the server sockets are
* bound.
* @return A new instance of a simple RMIServerSocketFactory
* creating java.net.ServerSocket
instances bound to
* the rmiHost
.
*/
protected RMIServerSocketFactory getRMIServerSocketFactory(
final InetAddress hostAddress) {
return new RMIServerSocketFactory() {
public ServerSocket createServerSocket(int port) throws IOException {
return new ServerSocket(port, -1, hostAddress);
}
};
}
/**
* optional class for RMI, will only be used, if RMI server is present
*/
protected static abstract class RemoteFactoryDelegater {
public abstract Remote createRemoteRepository(Repository repository)
throws RemoteException;
}
/**
* optional class for RMI, will only be used, if RMI server is present
*/
protected static class RMIRemoteFactoryDelegater extends RemoteFactoryDelegater {
private static final RemoteAdapterFactory FACTORY =
new ServerAdapterFactory();
public Remote createRemoteRepository(Repository repository)
throws RemoteException {
return FACTORY.getRemoteRepository(repository);
}
}
//-------------------------------------------------< Installer Routines >---
/**
* {@inheritDoc}
*/
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
if (repository == null) {
redirect(req, resp, "/bootstrap/missing.jsp");
} else {
redirect(req, resp, "/bootstrap/running.jsp");
}
}
/**
* {@inheritDoc}
*/
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
if (repository != null) {
redirect(req, resp, "/bootstrap/reconfigure.jsp");
} else {
int rc = new Installer(bootstrapConfigFile,
getServletContext()).installRepository(req);
switch (rc) {
case Installer.C_INSTALL_OK:
// restart rep
restart();
if (repository == null) {
redirect(req, resp, "/bootstrap/error.jsp");
} else {
redirect(req, resp, "/bootstrap/success.jsp");
}
break;
case Installer.C_INVALID_INPUT:
redirect(req, resp, "/bootstrap/missing.jsp");
break;
case Installer.C_CONFIG_EXISTS:
case Installer.C_BOOTSTRAP_EXISTS:
case Installer.C_HOME_EXISTS:
redirect(req, resp, "/bootstrap/exists.jsp");
break;
case Installer. C_HOME_MISSING:
case Installer.C_CONFIG_MISSING:
redirect(req, resp, "/bootstrap/notexists.jsp");
break;
case Installer.C_INSTALL_ERROR:
redirect(req, resp, "/bootstrap/error.jsp");
break;
}
}
}
/**
* Helper function to send a redirect response respecting the context path.
*
* @param req the request
* @param resp the response
* @param loc the location for the redirect
* @throws ServletException if an servlet error occurs.
* @throws IOException if an I/O error occurs.
*/
private void redirect(HttpServletRequest req,
HttpServletResponse resp, String loc)
throws ServletException, IOException {
String cp = req.getContextPath();
if (cp == null || cp.equals("/")) {
cp = "";
}
resp.sendRedirect(cp + loc);
}
}