001/*
002 * (C) Copyright 2019 (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Florent Guillaume
018 */
019package org.nuxeo.ecm.core.security;
020
021import static org.nuxeo.ecm.core.bulk.BulkServiceImpl.STATUS_STREAM;
022import static org.nuxeo.lib.stream.computation.AbstractComputation.INPUT_1;
023import static org.nuxeo.lib.stream.computation.AbstractComputation.OUTPUT_1;
024
025import java.io.Serializable;
026import java.util.Arrays;
027import java.util.Calendar;
028import java.util.Collection;
029import java.util.List;
030import java.util.Map;
031import java.util.stream.Collectors;
032
033import org.apache.logging.log4j.LogManager;
034import org.apache.logging.log4j.Logger;
035import org.nuxeo.ecm.core.api.CoreSession;
036import org.nuxeo.ecm.core.api.DocumentModel;
037import org.nuxeo.ecm.core.api.DocumentRef;
038import org.nuxeo.ecm.core.api.IdRef;
039import org.nuxeo.ecm.core.api.NuxeoException;
040import org.nuxeo.ecm.core.api.PropertyException;
041import org.nuxeo.ecm.core.api.event.CoreEventConstants;
042import org.nuxeo.ecm.core.api.event.DocumentEventCategories;
043import org.nuxeo.ecm.core.api.event.DocumentEventTypes;
044import org.nuxeo.ecm.core.bulk.action.computation.AbstractBulkComputation;
045import org.nuxeo.ecm.core.event.Event;
046import org.nuxeo.ecm.core.event.EventService;
047import org.nuxeo.ecm.core.event.impl.DocumentEventContext;
048import org.nuxeo.lib.stream.computation.Topology;
049import org.nuxeo.runtime.api.Framework;
050import org.nuxeo.runtime.stream.StreamProcessorTopology;
051
052/**
053 * Bulk action executed for documents whose retention has expired.
054 *
055 * @since 11.1
056 */
057public class RetentionExpiredAction implements StreamProcessorTopology {
058
059    public static final String ACTION_NAME = "retentionExpired";
060
061    public static final String ACTION_FULL_NAME = "retention/" + ACTION_NAME;
062
063    @Override
064    public Topology getTopology(Map<String, String> options) {
065        return Topology.builder()
066                       .addComputation(RetentionExpiredComputation::new, //
067                               Arrays.asList(INPUT_1 + ":" + ACTION_FULL_NAME, OUTPUT_1 + ":" + STATUS_STREAM))
068                       .build();
069    }
070
071    public static class RetentionExpiredComputation extends AbstractBulkComputation {
072
073        private static final Logger log = LogManager.getLogger(RetentionExpiredComputation.class);
074
075        public RetentionExpiredComputation() {
076            super(ACTION_FULL_NAME);
077        }
078
079        @Override
080        protected void compute(CoreSession session, List<String> ids, Map<String, Serializable> properties) {
081            Collection<DocumentRef> refs = ids.stream().map(IdRef::new).collect(Collectors.toList());
082            for (DocumentRef ref : refs) {
083                // sanity checks
084                if (!session.isRecord(ref)) {
085                    log.debug("Document: {} is not a record", ref);
086                    continue;
087                }
088                Calendar retainUntil = session.getRetainUntil(ref);
089                if (retainUntil == null) {
090                    log.debug("Document: {} was not under retention", ref);
091                    continue;
092                }
093                if (isUnderRetention(retainUntil)) {
094                    log.debug("Document: {} is still under retention: {}", () -> ref, retainUntil::toInstant);
095                    continue;
096                }
097
098                try {
099                    // reset retention to null
100                    session.setRetainUntil(ref, null, null);
101                    // send event about retention expired
102                    sendEvent(session, ref, retainUntil);
103                } catch (NuxeoException e) {
104                    // TODO send to error stream
105                    log.warn("Cannot reset retention on: {}", ref, e);
106                }
107
108            }
109            try {
110                session.save();
111            } catch (PropertyException e) {
112                // TODO send to error stream
113                log.warn("Cannot save session", e);
114            }
115        }
116
117        protected static boolean isUnderRetention(Calendar retainUntil) {
118            return retainUntil != null && Calendar.getInstance().before(retainUntil);
119        }
120
121        protected void sendEvent(CoreSession session, DocumentRef ref, Calendar retainUntil) {
122            DocumentModel doc = session.getDocument(ref);
123            DocumentEventContext ctx = new DocumentEventContext(session, null, doc);
124            ctx.setProperty(CoreEventConstants.REPOSITORY_NAME, session.getRepositoryName());
125            ctx.setProperty(DocumentEventContext.CATEGORY_PROPERTY_KEY, DocumentEventCategories.EVENT_DOCUMENT_CATEGORY);
126            ctx.setProperty(CoreEventConstants.RETAIN_UNTIL, retainUntil);
127            ctx.setProperty(DocumentEventContext.COMMENT_PROPERTY_KEY, retainUntil.toInstant().toString());
128            Event event = ctx.newEvent(DocumentEventTypes.RETENTION_EXPIRED);
129            Framework.getService(EventService.class).fireEvent(event);
130        }
131    }
132
133}