All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.composum.sling.core.servlet.JobControlServlet Maven / Gradle / Ivy

package com.composum.sling.core.servlet;

import com.composum.sling.core.CoreConfiguration;
import com.composum.sling.core.ResourceHandle;
import com.composum.sling.core.Restricted;
import com.composum.sling.core.concurrent.JobFacade;
import com.composum.sling.core.concurrent.JobUtil;
import com.composum.sling.core.service.RestrictedService;
import com.composum.sling.core.util.RequestUtil;
import com.composum.sling.core.util.ResourceUtil;
import com.composum.sling.core.util.ResponseUtil;
import com.composum.sling.core.util.XSS;
import com.google.gson.stream.JsonWriter;
import org.apache.commons.lang3.StringUtils;
import org.apache.jackrabbit.api.JackrabbitSession;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.request.RequestParameter;
import org.apache.sling.api.resource.PersistenceException;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.servlets.HttpConstants;
import org.apache.sling.api.servlets.ServletResolverConstants;
import org.apache.sling.event.jobs.Job;
import org.apache.sling.event.jobs.JobManager;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.osgi.framework.Constants;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static com.composum.sling.core.servlet.JobControlServlet.SERVICE_KEY;

@Component(service = {Servlet.class, RestrictedService.class},
        property = {
                Constants.SERVICE_DESCRIPTION + "=Composum Nodes Job Control Servlet",
                ServletResolverConstants.SLING_SERVLET_PATHS + "=" + JobControlServlet.SERVLET_PATH,
                ServletResolverConstants.SLING_SERVLET_METHODS + "=" + HttpConstants.METHOD_GET,
                ServletResolverConstants.SLING_SERVLET_METHODS + "=" + HttpConstants.METHOD_PUT,
                ServletResolverConstants.SLING_SERVLET_METHODS + "=" + HttpConstants.METHOD_POST,
                ServletResolverConstants.SLING_SERVLET_METHODS + "=" + HttpConstants.METHOD_DELETE,
                "sling.auth.requirements=" + JobControlServlet.SERVLET_PATH
        }
)
@Restricted(key = SERVICE_KEY)
public class JobControlServlet extends AbstractServiceServlet {

    private static final Logger LOG = LoggerFactory.getLogger(JobControlServlet.class);

    public static final String SERVICE_KEY = "core/job/execution";

    public static final String SERVLET_PATH = "/bin/cpm/core/jobcontrol";

    public enum Extension {txt, json}

    public enum Operation {job, jobs, outfile, cleanup}

    protected ServletOperationSet operations = new ServletOperationSet<>(Extension.json);

    @Reference
    private CoreConfiguration coreConfig;

    @Reference
    private JobManager jobManager;

    @Override
    @NotNull
    protected ServletOperationSet getOperations() {
        return operations;
    }

