/* * (C) Copyright 2014 Nuxeo SA (http://nuxeo.com/) and others. * * 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. * * Contributors: * Thierry Delprat * Benoit Delbosc */ package org.nuxeo.elasticsearch.commands; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.codehaus.jackson.JsonFactory; import org.codehaus.jackson.JsonGenerator; import org.codehaus.jackson.JsonNode; import org.codehaus.jackson.map.ObjectMapper; import org.nuxeo.ecm.core.api.CoreSession; import org.nuxeo.ecm.core.api.CoreSessionService; import org.nuxeo.ecm.core.api.DocumentModel; import org.nuxeo.ecm.core.api.DocumentRef; import org.nuxeo.ecm.core.api.IdRef; import org.nuxeo.runtime.api.Framework; import java.io.IOException; import java.io.PrintWriter; import java.io.Serializable; import java.io.StringWriter; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicLong; /** * Holds information about what type of indexing operation must be processed. IndexingCommands are create "on the fly" * via a Synchronous event listener and at post commit time the system will merge the commands and execute worker to * process them. */ public class IndexingCommand implements Serializable { private static final Log log = LogFactory.getLog(IndexingCommand.class); private static final long serialVersionUID = 1L; public enum Type { INSERT, UPDATE, UPDATE_SECURITY, DELETE, UPDATE_DIRECT_CHILDREN, } public static final String PREFIX = "IxCd-"; protected String id; protected Type type; protected boolean sync; protected boolean recurse; protected String targetDocumentId; protected String path; protected String repositoryName; protected List<String> schemas; protected long order; protected transient String sessionId; protected transient static AtomicLong seq = new AtomicLong(0); protected IndexingCommand() { } /** * Create an indexing command * * @param document the target document * @param commandType the type of command * @param sync if true the command will be processed on the same thread after transaction completion and the * Elasticsearch index will be refresh * @param recurse the command affect the document and all its descendants */ public IndexingCommand(DocumentModel document, Type commandType, boolean sync, boolean recurse) { id = PREFIX + seq.incrementAndGet(); type = commandType; this.sync = sync; this.recurse = recurse; if ((sync && recurse) && commandType != Type.DELETE) { // we don't want sync and recursive command throw new IllegalArgumentException("Recurse and synchronous command is not allowed: cmd: " + this + ", doc: " + document); } if (document == null) { throw new IllegalArgumentException("Target document is null for: " + this); } DocumentModel targetDocument = getValidTargetDocument(document); repositoryName = targetDocument.getRepositoryName(); targetDocumentId = targetDocument.getId(); sessionId = targetDocument.getSessionId(); path = targetDocument.getPathAsString(); if (targetDocumentId == null) { throw new IllegalArgumentException("Target document has a null uid: " + this); } } protected DocumentModel getValidTargetDocument(DocumentModel target) { if (target.getId() != null) { return target; } if (target.getRef() == null || target.getCoreSession() == null) { throw new IllegalArgumentException("Invalid target document: " + target); } // transient document try to get it from its path DocumentRef documentRef = target.getRef(); log.warn("Creating indexing command on a document with a null id, ref: " + documentRef + ", trying to get the docId from its path, activate trace level for more info " + this); if (log.isTraceEnabled()) { Throwable throwable = new Throwable(); StringWriter stack = new StringWriter(); throwable.printStackTrace(new PrintWriter(stack)); log.trace("You should use a document returned by session.createDocument, stack " + stack.toString()); } return target.getCoreSession().getDocument(documentRef); } public void attach(CoreSession session) { if (!session.getRepositoryName().equals(repositoryName)) { throw new IllegalArgumentException("Invalid session, expected repo: " + repositoryName + " actual: " + session.getRepositoryName()); } sessionId = session.getSessionId(); assert sessionId != null : "Attach to session with a null sessionId"; } /** * Return the document or null if it does not exists anymore. * * @throws java.lang.IllegalStateException if there is no session attached */ public DocumentModel getTargetDocument() { CoreSession session = null; if (sessionId != null) { session = Framework.getService(CoreSessionService.class).getCoreSession(sessionId); } if (session == null) { throw new IllegalStateException("Command is not attached to a valid session: " + this); } IdRef idref = new IdRef(targetDocumentId); if (!session.exists(idref)) { // Doc was deleted : no way we can fetch it return null; } return session.getDocument(idref); } public String getRepositoryName() { return repositoryName; } /** * @return true if merged */ public boolean merge(IndexingCommand other) { if (canBeMerged(other)) { merge(other.sync, other.recurse); return true; } return false; } protected void merge(boolean sync, boolean recurse) { this.sync = this.sync || sync; this.recurse = this.recurse || recurse; } protected boolean canBeMerged(IndexingCommand other) { if (type != other.type) { return false; } if (type == Type.DELETE) { // we support recursive sync deletion return true; } // only if the result is not a sync and recurse command return !((other.sync || sync) && (other.recurse || recurse)); } public boolean isSync() { return sync; } public boolean isRecurse() { return recurse; } public Type getType() { return type; } public String toJSON() throws IOException { StringWriter out = new StringWriter(); JsonFactory factory = new JsonFactory(); JsonGenerator jsonGen = factory.createJsonGenerator(out); toJSON(jsonGen); out.flush(); jsonGen.close(); return out.toString(); } public void toJSON(JsonGenerator jsonGen) throws IOException { jsonGen.writeStartObject(); jsonGen.writeStringField("id", id); jsonGen.writeStringField("type", String.format("%s", type)); jsonGen.writeStringField("docId", getTargetDocumentId()); jsonGen.writeStringField("path", path); jsonGen.writeStringField("repo", getRepositoryName()); jsonGen.writeBooleanField("recurse", recurse); jsonGen.writeBooleanField("sync", sync); jsonGen.writeNumberField("order", getOrder()); jsonGen.writeEndObject(); } /** * Create a command from a JSON string. * * @throws IllegalArgumentException if json is invalid or command is invalid */ public static IndexingCommand fromJSON(String json) { JsonFactory jsonFactory = new JsonFactory(); ObjectMapper mapper = new ObjectMapper(jsonFactory); try { return fromJSON(mapper.readTree(json)); } catch (IOException e) { throw new IllegalArgumentException("Invalid JSON: " + json, e); } } public static IndexingCommand fromJSON(JsonNode jsonNode) { IndexingCommand cmd = new IndexingCommand(); Iterator<Map.Entry<String, JsonNode>> fieldsIterator = jsonNode.getFields(); while (fieldsIterator.hasNext()) { Map.Entry<String, JsonNode> field = fieldsIterator.next(); String key = field.getKey(); JsonNode value = field.getValue(); if (value.isNull()) { continue; } if ("type".equals(key)) { cmd.type = Type.valueOf(value.getTextValue()); } else if ("docId".equals(key)) { cmd.targetDocumentId = value.getTextValue(); } else if ("path".equals(key)) { cmd.path = value.getTextValue(); } else if ("repo".equals(key)) { cmd.repositoryName = value.getTextValue(); } else if ("id".equals(key)) { cmd.id = value.getTextValue(); } else if ("recurse".equals(key)) { cmd.recurse = value.getBooleanValue(); } else if ("sync".equals(key)) { cmd.sync = value.getBooleanValue(); } } if (cmd.targetDocumentId == null) { throw new IllegalArgumentException("Document uid is null: " + cmd); } if (cmd.type == null) { throw new IllegalArgumentException("Invalid type: " + cmd); } return cmd; } public String getId() { return id; } public String getTargetDocumentId() { return targetDocumentId; } public IndexingCommand clone(DocumentModel newDoc) { return new IndexingCommand(newDoc, type, sync, recurse); } public String[] getSchemas() { String[] ret = null; if (schemas != null && schemas.size() > 0) { ret = schemas.toArray(new String[schemas.size()]); } return ret; } public void addSchemas(String schema) { if (schemas == null) { schemas = new ArrayList<>(); } if (!schemas.contains(schema)) { schemas.add(schema); } } @Override public String toString() { try { return toJSON(); } catch (IOException e) { return super.toString(); } } /** * Try to make the command synchronous. Recurse command will stay in async for update. */ public void makeSync() { if (!sync) { if (!recurse || type == Type.DELETE) { sync = true; if (log.isDebugEnabled()) { log.debug("Turn command into sync: " + toString()); } } } } // @since 8.3 public long getOrder() { return order; } // @since 8.3 public void setOrder(long order) { this.order = order; } }