org.onehippo.forge.content.exim.repository.jaxrs.AbstractContentEximService.java Source code

Java tutorial

Introduction

Here is the source code for org.onehippo.forge.content.exim.repository.jaxrs.AbstractContentEximService.java

Source

/*
 * Copyright 2018 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.onehippo.forge.content.exim.repository.jaxrs;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.security.Principal;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import javax.jcr.Credentials;
import javax.jcr.LoginException;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.SimpleCredentials;
import javax.jcr.query.Query;
import javax.jcr.query.QueryResult;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.core.SecurityContext;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.math.NumberUtils;
import org.apache.commons.lang.time.FastDateFormat;
import org.apache.commons.vfs2.FileObject;
import org.apache.cxf.jaxrs.ext.multipart.Attachment;
import org.apache.cxf.jaxrs.ext.multipart.ContentDisposition;
import org.onehippo.cms7.utilities.logging.PrintStreamLogger;
import org.onehippo.forge.content.exim.core.ContentMigrationRecord;
import org.onehippo.forge.content.exim.core.util.AntPathMatcher;
import org.onehippo.forge.content.exim.core.util.TeeLoggerWrapper;
import org.onehippo.forge.content.exim.repository.jaxrs.param.ExecutionParams;
import org.onehippo.forge.content.exim.repository.jaxrs.param.QueriesAndPaths;
import org.onehippo.forge.content.exim.repository.jaxrs.param.ResultItem;
import org.onehippo.forge.content.exim.repository.jaxrs.status.ProcessStatus;
import org.onehippo.forge.content.exim.repository.jaxrs.util.ServletRequestUtils;
import org.onehippo.forge.content.pojo.model.ContentNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;

/**
 * AbstractContentEximService.
 */
public abstract class AbstractContentEximService {

    private static Logger log = LoggerFactory.getLogger(AbstractContentEximService.class);

    /**
     * System session credentials.
     */
    protected static final Credentials SYSTEM_CREDENTIALS = new SimpleCredentials("system", new char[] {});

    /**
     * Prefix of the temporary folder or files. e.g, temporary folder in zip content creation.
     */
    protected static final String TEMP_PREFIX = "_exim_";

    /**
     * The whole execution log file entry name.
     */
    protected static final String EXIM_EXECUTION_LOG_REL_PATH = "EXIM-INF/execution.log";

    /**
     * Zip Entry name of the summary log for binaries.
     */
    protected static final String EXIM_SUMMARY_BINARIES_LOG_REL_PATH = "EXIM-INF/summary-binaries.log";

    /**
     * Zip Entry name of the summary log for documents.
     */
    protected static final String EXIM_SUMMARY_DOCUMENTS_LOG_REL_PATH = "EXIM-INF/summary-documents.log";

    /**
     * Zip Entry name prefix for the binary attachments.
     */
    protected static final String BINARY_ATTACHMENT_REL_PATH = "EXIM-INF/data/attachments";

    /**
     * Stop signal file's relative path under the zip creating base folder.
     * If this file is found in the process, the export or import process will stop right away.
     */
    protected static final String STOP_REQUEST_FILE_REL_PATH = "EXIM-INF/_stop_";

    private ProcessMonitor processMonitor;

    /**
     * Jackson ObjectMapper instance.
     */
    private ObjectMapper objectMapper;

    /**
     * JCR session given by the DaemonModule and used to create a new system session from it.
     */
    private Session daemonSession;