    @Override
    public void init() throws ServletException {
        super.init();

        // GET
        // curl -X GET http://localhost:9090/bin/cpm/core/jobcontrol.jobs.HISTORY.json/?topic=com/composum/sling/core/script/GroovyJobExecutor
        // [ALL, ACTIVE, QUEUED, HISTORY, CANCELLED, SUCCEEDED, STOPPED, GIVEN_UP, ERROR, DROPPED]
        operations.setOperation(ServletOperationSet.Method.GET, Extension.json, Operation.jobs, new GetAllJobs());

        // curl http://localhost:9090/bin/cpm/core/jobcontrol.job.json/2016/4/11/13/48/3d51ae17-ce12-4fa3-a87a-5dbfdd739093_0
        operations.setOperation(ServletOperationSet.Method.GET, Extension.json, Operation.job, new GetJob());

        // curl -r 100-125 -X GET http://localhost:9090/bin/cpm/core/jobcontrol.outfile.txt/2016/4/8/15/21/3d51ae17-ce12-4fa3-a87a-5dbfdd739093_81
        operations.setOperation(ServletOperationSet.Method.GET, Extension.txt, Operation.outfile, new GetOutfile());

        // POST
        // curl -v -Fevent.job.topic=com/composum/sling/core/script/GroovyJobExecutor -Freference=/hello.groovy -Foutfileprefix=groovyjob -X POST http://localhost:9090/bin/cpm/core/jobcontrol.job.json
        operations.setOperation(ServletOperationSet.Method.POST, Extension.json, Operation.job, new CreateJob());
        // curl -v -u admin:admin -Fevent.job.topic=com/composum/sling/core/script/GroovyJobExecutor -Freference=/libs/hello.groovy -Fkeep=2 -X POST http://localhost:9090/bin/cpm/core/jobcontrol.cleanup.json
        // curl -v -u admin:admin -Fevent.job.topic=com/composum/sling/core/script/GroovyJobExecutor -Fkeep=2 -X POST http://localhost:9090/bin/cpm/core/jobcontrol.cleanup.json
        operations.setOperation(ServletOperationSet.Method.POST, Extension.json, Operation.cleanup, new PurgeAudit());

        // DELETE
        // curl -v -X DELETE http://localhost:9090/bin/cpm/core/jobcontrol.job.json/2016/4/8/15/21/3d51ae17-ce12-4fa3-a87a-5dbfdd739093_81
        operations.setOperation(ServletOperationSet.Method.DELETE, Extension.json, Operation.job, new CancelJob());
        // curl -u admin:admin -X DELETE http://localhost:9090/bin/cpm/core/jobcontrol.cleanup.json/2016/4/12/9/50/3d51ae17-ce12-4fa3-a87a-5dbfdd739093_0
        operations.setOperation(ServletOperationSet.Method.DELETE, Extension.json, Operation.cleanup, new CleanupJob());

    }

    /**
     * Gets a part of the named temp. outputfile.
     */
    private class GetOutfile implements ServletOperation {

        @Override
        public void doIt(@NotNull final SlingHttpServletRequest request,
                         @NotNull final SlingHttpServletResponse response,
                         @Nullable final ResourceHandle resource) throws IOException {
            final String jobId = AbstractServiceServlet.getPath(request).substring(1);
            final JobFacade job = JobUtil.getJobById(jobManager, request.getResourceResolver(), jobId);
            if (job != null) {
                final String path = job.getProperty("outfile", String.class);
                final String range = request.getHeader("Range");
                final List ranges = decodeRange(range);
                final File file = new File(path);
                response.setCharacterEncoding("UTF-8");
                response.setContentType("text/plain;charset=utf-8");
                if (file.exists()) {
                    try (final ServletOutputStream outputStream = response.getOutputStream();
                         final InputStream inputStream = new FileInputStream(file)) {
                        writeStream(ranges, outputStream, inputStream);
                    } catch (FileNotFoundException e) {
                        response.sendError(HttpServletResponse.SC_NOT_FOUND, path);
                    }
                } else {
                    final ResourceResolver resolver = request.getResourceResolver();
                    final Iterator resources = resolver.findResources("/jcr:root/var/audit/jobs//*[outfile='" + path + "']", "xpath");
                    if (resources.hasNext()) {
                        final Resource audit = resources.next();
                        final Resource outfileResource = resolver.getResource(audit, path.substring(path.lastIndexOf(File.separator) + 1));
                        if (outfileResource != null) {
                            try (final ServletOutputStream outputStream = response.getOutputStream();
                                 final InputStream inputStream = outfileResource.adaptTo(InputStream.class)) {
                                writeStream(ranges, outputStream, inputStream);
                            }
                        }
                    }
                }
            } else {
                response.sendError(HttpServletResponse.SC_NOT_FOUND);
            }
        }

        private void writeStream(List ranges, ServletOutputStream outputStream, InputStream inputStream) throws IOException {
            long end = Long.MAX_VALUE;
            long pos = 0L;
            if (!ranges.isEmpty()) {
                final Range range1 = ranges.get(0);
                if (range1.start != null) {
                    pos = inputStream.skip(range1.start);
                }
                if (range1.end != null) {
                    end = range1.end;
                }
            }
            int read;
            while ((read = inputStream.read()) >= 0 && !(pos > end)) {
                outputStream.write(read);
                pos++;
            }
        }

