org.hippoecm.repository.PingServlet.java Source code

Java tutorial

Introduction

Here is the source code for org.hippoecm.repository.PingServlet.java

Source

/*
 *  Copyright 2009-2015 Hippo B.V. (http://www.onehippo.com)
 * 
 *  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.hippoecm.repository;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.Calendar;

import javax.jcr.LoginException;
import javax.jcr.Node;
import javax.jcr.PathNotFoundException;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.apache.commons.lang.StringUtils;
import org.onehippo.repository.security.JvmCredentials;

/**
 * <p>A servlet that can be used to check if the repository is up-and-running. This
 * is especially useful for load balancer checks. The check does the following steps:</p>
 * <ul>
 *   <li>Check for static custom return message, else</li>
 *   <ul>
 *     <li>obtain the repository with the connection string</li>
 *     <li>obtain the session with the specified username and password or an anonymous session</li>
 *     <li>try to read the check node</li>
 *     <li>try to write to the repository if enabled</li>
 *     <li>logout and close jcr session</li>
 *   </ul>
 *   <li>logout and close http session</li>
 * </ul>
 * <p>On success the servlet prints "Ok" and returns a 200 status, on failure, the error is 
 * printed and a 500 (internal server error) status is returned.</p>
 * <p>In case the custom message is provided, a service unavailable error (503) is returned</p>
 * 
 * <p>To enable the servlet add the following to your web.xml. Set the username to anonymous or leave out the username
  * for anonymous checks.</p>
 * <code><![CDATA[
<servlet>
  <servlet-name>PingServlet</servlet-name>
  <servlet-class>org.hippoecm.repository.PingServlet</servlet-class>
  <init-param>
    <param-name>repository-address</param-name>
    <param-value>rmi://localhost:1099/hipporepository</param-value>
  </init-param>
  <init-param>
    <param-name>check-username</param-name>
    <param-value>admin</param-value>
  </init-param>
  <init-param>
    <param-name>check-password</param-name>
    <param-value>admin</param-value>
  </init-param>
  <init-param>
    <param-name>check-node</param-name>
    <param-value>content/documents</param-value>
  </init-param>
  <init-param>
    <param-name>write-check-enable</param-name>
    <param-value>false</param-value>
  </init-param>
  <init-param>
    <param-name>write-check-node</param-name>
    <param-value>pingcheck</param-value>
  </init-param>
  <!-- enable while doing upgrades
    init-param>
    <param-name>custom-message</param-name>
    <param-value>Down for upgrade</param-value>
  </init-param -->
</servlet>
<servlet-mapping>
  <servlet-name>PingServlet</servlet-name>
  <url-pattern>/ping/*</url-pattern>
</servlet-mapping>
 * ]]></code>
 *
 */
