org.pentaho.reporting.platform.plugin.JobManager.java Source code

Java tutorial

Introduction

Here is the source code for org.pentaho.reporting.platform.plugin.JobManager.java

Source

/*
 * This program is free software; you can redistribute it and/or modify it under the
 * terms of the GNU General Public License, version 2 as published by the Free Software
 * Foundation.
 *
 * You should have received a copy of the GNU General Public License along with this
 * program; if not, you can obtain a copy at http://www.gnu.org/licenses/gpl-2.0.html
 * or from the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU General Public License for more details.
 *
 *
 * Copyright 2006 - 2017 Hitachi Vantara.  All rights reserved.
 */

package org.pentaho.reporting.platform.plugin;

import org.apache.commons.io.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.codehaus.jackson.annotate.JsonPropertyOrder;
import org.codehaus.jackson.map.ObjectMapper;
import org.pentaho.platform.api.engine.IPentahoSession;
import org.pentaho.platform.api.repository2.unified.IUnifiedRepository;
import org.pentaho.platform.api.repository2.unified.RepositoryFile;
import org.pentaho.platform.engine.core.system.PentahoSessionHolder;
import org.pentaho.platform.engine.core.system.PentahoSystem;
import org.pentaho.platform.util.RepositoryPathEncoder;
import org.pentaho.platform.util.StringUtil;
import org.pentaho.platform.util.web.MimeHelper;
import org.pentaho.reporting.engine.classic.core.MasterReport;
import org.pentaho.reporting.libraries.resourceloader.ResourceException;
import org.pentaho.reporting.platform.plugin.async.AsyncExecutionStatus;
import org.pentaho.reporting.platform.plugin.async.IAsyncReportState;
import org.pentaho.reporting.platform.plugin.async.IJobIdGenerator;
import org.pentaho.reporting.platform.plugin.async.IPentahoAsyncExecutor;
import org.pentaho.reporting.platform.plugin.async.ISchedulingDirectoryStrategy;
import org.pentaho.reporting.platform.plugin.staging.IFixedSizeStreamingContent;

import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.CacheControl;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.StreamingOutput;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.Future;

@Path("/reporting/api/jobs")
public class JobManager {

    private static final Log logger = LogFactory.getLog(JobManager.class);
    private static final String ASYNC_DISABLED = "JobManager initialization: async mode marked as disabled.";
    private static final String ERROR_GENERATING_REPORT = "Error generating report";
    private static final String UNABLE_TO_SERIALIZE_TO_JSON = "Unable to serialize to json : ";
    private static final String UNCKNOWN_MEDIA_TYPE = "Can't determine JAX-RS media type for: ";
    private final Config config;

    public JobManager() {
        this(true, 500, 1500, false);
    }

    public JobManager(final boolean isSupportAsync, final long pollingIntervalMilliseconds,
            final long dialogThresholdMillisecond) {
        this(isSupportAsync, pollingIntervalMilliseconds, dialogThresholdMillisecond, false);
    }

    public JobManager(final boolean isSupportAsync, final long pollingIntervalMilliseconds,
            final long dialogThresholdMillisecond, final boolean promptForLocation) {
        if (!isSupportAsync) {
            logger.info(ASYNC_DISABLED);
        }
        this.config = new Config(isSupportAsync, pollingIntervalMilliseconds, dialogThresholdMillisecond,
                promptForLocation);
    }

    @GET
    @Path("config")
    public Response getConfig() {
        return getJson(config);
    }

    @GET
    @Path("{job_id}/content")
    public Response getPDFContent(@PathParam("job_id") final String job_id) throws IOException {
        logger.debug("Chrome pdf viewer workaround. See BACKLOG-7598 for details");

        return this.getContent(job_id);
    }