        class Range {
            Integer start;
            Integer end;
            Integer suffixLength;
        }

        private List decodeRange(String rangeHeader) {
            List ranges = new ArrayList<>();
            if (StringUtils.isEmpty(rangeHeader)) {
                ranges.add(new Range());
                return ranges;
            }
            String byteRangeSetRegex = "(((?(?\\d+)-(?\\d+)?)|(?-(?\\d+)))(,|$))";
            String byteRangesSpecifierRegex = "bytes=(?" + byteRangeSetRegex + "+)";
            Pattern byteRangeSetPattern = Pattern.compile(byteRangeSetRegex);
            Pattern byteRangesSpecifierPattern = Pattern.compile(byteRangesSpecifierRegex);
            Matcher byteRangesSpecifierMatcher = byteRangesSpecifierPattern.matcher(rangeHeader);
            if (byteRangesSpecifierMatcher.matches()) {
                String byteRangeSet = byteRangesSpecifierMatcher.group("byteRangeSet");
                Matcher byteRangeSetMatcher = byteRangeSetPattern.matcher(byteRangeSet);
                while (byteRangeSetMatcher.find()) {
                    Range range = new Range();
                    if (byteRangeSetMatcher.group("byteRangeSpec") != null) {
                        String start = byteRangeSetMatcher.group("firstBytePos");
                        String end = byteRangeSetMatcher.group("lastBytePos");
                        range.start = Integer.valueOf(start);
                        range.end = end == null ? null : Integer.valueOf(end);
                    } else if (byteRangeSetMatcher.group("suffixByteRangeSpec") != null) {
                        range.suffixLength = Integer.valueOf(byteRangeSetMatcher.group("suffixLength"));
                    } else {
                        return Collections.emptyList();
                    }
                    ranges.add(range);
                }
            } else {
                return Collections.emptyList();
            }
            return ranges;
        }
    }

    /**
     * Gets a list of all jobs matching the state given in the extra selector.
     */
    private class GetAllJobs implements ServletOperation {

        @Override
        public void doIt(@NotNull final SlingHttpServletRequest request,
                         @NotNull final SlingHttpServletResponse response,
                         @Nullable final ResourceHandle resource) throws IOException {
            final String path = AbstractServiceServlet.getPath(request);
            final RequestParameter topic = request.getRequestParameter("topic");
            final List allJobs = new ArrayList<>();
            if (topic != null) {
                final JobManager.QueryType selector = RequestUtil.getSelector(request, JobManager.QueryType.ALL);
                boolean useAudit = (selector == JobManager.QueryType.ALL || selector == JobManager.QueryType.HISTORY || selector == JobManager.QueryType.SUCCEEDED);
                final Collection jobs = jobManager.findJobs(selector, topic.getString(), 0);
                for (Job job : jobs) {
                    allJobs.add(new JobFacade.EventJob(job));
                }
                if (useAudit) {
                    final Collection auditJobs = JobUtil.getAuditJobs(selector, request.getResourceResolver());
                    for (JobFacade auditJob : auditJobs) {
                        if (!containsJob(jobs, auditJob)) {
                            allJobs.add(auditJob);
                        }
                    }
                }
            }
            try (final JsonWriter jsonWriter = ResponseUtil.getJsonWriter(response)) {
                allJobs.sort(Comparator.comparing(JobFacade::getCreated));
                jsonWriter.beginArray();
                for (JobFacade job : allJobs) {
                    if (path.length() > 1) {
                        final String script = job.getProperty("reference", String.class);
                        if (script != null && script.equals(path)) {
                            job2json(jsonWriter, job);
                        }
                    } else {
                        job2json(jsonWriter, job);
                    }
                }
                jsonWriter.endArray();
            }
        }

        private boolean containsJob(Collection jobs, JobFacade jobToFind) {
            for (Job job : jobs) {
                if (job.getId().equals(jobToFind.getId())) {
                    return true;
                }
            }
            return false;
        }
    }