    /**
     * Default constructor.
     */
    public AbstractContentEximService() {
        objectMapper = new ObjectMapper();
        objectMapper.configure(JsonParser.Feature.ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER, true);
        objectMapper.configure(JsonParser.Feature.ALLOW_COMMENTS, true);
        objectMapper.configure(JsonParser.Feature.ALLOW_MISSING_VALUES, true);
        objectMapper.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true);
        objectMapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);
        objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
    }

    protected void setProcessMonitor(ProcessMonitor processMonitor) {
        this.processMonitor = processMonitor;
    }

    protected ProcessMonitor getProcessMonitor() {
        return processMonitor;
    }

    /**
     * Return the default Jackson ObjectMapper instance.
     * @return ObjectMapper instance
     */
    protected ObjectMapper getObjectMapper() {
        return objectMapper;
    }

    /**
     * Return the JCR session given by the DaemonModule.
     * It is not supposed to use this JCR session directly to retrieve or manipulate content.
     * You should use {@link #createSession()} for that purpose instead.
     * @return the JCR session given by the DaemonModule
     */
    protected Session getDaemonSession() {
        return daemonSession;
    }

    /**
     * Set the JCR session, supposed to be set by the DaemonModule.
     * @param daemonSession the JCR session given by the DaemonModule
     */
    protected void setDaemonSession(Session daemonSession) {
        this.daemonSession = daemonSession;
    }

    /**
     * Create a new JCR system session by impersonating the JCR session returned from {@link #getDaemonSession()}.
     * @return a new JCR system session by impersonating the JCR session returned from {@link #getDaemonSession()}
     * @throws LoginException if impersonation with system credentials fails
     * @throws RepositoryException if repository exception occurs
     */
    protected Session createSession() throws LoginException, RepositoryException {
        return getDaemonSession().impersonate(SYSTEM_CREDENTIALS);
    }

    /**
     * Return true if a stop signal file is found under the base folder.
     * @param baseFolder the base folder where zip content files are created temporarily.
     * @return true if a stop signal file is found under the base folder
     */
    protected boolean isStopRequested(FileObject baseFolder) {
        try {
            FileObject stopSignalFile = baseFolder.resolveFile(STOP_REQUEST_FILE_REL_PATH);
            return stopSignalFile.exists();
        } catch (Exception e) {
            log.error("Failed to check stop request file.", e);
        }

        return false;
    }

    /**
     * Return a JSON string by stringifying the {@code object} with the Jackson ObjectMapper.
     * @param object object to stringify
     * @return a JSON string by stringifying the {@code object} with the Jackson ObjectMapper
     * @throws JsonProcessingException if JSON stringifying fails
     */
    protected String toJsonString(Object object) throws JsonProcessingException {
        return getObjectMapper().writeValueAsString(object);
    }

    /**
     * Read the CXF attachment JAX-RS argument and convert it to a string.
     * @param attachment CXF attachment JAX-RS argument
     * @param charsetName charset name used in encoding
     * @return string converted from the CXF attachment JAX-RS argument
     * @throws IOException if IO exception occurs
     */
    protected String attachmentToString(Attachment attachment, String charsetName) throws IOException {
        InputStream input = null;

        try {
            input = attachment.getObject(InputStream.class);
            return IOUtils.toString(input, charsetName);
        } finally {
            IOUtils.closeQuietly(input);
        }
    }

    /**
     * Transfer attachment content into the given {@code file}.
     * @param attachment attachment
     * @param file destination file
     * @throws IOException if IO exception occurs
     */
    protected void transferAttachmentToFile(Attachment attachment, File file) throws IOException {
        InputStream input = null;
        OutputStream output = null;

        try {
            input = attachment.getObject(InputStream.class);
            output = new FileOutputStream(file);
            IOUtils.copyLarge(input, output);
        } finally {
            IOUtils.closeQuietly(output);
            IOUtils.closeQuietly(input);
        }
    }

    /**
     * Convert the {@link ContentMigrationRecord} instance to a {@link ResultItem} instance.
     * @param record a {@link ContentMigrationRecord} instance
     * @return a converted {@link ResultItem} instance
     */
    protected ResultItem recordToResultItem(ContentMigrationRecord record) {
        ResultItem item = new ResultItem(record.getContentPath(), record.getContentType());
        item.setSucceeded(record.isSucceeded());
        item.setErrorMessage(record.getErrorMessage());
        return item;
    }

    /**
     * Executes JCR query using the query {@code statement} in the query {@code language} and collect all the result
     * node paths in a set to return.
     * @param session JCR session
     * @param statement JCR query statement
     * @param language JCR query language
     * @return a set containing all the nodes from the query result
     * @throws RepositoryException if repository exception occurs
     */
    protected Set<String> getQueriedNodePaths(Session session, String statement, String language)
            throws RepositoryException {
        Set<String> nodePaths = new LinkedHashSet<>();
        Query query = session.getWorkspace().getQueryManager().createQuery(statement, language);
        QueryResult result = query.execute();

        for (NodeIterator nodeIt = result.getNodes(); nodeIt.hasNext();) {
            Node node = nodeIt.nextNode();

            if (node != null) {
                nodePaths.add(node.getPath());
            }
        }

        return nodePaths;
    }

    /**
     * Override {@code params} by the give request parameter values.
     * @param params {@link ExecutionParams} instance
     * @param batchSizeParam batch size request parameter value
     * @param throttleParam throttle request parameter value
     * @param publishOnImportParam publishOnImport request parameter value
     * @param dataUrlSizeThresholdParam dataUrlSizeThreshold request parameter value
     * @param docbasePropNamesParam docbasePropNames request parameter value
     * @param documentTagsParam documentTags request parameter value
     * @param binaryTagsParam binaryTags request parameter value
     */
    protected void overrideExecutionParamsByParameters(ExecutionParams params, String batchSizeParam,
            String throttleParam, String publishOnImportParam, String dataUrlSizeThresholdParam,
            String docbasePropNamesParam, String documentTagsParam, String binaryTagsParam) {
        if (StringUtils.isNotBlank(batchSizeParam)) {
            params.setBatchSize(NumberUtils.toInt(batchSizeParam, params.getBatchSize()));
        }

        if (StringUtils.isNotBlank(throttleParam)) {
            params.setThrottle(NumberUtils.toLong(throttleParam, params.getThrottle()));
        }

        if (StringUtils.isNotBlank(publishOnImportParam)) {
            params.setPublishOnImport(publishOnImportParam);
        }

        if (StringUtils.isNotBlank(dataUrlSizeThresholdParam)) {
            params.setDataUrlSizeThreshold(
                    NumberUtils.toLong(dataUrlSizeThresholdParam, params.getDataUrlSizeThreshold()));
        }

        if (StringUtils.isNotBlank(docbasePropNamesParam)) {
            params.setDocbasePropNames(
                    new LinkedHashSet<>(Arrays.asList(StringUtils.split(docbasePropNamesParam, ","))));
        }

        if (StringUtils.isNotBlank(documentTagsParam)) {
            params.setDocumentTags(new LinkedHashSet<>(Arrays.asList(StringUtils.split(documentTagsParam, ";"))));
        }

        if (StringUtils.isNotBlank(binaryTagsParam)) {
            params.setBinaryTags(new LinkedHashSet<>(Arrays.asList(StringUtils.split(binaryTagsParam, ";"))));
        }
    }

    /**
     * Find the attachment in {@code attachments} list by the {@code contentId}.
     * @param attachments attachment list
     * @param contentId content Id
     * @return the attachment in {@code attachments} list found by the {@code contentId}
     */
    protected Attachment getAttachmentByContentId(List<Attachment> attachments, String contentId) {
        if (attachments == null || attachments.isEmpty()) {
            return null;
        }

        for (Attachment attachment : attachments) {
            if (StringUtils.equals(contentId, attachment.getContentId())) {
                return attachment;
            }
            ContentDisposition contentDisposition = attachment.getContentDisposition();
            if (contentDisposition != null
                    && StringUtils.equals(contentId, contentDisposition.getParameter("name"))) {
                return attachment;
            }
        }

        return null;
    }

    /**
     * Apply tag field on the content node with give {@code tagInfos} list, each item of which should look like
     * "myhippoproject:tags=a,b,c".
     * @param contentNode content node
     * @param tagInfos tag info line like "myhippoproject:tags=a,b,c"
     * @return true if any tag field is added
     */
    protected boolean applyTagContentProperties(ContentNode contentNode, Set<String> tagInfos) {
        if (CollectionUtils.isEmpty(tagInfos)) {
            return false;
        }

        boolean updated = false;

        for (String tagInfo : tagInfos) {
            String name = StringUtils.substringBefore(tagInfo, "=");
            String values = StringUtils.substringAfter(tagInfo, "=");

            if (StringUtils.isBlank(name) || StringUtils.isBlank(values)) {
                log.warn("Invalid content tag info: {}", tagInfo);
                continue;
            }

            contentNode.setProperty(name, StringUtils.split(values, ","));
            updated = true;
        }

        return updated;
    }

    /**
     * Find user principal's name from {@code securityContext} or {@code request}.
     * @param securityContext security context
     * @param request servlet request
     * @return user principal's name from {@code securityContext} or {@code request}
     */
    protected String getUserPrincipalName(SecurityContext securityContext, HttpServletRequest request) {
        if (securityContext != null) {
            Principal userPrincipal = securityContext.getUserPrincipal();
            if (userPrincipal != null) {
                return userPrincipal.getName();
            }
        }

        if (request != null) {
            Principal userPrincipal = request.getUserPrincipal();
            if (userPrincipal != null) {
                return userPrincipal.getName();
            }

            final String authHeader = request.getHeader("Authorization");

            if (StringUtils.isNotBlank(authHeader)) {
                if (StringUtils.startsWithIgnoreCase(authHeader, "Basic ")) {
                    final String encoded = authHeader.substring(6).trim();
                    final String decoded = new String(Base64.getDecoder().decode(encoded));
                    return StringUtils.substringBefore(decoded, ":");
                }
            }
        }

        return null;
    }

    /**
     * Fill basic info from {@code securityContext} and {@code request} in {@code process}.
     * @param process process
     * @param securityContext security context
     * @param request servlet request
     */
    protected void fillProcessStatusByRequestInfo(ProcessStatus process, SecurityContext securityContext,
            HttpServletRequest request) {
        process.setUsername(getUserPrincipalName(securityContext, request));
        process.setClientInfo(ServletRequestUtils.getFarthestRemoteAddr(request));

        StringBuilder sbCommand = new StringBuilder(256).append(request.getMethod()).append(' ')
                .append(request.getRequestURI());
        String queryString = request.getQueryString();
        if (StringUtils.isNotBlank(queryString)) {
            sbCommand.append('?').append(queryString);
        }
        process.setCommandInfo(sbCommand.toString());
    }

    /**
     * Create a tee-ing logger.
     * @param mainLogger main logger
     * @param secondOutput output for the second logger
     * @return a tee-ing logger
     */
    protected Logger createTeeLogger(final Logger mainLogger, final PrintStream secondOutput) {
        final Logger second = new TimestampPrintStreamLogger("exim", PrintStreamLogger.INFO_LEVEL, secondOutput);
        return new TeeLoggerWrapper(mainLogger, second);
    }

    /**
     * Return true if the given {@code path} is included in the {@code param}'s binary path includes parameter.
     * @param pathMatcher AntPathMatcher instance
     * @param params Execution params
     * @param path binary path
     * @return true if the given {@code path} is included in the {@code param}'s binary path includes parameter
     */
    protected boolean isBinaryPathIncluded(final AntPathMatcher pathMatcher, final ExecutionParams params,
            final String path) {
        QueriesAndPaths queriesAndPaths = params.getBinaries();

        if (queriesAndPaths == null) {
            return true;
        }

        return isPathIncluded(pathMatcher, queriesAndPaths.getExcludes(), queriesAndPaths.getIncludes(), path);
    }

    /**
     * Return true if the given {@code path} is included in the {@code param}'s document path includes parameter.
     * @param pathMatcher AntPathMatcher instance
     * @param params Execution params
     * @param path document path
     * @return true if the given {@code path} is included in the {@code param}'s document path includes parameter
     */
    protected boolean isDocumentPathIncluded(final AntPathMatcher pathMatcher, final ExecutionParams params,
            final String path) {
        QueriesAndPaths queriesAndPaths = params.getDocuments();

        if (queriesAndPaths == null) {
            return true;
        }

        return isPathIncluded(pathMatcher, queriesAndPaths.getExcludes(), queriesAndPaths.getIncludes(), path);
    }

    private boolean isPathIncluded(final AntPathMatcher pathMatcher, final Collection<String> excludes,
            final Collection<String> includes, final String path) {
        if (CollectionUtils.isNotEmpty(excludes)) {
            for (String exclude : excludes) {
                if (pathMatcher.match(exclude, path)) {
                    return false;
                }
            }
        }

        if (CollectionUtils.isNotEmpty(includes)) {
            for (String include : includes) {
                if (pathMatcher.match(include, path)) {
                    return true;
                }
            }
            return false;
        } else {
            return true;
        }
    }

    private static class TimestampPrintStreamLogger extends PrintStreamLogger {

        private static final FastDateFormat dateFormat = FastDateFormat.getInstance("yyyy-MM-dd HH:mm:ss,SSS");

        public TimestampPrintStreamLogger(final String name, final int level, final PrintStream... out)
                throws IllegalArgumentException {
            super(name, level, out);
        }

        @Override
        protected String getMessageString(final String level, final String message) {
            final String ts = dateFormat.format(System.currentTimeMillis());
            return level + " " + ts + " " + message;
        }
    }
}