    @SuppressWarnings("unchecked")
    @POST
    @Path("{job_id}/content")
    public Response getContent(@PathParam("job_id") final String jobId) throws IOException {

        try {
            final ExecutionContext context = getContext(jobId);
            final Future<IFixedSizeStreamingContent> future = context.getFuture();
            final IAsyncReportState state = context.getReportState();

            if (!AsyncExecutionStatus.FINISHED.equals(state.getStatus())) {
                return Response.status(Response.Status.ACCEPTED).build();
            }

            final IFixedSizeStreamingContent input;
            try {
                input = future.get();
            } catch (final Exception e) {
                logger.error(ERROR_GENERATING_REPORT, e);
                return Response.serverError().build();
            }

            final StreamingOutput stream = new StreamingOutputWrapper(input.getStream());

            MediaType mediaType;
            Response.ResponseBuilder response;

            try {
                mediaType = MediaType.valueOf(state.getMimeType());
            } catch (final Exception e) {
                logger.warn(UNCKNOWN_MEDIA_TYPE + state.getMimeType(), e);
                //Downloadable type
                mediaType = MediaType.APPLICATION_OCTET_STREAM_TYPE;
            }

            response = Response.ok(stream, mediaType);

            response = noCache(response);
            response = calculateContentDisposition(response, state);

            return response.build();

        } catch (final ContextFailedException | FutureNotFoundException e) {
            return get404();
        }
    }

    @GET
    @Path("{job_id}/status")
    @Produces("application/json")
    public Response getStatus(@PathParam("job_id") final String jobId) {

        try {

            final ExecutionContext context = getContext(jobId);

            final IAsyncReportState responseJson = context.getReportState();

            return getJson(responseJson);
        } catch (final ContextFailedException e) {
            return get404();
        }
    }

    private Response getJson(final Object responseJson) {
        final ObjectMapper mapper = new ObjectMapper();
        String json = null;
        try {
            json = mapper.writeValueAsString(responseJson);
        } catch (final Exception e) {
            logger.error(UNABLE_TO_SERIALIZE_TO_JSON + responseJson.toString());
            Response.serverError().build();
        }
        return Response.ok(json).build();
    }

    protected IPentahoAsyncExecutor getExecutor() {
        return PentahoSystem.get(IPentahoAsyncExecutor.class);
    }

    @SuppressWarnings("unchecked")
    @GET
    @Path("{job_id}/cancel")
    public Response cancel(@PathParam("job_id") final String jobId) {
        try {

            final ExecutionContext context = getContext(jobId);

            final Future<InputStream> future = context.getFuture();
            final IAsyncReportState state = context.getReportState();

            logger.debug("Cancellation of report: " + state.getPath() + ", requested by : " + context.getSession());

            future.cancel(true);

            return Response.ok().build();
        } catch (final ContextFailedException e) {
            return get404();
        } catch (final FutureNotFoundException e) {
            return Response.ok().build();
        }
    }

    @GET
    @Path("{job_id}/requestPage/{page}")
    @Produces("text/text")
    public Response requestPage(@PathParam("job_id") final String jobId, @PathParam("page") final int page) {
        try {

            final ExecutionContext context = getContext(jobId);

            context.requestPage(page);

            return Response.ok(String.valueOf(page)).build();
        } catch (final ContextFailedException e) {
            return get404();
        }
    }

    @GET
    @Path("{job_id}/schedule")
    @Produces("text/text")
    public Response schedule(@PathParam("job_id") final String jobId,
            @DefaultValue("true") @QueryParam("confirm") final boolean confirm) {
        try {
            ExecutionContext context = getContext(jobId);

            if (confirm) {
                if (context.needRecalculation(Boolean.FALSE)) {
                    //Get new job id
                    final UUID recalculate = context.recalculate();
                    if (null != recalculate) {
                        context = getContext(recalculate.toString());
                    }
                }
                context.schedule();
            } else {
                context.preSchedule();
            }

            return Response.ok().build();
        } catch (final ContextFailedException e) {
            return get404();
        }
    }

    @POST
    @Path("{job_id}/schedule")
    @Produces("application/json")
    public Response confirmSchedule(@PathParam("job_id") final String jobId,
            @DefaultValue("true") @QueryParam("confirm") final boolean confirm,
            @DefaultValue("false") @QueryParam("recalculateFinished") final boolean recalculateFinished,
            @QueryParam("folderId") final String folderId, @QueryParam("newName") final String newName) {
        try {

            //We can't go further without folder id and file name
            if (StringUtil.isEmpty(folderId) || StringUtil.isEmpty(newName)) {
                return get404();
            }

            ExecutionContext context = getContext(jobId);

            //The report can be already scheduled but we still may want to update the location
            if (confirm) {
                if (context.needRecalculation(recalculateFinished)) {
                    //Get new job id
                    final UUID recalculate = context.recalculate();
                    if (null != recalculate) {
                        context = getContext(recalculate.toString());
                    }
                }
                context.schedule();
            }

            //Update the location
            context.updateSchedulingLocation(folderId, newName);

            return getJson(Collections.singletonMap("uuid", context.jobId));
        } catch (final ContextFailedException e) {
            return get404();
        }
    }