    /**
     * Retrieves a singe job.
     */
    private class GetJob implements ServletOperation {

        @Override
        public void doIt(@NotNull final SlingHttpServletRequest request,
                         @NotNull final SlingHttpServletResponse response,
                         @Nullable final ResourceHandle resource) throws IOException {
            final String path = AbstractServiceServlet.getPath(request);
            String jobId = path.substring(1);
            JobFacade job = JobUtil.getJobById(jobManager, request.getResourceResolver(), jobId);
            if (job != null) {
                try (final JsonWriter jsonWriter = ResponseUtil.getJsonWriter(response)) {
                    job2json(jsonWriter, job);
                }
            }
        }
    }

    /**
     * Cancel a single job by its id.
     */
    private class CancelJob implements ServletOperation {

        @Override
        public void doIt(@NotNull final SlingHttpServletRequest request,
                         @NotNull final SlingHttpServletResponse response,
                         @Nullable final ResourceHandle resource) {
            final String path = AbstractServiceServlet.getPath(request);
            String jobId = path.substring(1);
            jobManager.stopJobById(jobId);
        }
    }

    private class PurgeAudit implements ServletOperation {

        @SuppressWarnings("ConstantConditions")
        @Override
        public void doIt(@NotNull final SlingHttpServletRequest request,
                         @NotNull final SlingHttpServletResponse response,
                         @Nullable final ResourceHandle resource)
                throws RepositoryException, IOException, ServletException {
            try {
                final ResourceResolver resolver = request.getResourceResolver();
                final int keep = Integer.parseInt(request.getRequestParameter("keep").getString());
                final RequestParameter reference = request.getRequestParameter("reference");
                final RequestParameter topic = request.getRequestParameter("event.job.topic");
                if (reference != null) {
                    final String referenceString = reference.getString();
                    final String query = "/jcr:root/var/audit/jobs/" + topic.getString().replaceAll("/", ".") + referenceString + "/*[@slingevent:eventId]";
                    final Iterator auditResources = resolver.findResources(query, "xpath");
                    removeAudits(resolver, keep, auditResources);
                } else {
                    final String allAuditsQuery = "/jcr:root/var/audit/jobs/" + topic.getString().replaceAll("/", ".") + "//*[@slingevent:eventId]";
                    final Iterator allAuditResources = resolver.findResources(allAuditsQuery, "xpath");
                    final Set referencePaths = new HashSet<>();
                    while (allAuditResources.hasNext()) {
                        final Resource auditResource = allAuditResources.next();
                        final String referencePath = auditResource.getParent().getPath();
                        referencePaths.add(referencePath);
                    }
                    for (String path : referencePaths) {
                        final Resource referenceResource = resolver.getResource(path);
                        final Iterable auditResources = referenceResource.getChildren();
                        removeAudits(resolver, keep, auditResources.iterator());
                    }

                }
            } catch (Exception ex) {
                LOG.error(ex.getMessage(), ex);
                response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());
            }
        }