public class PingServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;

    /** Servlet parameters */
    private static final String REPOSITORY_ADDRESS_PARAM = "repository-address";
    private static final String USERNAME_PARAM = "check-username";
    private static final String PASSWORD_PARAM = "check-password";
    private static final String NODE_PARAM = "check-node";
    private static final String WRITE_ENABLE_PARAM = "write-check-enable";
    private static final String WRITE_PATH_PARAM = "write-check-path";
    private static final String CUSTOM_MESSAGE_PARAM = "custom-message";

    /** Default values */
    private static final String DEFAULT_REPOSITORY_ADDRESS = "vm://";
    private static final String DEFAULT_USERNAME = "anonymous";
    private static final String DEFAULT_PASSWORD = "";
    private static final String DEFAULT_NODE = "";
    private static final String DEFAULT_WRITE_ENABLE = "false";
    private static final String DEFAULT_WRITE_PATH = "pingcheck";
    private static final String DEFAULT_CLUSTER_NODE_ID = "default";

    /** Running config */
    private String repositoryLocation;
    private String username;
    private String password;
    private String checkNode;
    private String customMessage;
    private String writeTestPath;
    private boolean writeTestEnabled = false;

    /** Local vars */
    private volatile HippoRepository repository;

    @Override
    public void init(ServletConfig config) throws ServletException {
        super.init(config);
        repositoryLocation = getParameter(config, REPOSITORY_ADDRESS_PARAM, DEFAULT_REPOSITORY_ADDRESS);
        username = getParameter(config, USERNAME_PARAM, DEFAULT_USERNAME);
        password = getParameter(config, PASSWORD_PARAM, DEFAULT_PASSWORD);
        checkNode = makePathRelative(getParameter(config, NODE_PARAM, DEFAULT_NODE));
        writeTestPath = makePathRelative(getParameter(config, WRITE_PATH_PARAM, DEFAULT_WRITE_PATH));
        writeTestEnabled = isTrueOrYes(getParameter(config, WRITE_ENABLE_PARAM, DEFAULT_WRITE_ENABLE));
        customMessage = getParameter(config, CUSTOM_MESSAGE_PARAM, null);
    }

    private String getParameter(ServletConfig config, String paramName, String defaultValue) {
        String initValue = config.getInitParameter(paramName);
        String contextValue = config.getServletContext().getInitParameter(paramName);
        if (isNotNullAndNotEmpty(initValue)) {
            return initValue;
        } else if (isNotNullAndNotEmpty(contextValue)) {
            return contextValue;
        } else {
            return defaultValue;
        }
    }

    private boolean isNotNullAndNotEmpty(String s) {
        return (s != null && s.length() != 0);
    }

    private String makePathRelative(String path) {
        while (path.startsWith("/")) {
            path = path.substring(1);
        }
        return path;
    }

    private boolean isTrueOrYes(String s) {
        return ("true".equalsIgnoreCase(s) || "yes".equalsIgnoreCase(s));
    }

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        int resultStatus = HttpServletResponse.SC_OK;
        String resultMessage = getOkMessage();
        Exception exception = null;

        if (hasCustomMessage()) {
            resultMessage = "CUSTOM - " + customMessage;
            resultStatus = HttpServletResponse.SC_SERVICE_UNAVAILABLE;
            printMessage(response, resultMessage, resultStatus, exception);
            return;
        }

        try {
            doRepositoryChecks();
        } catch (PingException e) {
            resultStatus = HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
            resultMessage = e.getMessage();
            exception = e;
        } catch (RuntimeException e) {
            resultStatus = HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
            resultMessage = "FAILURE - Serious problem with the ping servlet. Might have lost repository access: "
                    + e.getClass().getName() + ": " + e.getMessage();
            exception = e;
        } finally {
            printMessage(response, resultMessage, resultStatus, exception);
            closeHttpSession(request);
        }
    }

    private String getOkMessage() {
        String resultMessage = "OK - Repository online and accessible.";
        if (writeTestEnabled) {
            resultMessage = "OK - Repository online, accessible and writable.";
        }
        return resultMessage;
    }

    private boolean hasCustomMessage() {
        return (customMessage != null);
    }

    private void printMessage(HttpServletResponse response, String message, int status, Exception exception)
            throws IOException {
        response.setContentType("text/plain");
        response.setStatus(status);
        PrintWriter writer = response.getWriter();
        writer.println(message);
        if (exception != null) {
            exception.printStackTrace(writer);
        }
    }

    private void closeHttpSession(HttpServletRequest req) {
        if (req != null) {
            // close open session
            HttpSession httpSession = req.getSession(false);
            if (httpSession != null) {
                httpSession.invalidate();
            }
        }
    }

    private synchronized void doRepositoryChecks() throws PingException {
        Session session = null;
        try {
            session = obtainSession();
            doReadTest(session);
            doWriteTestIfEnabled(session);
        } finally {
            closeSession(session);
        }
    }

    private void obtainRepository() throws PingException {
        try {
            repository = HippoRepositoryFactory.getHippoRepository(repositoryLocation);
        } catch (RepositoryException e) {
            repository = null;
            String msg = "FAILURE - Problem obtaining repository connection in ping servlet : Is the property"
                    + " '" + REPOSITORY_ADDRESS_PARAM + "' configured as an init-param or context-param?";
            throw new PingException(msg, e);
        }
    }

    private Session obtainSession() throws PingException {
        if (repository == null) {
            obtainRepository();
        }
        try {
            if (username.isEmpty() || "anonymous".equalsIgnoreCase(username)) {
                return repository.login();
            }
            if (StringUtils.isBlank(password)) {
                // try as jvm enabled user
                final JvmCredentials credentials = JvmCredentials.getCredentials(username);
                return repository.login(credentials);
            } else {
                return repository.login(username, password.toCharArray());
            }
        } catch (LoginException e) {
            String msg = "FAILURE - Wrong credentials for obtaining session from repository in ping servlet : " + ""
                    + "Are the '" + USERNAME_PARAM + "' and '" + PASSWORD_PARAM
                    + "' configured as an init-param or context-param?";
            throw new PingException(msg, e);
        } catch (RepositoryException e) {
            // make sure the the obtainRepository is tried again the next request.
            repository = null;
            String msg = "FAILURE - Problem obtaining session from repository in ping servlet : Are the '"
                    + USERNAME_PARAM + "' and" + " '" + PASSWORD_PARAM
                    + "' configured as an init-param or context-param?";
            throw new PingException(msg, e);
        }
    }

    private void closeSession(Session session) {
        if (session != null && session.isLive()) {
            session.logout();
        }
    }

    private void doReadTest(Session session) throws PingException {
        String msg;
        try {
            if (checkNode.isEmpty()) {
                session.getRootNode();
            } else {
                session.getRootNode().getNode(checkNode);
            }
        } catch (PathNotFoundException e) {
            msg = "FAILURE - Path for node to lookup '" + checkNode + "' is not found by ping servlet. ";
            throw new PingException(msg, e);
        } catch (RepositoryException e) {
            msg = "FAILURE - Could not obtain a node, which is at this point unexpected since we already have a connection."
                    + "Maybe we lost the connection to the repository.";
            throw new PingException(msg, e);
        }
    }

    private void doWriteTestIfEnabled(Session session) throws PingException {
        if (writeTestEnabled) {
            doWriteTest(session);
        }
    }

    private void doWriteTest(Session session) throws PingException {
        try {
            Node writePath = getOrCreateWriteNode(session);
            writePath.setProperty("lastcheck", Calendar.getInstance());
            session.save();
        } catch (RepositoryException e) {
            String msg = "FAILURE - Error during write test. There could be an issue with the (connection to) the storage.";
            throw new PingException(msg, e);
        }
    }

    private Node getOrCreateWriteNode(Session session) throws PingException {
        Node path = getOrCreateWritePath(session);
        String clusterId = getClusterNodeId();
        try {
            if (path.hasNode(clusterId)) {
                return path.getNode(clusterId);
            } else {
                Node node = path.addNode(clusterId);
                session.save();
                return node;
            }
        } catch (RepositoryException e) {
            String msg = "FAILURE - Could not obtain the write test node '" + writeTestPath + "/" + clusterId
                    + "'.";
            throw new PingException(msg, e);
        }
    }

    private Node getOrCreateWritePath(Session session) throws PingException {
        Node path;
        try {
            if (session.getRootNode().hasNode(writeTestPath)) {
                path = session.getRootNode().getNode(writeTestPath);
            } else {
                path = session.getRootNode().addNode(writeTestPath);
                session.save();
            }
            return path;
        } catch (RepositoryException e) {
            String msg = "FAILURE - Could not obtain the write path node '" + writeTestPath + "'.";
            throw new PingException(msg, e);
        }
    }

    private String getClusterNodeId() {
        String clusterNodeId = null;
        if (repository != null) {
            clusterNodeId = repository.getRepository().getDescriptor("jackrabbit.cluster.id");
        }
        if (isNotNullAndNotEmpty(clusterNodeId)) {
            return clusterNodeId;
        } else {
            return DEFAULT_CLUSTER_NODE_ID;
        }
    }

    /**
     * Internal Exception class to be used internally to communicate the exception during the ping of the repository
     */
    private static final class PingException extends Exception {
        private static final long serialVersionUID = 1L;

        private PingException(String s) {
            super(s);
        }

        private PingException(String s, Exception e) {
            super(s, e);
        }
    }
}