    public ExecutionContext getContext(final String jobId) throws ContextFailedException {
        final ExecutionContext executionContext = new ExecutionContext(jobId);
        executionContext.evaluate();
        return executionContext;
    }

    @POST
    @Path("reserveId")
    @Produces("application/json")
    public Response reserveId() {
        final IPentahoSession session = PentahoSessionHolder.getSession();
        final IJobIdGenerator iJobIdGenerator = PentahoSystem.get(IJobIdGenerator.class);
        if (session != null && iJobIdGenerator != null) {
            final UUID reservedId = iJobIdGenerator.generateId(session);
            return getJson(Collections.singletonMap("reservedId", reservedId.toString()));
        } else {
            return get404();
        }
    }

    protected final Response get404() {
        return Response.status(Response.Status.NOT_FOUND).build();
    }

    /**
     * In-place implementation to support streaming responses. By default - even InputStream passed - streaming is not
     * occurs.
     */
    protected static final class StreamingOutputWrapper implements StreamingOutput {

        private InputStream input;

        public StreamingOutputWrapper(final InputStream readFrom) {
            this.input = readFrom;
        }

        @Override
        public void write(final OutputStream outputStream) throws IOException, WebApplicationException {
            try {
                IOUtils.copy(input, outputStream);
                outputStream.flush();
            } finally {
                IOUtils.closeQuietly(outputStream);
                IOUtils.closeQuietly(input);
            }
        }
    }

    protected static Response.ResponseBuilder noCache(final Response.ResponseBuilder response) {
        // no cache
        final CacheControl cacheControl = new CacheControl();
        cacheControl.setPrivate(true);
        cacheControl.setMaxAge(0);
        cacheControl.setMustRevalidate(true);

        response.cacheControl(cacheControl);
        return response;
    }

    protected static Response.ResponseBuilder calculateContentDisposition(final Response.ResponseBuilder response,
            final IAsyncReportState state) {
        final org.pentaho.reporting.libraries.base.util.IOUtils utils = org.pentaho.reporting.libraries.base.util.IOUtils
                .getInstance();

        final String targetExt = MimeHelper.getExtension(state.getMimeType());
        final String fullPath = state.getPath();
        final String sourceExt = utils.getFileExtension(fullPath);
        String cleanFileName = utils.stripFileExtension(utils.getFileName(fullPath));
        if (StringUtil.isEmpty(cleanFileName)) {
            cleanFileName = "content";
        }

        final String disposition = "inline; filename*=UTF-8''" + RepositoryPathEncoder
                .encode(RepositoryPathEncoder.encodeRepositoryPath(cleanFileName + targetExt));
        response.header("Content-Disposition", disposition);

        response.header("Content-Description", cleanFileName + sourceExt);

        return response;
    }

    @JsonPropertyOrder(alphabetic = true) //stable response structure
    private class Config {
        private final boolean isSupportAsync;
        private final long pollingIntervalMilliseconds;
        private final long dialogThresholdMilliseconds;
        private final boolean promptForLocation;

        private Config(final boolean isSupportAsync, final long pollingIntervalMilliseconds,
                final long dialogThresholdMilliseconds, final boolean promptForLocation) {
            this.isSupportAsync = isSupportAsync;
            this.pollingIntervalMilliseconds = pollingIntervalMilliseconds;
            this.dialogThresholdMilliseconds = dialogThresholdMilliseconds;
            this.promptForLocation = promptForLocation;
        }

        public boolean isSupportAsync() {
            return isSupportAsync;
        }

        public long getPollingIntervalMilliseconds() {
            return pollingIntervalMilliseconds;
        }

        public long getDialogThresholdMilliseconds() {
            return dialogThresholdMilliseconds;
        }

        public boolean isPromptForLocation() {
            return promptForLocation;
        }

        //Location can be changed at any time depending on ISchedulingDirectoryStrategy implementation
        public String getDefaultOutputPath() {
            return getLocation();
        }
    }