        private void removeAudits(ResourceResolver resolver, int keep, Iterator auditResources) throws PersistenceException {
            final List allAuditJobs = new ArrayList<>();
            while (auditResources.hasNext()) {
                final Resource auditResource = auditResources.next();
                final JobFacade.AuditJob auditJob = new JobFacade.AuditJob(auditResource);
                allAuditJobs.add(auditJob);
            }
            final Comparator comparator = new JobUtil.JobComparator();
            allAuditJobs.sort(comparator);
            int size = allAuditJobs.size();
            for (int i = 0; i < size - keep; i++) {
                final JobFacade.AuditJob x = allAuditJobs.get(i);
                resolver.delete(x.resource);
            }
            resolver.commit();
        }
    }

    /**
     * Cleans up audit and tempfile.
     */
    private class CleanupJob implements ServletOperation {

        @Override
        public void doIt(@NotNull final SlingHttpServletRequest request,
                         @NotNull final SlingHttpServletResponse response,
                         @Nullable final ResourceHandle resource)
                throws RepositoryException, IOException, ServletException {
            try {
                final String path = AbstractServiceServlet.getPath(request);
                final String jobId = path.substring(1);
                final Job job = jobManager.getJobById(jobId);
                final String outfile = job.getProperty("outfile", String.class);
                final String topic = job.getProperty("event.job.topic", String.class);
                final ResourceResolver resolver = request.getResourceResolver();
                final Iterator resources = resolver.findResources("/jcr:root/var/audit/jobs/" + topic + "//*[slingevent:eventId='" + jobId + "']", "xpath");
                boolean auditResourceDeleted = false;
                if (resources.hasNext()) {
                    final Resource audit = resources.next();
                    if (audit != null && !ResourceUtil.isNonExistingResource(audit)) {
                        resolver.delete(audit);
                        resolver.commit();
                        auditResourceDeleted = true;
                    }
                }
                final boolean b = new File(outfile).delete();
                try (final JsonWriter jsonWriter = ResponseUtil.getJsonWriter(response)) {
                    jsonWriter
                            .beginObject()
                            .name("audit").value(auditResourceDeleted)
                            .name("outfile").value(b)
                            .endObject();
                }
            } catch (final Exception ex) {
                LOG.error(ex.getMessage(), ex);
                response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());
            }
        }
    }

    /**
     * Creates a new job.
     * 

* used parameters: *

    *
  • outfileprefix
  • *
  • event.job.topic
  • *
  • reference
  • *
*/ private class CreateJob implements ServletOperation { @Override public void doIt(@NotNull final SlingHttpServletRequest request, @NotNull final SlingHttpServletResponse response, @Nullable final ResourceHandle resource) throws IOException { final ResourceResolver resolver = request.getResourceResolver(); final JackrabbitSession session = (JackrabbitSession) resolver.adaptTo(Session.class); String topic = ""; Map properties = new HashMap<>(); Map parameters = request.getParameterMap(); for (Map.Entry parameter : parameters.entrySet()) { if (parameter.getKey().equals("event.job.topic")) { topic = XSS.filter(parameter.getValue()[0]); } else { String[] value = XSS.filter(parameter.getValue()); if (value.length == 1) { properties.put(parameter.getKey(), value[0]); } else { properties.put(parameter.getKey(), value); } } } if (session != null) { properties.put("userid", session.getUserID()); } JobUtil.buildOutfileName(properties); Job job = jobManager.addJob(topic, properties); try (final JsonWriter jsonWriter = ResponseUtil.getJsonWriter(response)) { job2json(jsonWriter, new JobFacade.EventJob(job)); } } } private void job2json(JsonWriter jsonWriter, JobFacade job) throws IOException { jsonWriter.beginObject(); Set propertyNames = Collections.unmodifiableSet(job.getPropertyNames()); SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); for (String propertyName : propertyNames) { Object property = job.getProperty(propertyName); if (property instanceof Calendar) { dateFormat.setTimeZone(((Calendar) property).getTimeZone()); jsonWriter.name(propertyName).value(dateFormat.format(((Calendar) property).getTime())); } else if (property instanceof Boolean) { jsonWriter.name(propertyName).value((Boolean) property); } else if (property instanceof Long) { jsonWriter.name(propertyName).value((Long) property); } else if (property instanceof Number) { jsonWriter.name(propertyName).value((Number) property); } else if (property instanceof String) { final String s = (String) property; if (propertyName.equals("outfile")) { jsonWriter.name(propertyName).value(s.substring(s.lastIndexOf(File.separator) + 1)); } else { jsonWriter.name(propertyName).value(s); } } else if (property instanceof Object[]) { jsonWriter.name(propertyName); jsonWriter.beginArray(); for (Object o : (Object[]) property) { jsonWriter.value(String.valueOf(o)); } jsonWriter.endArray(); } else { jsonWriter.name(propertyName).value(String.valueOf(property)); } } jsonWriter.name("jobState").value(job.getJobState().name()); jsonWriter.endObject(); } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy