/* * 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. * <p id="registerAlgo"> * <b>Registration with RMI</b> * <p> * 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 * <code>init-params</code> are considered: <code>rmi-port</code> designating * the port on which the RMI registry is listening, <code>rmi-host</code> * designating the interface on the local host on which the RMI registry is * active, <code>repository-name</code> designating the name to which the * repository is to be bound in the registry, and <code>rmi-uri</code> * designating an RMI URI complete with host, optional port and name to which * the object is bound. * <p> * If the <code>rmi-uri</code> parameter is configured with a non-empty value, * the <code>rmi-port</code> and <code>rmi-host</code> parameters are ignored. * The <code>repository-name</code> parameter is only considered if a non-empty * <code>rmi-uri</code> parameter is configured if the latter does not contain * a name to which to bind the repository. * <p> * This is the algorithm used to find out the host, port and name for RMI * registration: * <ol> * <li>If neither a <code>rmi-uri</code> nor a <code>rmi-host</code> nor a * <code>rmi-port</code> parameter is configured, the repository is not * registered with any RMI registry. * <li>If a non-empty <code>rmi-uri</code> 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 <code>0.0.0.0</code> * meaning all interfaces on the local host, port defaults to the RMI * default port (<code>1099</code>) and the name defaults to the value * of the <code>repository-name</code> parameter. * <li>If a non-empty <code>rmi-uri</code> is not configured, the host is taken * from the <code>rmi-host</code> parameter, the port from the * <code>rmi-port</code> parameter and the name to bind the repository to * from the <code>repository-name</code> parameter. If the * <code>rmi-host</code> parameter is empty or not configured, the host * defaults to <code>0.0.0.0</code> meaning all interfaces on the local * host. If the <code>rmi-port</code> parameter is empty, not configured, * zero or a negative value, the default port for the RMI registry * (<code>1099</code>) is used. * </ol> * <p> * 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 <code>LocateRegistry.createRegistry</code> method is * called to create a registry on the local host listening on the host and port * configured. If creation fails, the <code>LocateRegistry.getRegistry</code> * method is called to get a remote instance of the registry. Note, that * <code>getRegistry</code> does not create an actual registry on the given * host/port nor does it check, whether an RMI registry is active. * <p> * 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. * <p> * Possible causes for registration failures include: * <ul> * <li>The web application is not configured to register with an RMI registry at * all. * <li>The registry is expected to be running on a remote host but does not. * <li>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. * <li>An object may already be bound to the same name as is configured to be * used for the repository. * </ul> * <p> * <b>Note:</b> if a <code>bootstrap-config</code> 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 <code>web.xml</code>. * <p> * <p> * <b>Setup Wizard Functionality</b><br> * When using the first time, the configuraition can miss the relevant * repository parameters in the web.xml. if so, it must contain a * <code>bootstrap-config</code> parameter that referrs to a propertiy file. * This file must exsit for proper working. If not, the repository is not * started.<br> * 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-<code>null</code> 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.<br> * 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 <code>null</code> 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 * <code>nulled</code>. */ 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 <a href="#registerAlgo">Registration with RMI</a> 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 <code>null</code>. */ 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. * <p> * 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 <code>RMIServerSocketFactory</code> used to create the server * socket for a locally created RMI registry. * <p> * This implementation returns a new instance of a simple * <code>RMIServerSocketFactory</code> which just creates instances of * the <code>java.net.ServerSocket</code> class bound to the given * <code>hostAddress</code>. Implementations may overwrite this method to * provide factory instances, which provide more elaborate server socket * creation, such as SSL server sockets. * * @param hostAddress The <code>InetAddress</code> instance representing the * the interface on the local host to which the server sockets are * bound. * @return A new instance of a simple <code>RMIServerSocketFactory</code> * creating <code>java.net.ServerSocket</code> instances bound to * the <code>rmiHost</code>. */ 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); } }