    private String getLocation() {
        final ISchedulingDirectoryStrategy directoryStrategy = PentahoSystem
                .get(ISchedulingDirectoryStrategy.class);
        final IUnifiedRepository repository = PentahoSystem.get(IUnifiedRepository.class);
        if (directoryStrategy != null && repository != null) {
            //We may consider caching in strategy implementation
            final RepositoryFile outputFolder = directoryStrategy.getSchedulingDir(repository);
            return outputFolder.getPath();
        }
        return "/";
    }

    /**
     * Used to get context for operation execution and validate it
     */
    public class ExecutionContext {
        private IPentahoSession session;
        private final String jobId;
        private UUID uuid = null;

        private ExecutionContext(final String jobId) {
            this.jobId = jobId;
        }

        private void evaluate() throws ContextFailedException {
            try {
                this.session = PentahoSessionHolder.getSession();
                this.uuid = UUID.fromString(jobId);
            } catch (final Exception e) {
                logger.error(e);
                throw new ContextFailedException(e);
            }
        }

        public IPentahoSession getSession() {
            return session;
        }

        //Be sure to get it from context each time to make it work for PIR too
        private IPentahoAsyncExecutor getReportExecutor() {
            return getExecutor();
        }

        public Future getFuture() throws FutureNotFoundException {
            final Future future = getReportExecutor().getFuture(uuid, session);
            if (future == null) {
                throw new FutureNotFoundException("Can't get future");
            }
            return future;
        }

        public IAsyncReportState getReportState() throws ContextFailedException {
            final IAsyncReportState reportState = getReportExecutor().getReportState(uuid, session);
            if (reportState == null) {
                throw new ContextFailedException("Can't get state");
            }
            return reportState;
        }

        public void requestPage(final int page) throws ContextFailedException {
            //Check if there is a task
            getReportState();
            getReportExecutor().requestPage(uuid, session, page);
        }

        public void schedule() throws ContextFailedException {
            //Check if there is a task
            final IAsyncReportState reportState = getReportState();
            if (reportState.getStatus().equals(AsyncExecutionStatus.SCHEDULED)) {
                throw new ContextFailedException("Report is already scheduled.");
            }
            getReportExecutor().schedule(uuid, session);
        }

        public void updateSchedulingLocation(final String folderId, final String newName)
                throws ContextFailedException {
            if (!config.isPromptForLocation()) {
                throw new ContextFailedException("Location update is disabled");
            }
            //Check if there is a task
            final IAsyncReportState reportState = getReportState();
            if (reportState.getStatus().equals(AsyncExecutionStatus.SCHEDULED)) {
                getReportExecutor().updateSchedulingLocation(uuid, session, folderId, newName);
            } else {
                throw new ContextFailedException("Can't update the location of not scheduled report.");
            }
        }

        public void preSchedule() throws ContextFailedException {
            //Check if there is a task
            getReportState();
            getReportExecutor().preSchedule(uuid, session);
        }

        public UUID recalculate() throws ContextFailedException {
            //Check if there is a task
            getReportState();
            return getReportExecutor().recalculate(uuid, session);
        }

        public boolean needRecalculation(final boolean recalculateFinished) throws ContextFailedException {
            return (AsyncExecutionStatus.FINISHED.equals(getReportState().getStatus()) && recalculateFinished)
                    || isRowLimitRecalculationNeeded();
        }

        private boolean isRowLimitRecalculationNeeded() throws ContextFailedException {
            try {
                final IAsyncReportState state = this.getReportState();
                final String path = state.getPath();
                final MasterReport report = ReportCreator.createReportByName(path);
                final int queryLimit = report.getQueryLimit();
                if (queryLimit > 0) {
                    return Boolean.TRUE;
                } else {
                    if (state.getIsQueryLimitReached()) {
                        return Boolean.TRUE;
                    }
                }
                return Boolean.FALSE;
            } catch (ResourceException | IOException e) {
                return Boolean.FALSE;
            }
        }
    }

    public static class ContextFailedException extends Exception {

        public ContextFailedException(final String message) {
            super(message);
        }

        ContextFailedException(final Throwable cause) {
            super(cause);
        }
    }

    private static class FutureNotFoundException extends Exception {
        public FutureNotFoundException(final String message) {
            super(message);
        }
    }

}