001/*
002 * (C) Copyright 2014-2016 Nuxeo SA (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 *     Thierry Delprat
018 *     Benoit Delbosc
019 */
020package org.nuxeo.elasticsearch.commands;
021
022import static org.nuxeo.ecm.core.api.event.DocumentEventTypes.ABOUT_TO_CHECKIN;
023import static org.nuxeo.ecm.core.api.event.DocumentEventTypes.AFTER_EXTEND_RETENTION;
024import static org.nuxeo.ecm.core.api.event.DocumentEventTypes.AFTER_MAKE_RECORD;
025import static org.nuxeo.ecm.core.api.event.DocumentEventTypes.AFTER_REMOVE_LEGAL_HOLD;
026import static org.nuxeo.ecm.core.api.event.DocumentEventTypes.AFTER_SET_LEGAL_HOLD;
027import static org.nuxeo.ecm.core.api.event.DocumentEventTypes.AFTER_SET_RETENTION;
028import static org.nuxeo.ecm.core.api.event.DocumentEventTypes.BEFORE_DOC_UPDATE;
029import static org.nuxeo.ecm.core.api.event.DocumentEventTypes.BINARYTEXT_UPDATED;
030import static org.nuxeo.ecm.core.api.event.DocumentEventTypes.DOCUMENT_CHECKEDIN;
031import static org.nuxeo.ecm.core.api.event.DocumentEventTypes.DOCUMENT_CHECKEDOUT;
032import static org.nuxeo.ecm.core.api.event.DocumentEventTypes.DOCUMENT_CHILDREN_ORDER_CHANGED;
033import static org.nuxeo.ecm.core.api.event.DocumentEventTypes.DOCUMENT_CREATED;
034import static org.nuxeo.ecm.core.api.event.DocumentEventTypes.DOCUMENT_CREATED_BY_COPY;
035import static org.nuxeo.ecm.core.api.event.DocumentEventTypes.DOCUMENT_IMPORTED;
036import static org.nuxeo.ecm.core.api.event.DocumentEventTypes.DOCUMENT_MOVED;
037import static org.nuxeo.ecm.core.api.event.DocumentEventTypes.DOCUMENT_PROXY_UPDATED;
038import static org.nuxeo.ecm.core.api.event.DocumentEventTypes.DOCUMENT_REMOVED;
039import static org.nuxeo.ecm.core.api.event.DocumentEventTypes.DOCUMENT_RESTORED;
040import static org.nuxeo.ecm.core.api.event.DocumentEventTypes.DOCUMENT_SECURITY_UPDATED;
041import static org.nuxeo.ecm.core.api.event.DocumentEventTypes.DOCUMENT_TAG_UPDATED;
042import static org.nuxeo.ecm.core.api.trash.TrashService.DOCUMENT_TRASHED;
043import static org.nuxeo.ecm.core.api.trash.TrashService.DOCUMENT_UNTRASHED;
044
045import java.util.Map;
046
047import org.apache.commons.logging.Log;
048import org.apache.commons.logging.LogFactory;
049import org.nuxeo.ecm.core.api.AbstractSession;
050import org.nuxeo.ecm.core.api.DocumentModel;
051import org.nuxeo.ecm.core.api.DocumentModelList;
052import org.nuxeo.ecm.core.api.IdRef;
053import org.nuxeo.ecm.core.api.LifeCycleConstants;
054import org.nuxeo.ecm.core.event.impl.DocumentEventContext;
055import org.nuxeo.elasticsearch.ElasticSearchConstants;
056import org.nuxeo.elasticsearch.commands.IndexingCommand.Type;
057import org.nuxeo.runtime.api.Framework;
058
059/**
060 * Contains logic to stack ElasticSearch commands depending on Document events This class is mainly here to make testing
061 * easier
062 */
063public abstract class IndexingCommandsStacker {
064
065    protected static final Log log = LogFactory.getLog(IndexingCommandsStacker.class);
066
067    protected abstract Map<String, IndexingCommands> getAllCommands();
068
069    protected abstract boolean isSyncIndexingByDefault();
070
071    protected IndexingCommands getCommands(DocumentModel doc) {
072        return getAllCommands().get(getDocKey(doc));
073    }
074
075    public void stackCommand(DocumentEventContext docCtx, String eventId) {
076        DocumentModel doc = docCtx.getSourceDocument();
077        if (doc == null) {
078            return;
079        }
080        if ("/".equals(doc.getPathAsString())) {
081            if (log.isDebugEnabled()) {
082                log.debug("Skip indexing command for root document");
083            }
084            if (eventId.equals(DOCUMENT_SECURITY_UPDATED)) {
085                if (log.isDebugEnabled()) {
086                    log.debug("Indexing root document children to update their permissions");
087                }
088                DocumentModelList children = doc.getCoreSession().getChildren(doc.getRef());
089                children.forEach(child -> stackCommand(child, docCtx, eventId));
090            }
091        } else {
092            stackCommand(doc, docCtx, eventId);
093        }
094    }
095
096    protected void stackCommand(DocumentModel doc, DocumentEventContext docCtx, String eventId) {
097        Boolean block = (Boolean) docCtx.getProperty(ElasticSearchConstants.DISABLE_AUTO_INDEXING);
098        if (block != null && block) {
099            if (log.isDebugEnabled()) {
100                log.debug("Indexing is disable, skip indexing command for doc " + doc);
101            }
102            return;
103        }
104        boolean sync = isSynchronous(docCtx, doc);
105        stackCommand(doc, eventId, sync);
106    }
107
108    protected boolean isSynchronous(DocumentEventContext docCtx, DocumentModel doc) {
109        // 1. look at event context
110        Boolean sync = (Boolean) docCtx.getProperty(ElasticSearchConstants.ES_SYNC_INDEXING_FLAG);
111        if (sync != null) {
112            return sync;
113        }
114        // 2. look at document context
115        sync = (Boolean) doc.getContextData(ElasticSearchConstants.ES_SYNC_INDEXING_FLAG);
116        if (sync != null) {
117            return sync;
118        }
119        // 3. get the default
120        sync = isSyncIndexingByDefault();
121        return sync;
122    }
123
124    protected void stackCommand(DocumentModel doc, String eventId, boolean sync) {
125        IndexingCommands cmds = getOrCreateCommands(doc);
126        Type type;
127        boolean recurse = false;
128        switch (eventId) {
129        case DOCUMENT_CREATED:
130        case DOCUMENT_IMPORTED:
131            type = Type.INSERT;
132            break;
133        case DOCUMENT_CREATED_BY_COPY:
134            type = Type.INSERT;
135            recurse = isFolderish(doc);
136            break;
137        case BEFORE_DOC_UPDATE:
138        case DOCUMENT_CHECKEDIN:
139        case DOCUMENT_CHECKEDOUT:
140        case BINARYTEXT_UPDATED:
141        case DOCUMENT_TAG_UPDATED:
142        case DOCUMENT_PROXY_UPDATED:
143        case LifeCycleConstants.TRANSITION_EVENT:
144        case DOCUMENT_TRASHED:
145        case DOCUMENT_UNTRASHED:
146        case DOCUMENT_RESTORED:
147        case AFTER_MAKE_RECORD:
148        case AFTER_SET_RETENTION:
149        case AFTER_EXTEND_RETENTION:
150        case AFTER_SET_LEGAL_HOLD:
151        case AFTER_REMOVE_LEGAL_HOLD:
152            if (doc.isProxy() && !doc.isImmutable()) {
153                stackCommand(doc.getCoreSession().getDocument(new IdRef(doc.getSourceId())), BEFORE_DOC_UPDATE, false);
154            }
155            type = Type.UPDATE;
156            break;
157        case ABOUT_TO_CHECKIN:
158            if (indexIsLatestVersion()) {
159                String query = String.format(
160                        "SELECT * FROM %s WHERE (ecm:isLatestMajorVersion = 1 OR ecm:isLatestVersion = 1) AND ecm:versionVersionableId= '%s'",
161                        doc.getType(), doc.getId());
162                DocumentModelList versions = doc.getCoreSession().query(query);
163                for (DocumentModel version : versions) {
164                    stackCommand(version, BEFORE_DOC_UPDATE, false);
165                }
166            }
167            type = Type.UPDATE;
168            break;
169        case DOCUMENT_MOVED:
170            type = Type.UPDATE;
171            recurse = isFolderish(doc);
172            break;
173        case DOCUMENT_REMOVED:
174            type = Type.DELETE;
175            recurse = isFolderish(doc);
176            break;
177        case DOCUMENT_SECURITY_UPDATED:
178            type = Type.UPDATE_SECURITY;
179            recurse = isFolderish(doc);
180            break;
181        case DOCUMENT_CHILDREN_ORDER_CHANGED:
182            type = Type.UPDATE_DIRECT_CHILDREN;
183            recurse = true;
184            break;
185        default:
186            return;
187        }
188        if (sync && recurse) {
189            // split into 2 commands one sync and an async recurse
190            cmds.add(type, true, false);
191            cmds.add(type, false, true);
192        } else {
193            cmds.add(type, sync, recurse);
194        }
195    }
196
197    private boolean indexIsLatestVersion() {
198        return !Framework.isBooleanPropertyTrue(AbstractSession.DISABLED_ISLATESTVERSION_PROPERTY);
199    }
200
201    private boolean isFolderish(DocumentModel doc) {
202        return doc.isFolder() && !doc.isVersion();
203    }
204
205    protected IndexingCommands getOrCreateCommands(DocumentModel doc) {
206        IndexingCommands cmds = getCommands(doc);
207        if (cmds == null) {
208            cmds = new IndexingCommands(doc);
209            getAllCommands().put(getDocKey(doc), cmds);
210        }
211        return cmds;
212    }
213
214    protected String getDocKey(DocumentModel doc) {
215        return doc.getId();
216    }
217
218}