001/*
002 * (C) Copyright 2006-2013 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 *     Bogdan Stefanescu
018 *     Florent Guillaume
019 *     Benoit Delbosc
020 */
021
022package org.nuxeo.ecm.core.api;
023
024import static org.nuxeo.ecm.core.api.event.CoreEventConstants.CHANGED_ACL_NAME;
025import static org.nuxeo.ecm.core.api.event.CoreEventConstants.NEW_ACE;
026import static org.nuxeo.ecm.core.api.event.CoreEventConstants.OLD_ACE;
027import static org.nuxeo.ecm.core.api.security.SecurityConstants.ADD_CHILDREN;
028import static org.nuxeo.ecm.core.api.security.SecurityConstants.BROWSE;
029import static org.nuxeo.ecm.core.api.security.SecurityConstants.READ;
030import static org.nuxeo.ecm.core.api.security.SecurityConstants.READ_CHILDREN;
031import static org.nuxeo.ecm.core.api.security.SecurityConstants.READ_LIFE_CYCLE;
032import static org.nuxeo.ecm.core.api.security.SecurityConstants.READ_PROPERTIES;
033import static org.nuxeo.ecm.core.api.security.SecurityConstants.READ_SECURITY;
034import static org.nuxeo.ecm.core.api.security.SecurityConstants.READ_VERSION;
035import static org.nuxeo.ecm.core.api.security.SecurityConstants.REMOVE;
036import static org.nuxeo.ecm.core.api.security.SecurityConstants.REMOVE_CHILDREN;
037import static org.nuxeo.ecm.core.api.security.SecurityConstants.SYSTEM_USERNAME;
038import static org.nuxeo.ecm.core.api.security.SecurityConstants.UNLOCK;
039import static org.nuxeo.ecm.core.api.security.SecurityConstants.WRITE;
040import static org.nuxeo.ecm.core.api.security.SecurityConstants.WRITE_LIFE_CYCLE;
041import static org.nuxeo.ecm.core.api.security.SecurityConstants.WRITE_PROPERTIES;
042import static org.nuxeo.ecm.core.api.security.SecurityConstants.WRITE_SECURITY;
043import static org.nuxeo.ecm.core.api.security.SecurityConstants.WRITE_VERSION;
044
045import java.io.Serializable;
046import java.security.Principal;
047import java.text.DateFormat;
048import java.util.ArrayList;
049import java.util.Arrays;
050import java.util.Collection;
051import java.util.Collections;
052import java.util.Comparator;
053import java.util.Date;
054import java.util.GregorianCalendar;
055import java.util.HashMap;
056import java.util.List;
057import java.util.Map;
058import java.util.Map.Entry;
059
060import org.apache.commons.logging.Log;
061import org.apache.commons.logging.LogFactory;
062import org.nuxeo.common.collections.ScopeType;
063import org.nuxeo.common.collections.ScopedMap;
064import org.nuxeo.ecm.core.CoreService;
065import org.nuxeo.ecm.core.NXCore;
066import org.nuxeo.ecm.core.api.DocumentModel.DocumentModelRefresh;
067import org.nuxeo.ecm.core.api.event.CoreEventConstants;
068import org.nuxeo.ecm.core.api.event.DocumentEventCategories;
069import org.nuxeo.ecm.core.api.event.DocumentEventTypes;
070import org.nuxeo.ecm.core.api.facet.VersioningDocument;
071import org.nuxeo.ecm.core.api.impl.DocumentModelChildrenIterator;
072import org.nuxeo.ecm.core.api.impl.DocumentModelListImpl;
073import org.nuxeo.ecm.core.api.impl.FacetFilter;
074import org.nuxeo.ecm.core.api.impl.UserPrincipal;
075import org.nuxeo.ecm.core.api.impl.VersionModelImpl;
076import org.nuxeo.ecm.core.api.security.ACE;
077import org.nuxeo.ecm.core.api.security.ACP;
078import org.nuxeo.ecm.core.api.security.SecurityConstants;
079import org.nuxeo.ecm.core.api.security.UserEntry;
080import org.nuxeo.ecm.core.api.security.impl.ACPImpl;
081import org.nuxeo.ecm.core.api.security.impl.UserEntryImpl;
082import org.nuxeo.ecm.core.api.validation.DocumentValidationException;
083import org.nuxeo.ecm.core.api.validation.DocumentValidationReport;
084import org.nuxeo.ecm.core.api.validation.DocumentValidationService;
085import org.nuxeo.ecm.core.event.Event;
086import org.nuxeo.ecm.core.event.EventService;
087import org.nuxeo.ecm.core.event.impl.DocumentEventContext;
088import org.nuxeo.ecm.core.lifecycle.LifeCycleService;
089import org.nuxeo.ecm.core.model.Document;
090import org.nuxeo.ecm.core.model.PathComparator;
091import org.nuxeo.ecm.core.model.Session;
092import org.nuxeo.ecm.core.query.QueryFilter;
093import org.nuxeo.ecm.core.query.QueryParseException;
094import org.nuxeo.ecm.core.query.sql.NXQL;
095import org.nuxeo.ecm.core.query.sql.model.SQLQuery.Transformer;
096import org.nuxeo.ecm.core.schema.DocumentType;
097import org.nuxeo.ecm.core.schema.FacetNames;
098import org.nuxeo.ecm.core.schema.SchemaManager;
099import org.nuxeo.ecm.core.schema.types.CompositeType;
100import org.nuxeo.ecm.core.schema.types.Schema;
101import org.nuxeo.ecm.core.security.SecurityService;
102import org.nuxeo.ecm.core.versioning.VersioningService;
103import org.nuxeo.runtime.api.Framework;
104import org.nuxeo.runtime.metrics.MetricsService;
105
106import com.codahale.metrics.Counter;
107import com.codahale.metrics.MetricRegistry;
108import com.codahale.metrics.SharedMetricRegistries;
109
110/**
111 * Abstract implementation of the client interface.
112 * <p>
113 * This handles all the aspects that are independent on the final implementation (like running inside a J2EE platform or
114 * not).
115 * <p>
116 * The only aspect not implemented is the session management that should be handled by subclasses.
117 *
118 * @author Bogdan Stefanescu
119 * @author Florent Guillaume
120 */
121public abstract class AbstractSession implements CoreSession, Serializable {
122
123    public static final NuxeoPrincipal ANONYMOUS = new UserPrincipal("anonymous", new ArrayList<String>(), true, false);
124
125    private static final Log log = LogFactory.getLog(CoreSession.class);
126
127    private static final long serialVersionUID = 1L;
128
129    private static final Comparator<? super Document> pathComparator = new PathComparator();
130
131    public static final String DEFAULT_MAX_RESULTS = "1000";
132
133    public static final String MAX_RESULTS_PROPERTY = "org.nuxeo.ecm.core.max.results";
134
135    public static final String LIMIT_RESULTS_PROPERTY = "org.nuxeo.ecm.core.limit.results";
136
137    public static final String BINARY_TEXT_SYS_PROP = "fulltextBinary";
138
139    private Boolean limitedResults;
140
141    private Long maxResults;
142
143    // @since 5.7.2
144    protected final MetricRegistry registry = SharedMetricRegistries.getOrCreate(MetricsService.class.getName());
145
146    protected Counter createDocumentCount;
147
148    protected Counter deleteDocumentCount;
149
150    protected Counter updateDocumentCount;
151
152    protected void createMetrics() {
153        createDocumentCount = registry.counter(
154                MetricRegistry.name("nuxeo.repositories", getRepositoryName(), "documents", "create"));
155        deleteDocumentCount = registry.counter(
156                MetricRegistry.name("nuxeo.repositories", getRepositoryName(), "documents", "delete"));
157        updateDocumentCount = registry.counter(
158                MetricRegistry.name("nuxeo.repositories", getRepositoryName(), "documents", "update"));
159    }
160
161    /**
162     * Used to check permissions.
163     */
164    private transient SecurityService securityService;
165
166    protected SecurityService getSecurityService() {
167        if (securityService == null) {
168            securityService = NXCore.getSecurityService();
169        }
170        return securityService;
171    }
172
173    private transient VersioningService versioningService;
174
175    protected VersioningService getVersioningService() {
176        if (versioningService == null) {
177            versioningService = Framework.getService(VersioningService.class);
178        }
179        return versioningService;
180    }
181
182    private transient DocumentValidationService validationService;
183
184    protected DocumentValidationService getValidationService() {
185        if (validationService == null) {
186            validationService = Framework.getService(DocumentValidationService.class);
187        }
188        return validationService;
189    }
190
191    /**
192     * Internal method: Gets the current session based on the client session id.
193     *
194     * @return the repository session
195     */
196    public abstract Session getSession();
197
198    @Override
199    public DocumentType getDocumentType(String type) {
200        return Framework.getLocalService(SchemaManager.class).getDocumentType(type);
201    }
202
203    protected final void checkPermission(Document doc, String permission) throws DocumentSecurityException {
204        if (isAdministrator()) {
205            return;
206        }
207        if (!hasPermission(doc, permission)) {
208            log.debug("Permission '" + permission + "' is not granted to '" + getPrincipal().getName()
209                    + "' on document " + doc.getPath() + " (" + doc.getUUID() + " - " + doc.getType().getName() + ")");
210            throw new DocumentSecurityException(
211                    "Privilege '" + permission + "' is not granted to '" + getPrincipal().getName() + "'");
212        }
213    }
214
215    protected Map<String, Serializable> getContextMapEventInfo(DocumentModel doc) {
216        Map<String, Serializable> options = new HashMap<String, Serializable>();
217        if (doc != null) {
218            ScopedMap ctxData = doc.getContextData();
219            if (ctxData != null) {
220                options.putAll(ctxData.getDefaultScopeValues());
221                options.putAll(ctxData.getScopeValues(ScopeType.REQUEST));
222            }
223        }
224        return options;
225    }
226
227    public DocumentEventContext newEventContext(DocumentModel source) {
228        DocumentEventContext ctx = new DocumentEventContext(this, getPrincipal(), source);
229        ctx.setProperty(CoreEventConstants.REPOSITORY_NAME, getRepositoryName());
230        ctx.setProperty(CoreEventConstants.SESSION_ID, getSessionId());
231        return ctx;
232    }
233
234    protected void notifyEvent(String eventId, DocumentModel source, Map<String, Serializable> options, String category,
235            String comment, boolean withLifeCycle, boolean inline) {
236
237        DocumentEventContext ctx = new DocumentEventContext(this, getPrincipal(), source);
238
239        // compatibility with old code (< 5.2.M4) - import info from old event
240        // model
241        if (options != null) {
242            ctx.setProperties(options);
243        }
244        ctx.setProperty(CoreEventConstants.REPOSITORY_NAME, getRepositoryName());
245        ctx.setProperty(CoreEventConstants.SESSION_ID, getSessionId());
246        // Document life cycle
247        if (source != null && withLifeCycle) {
248            String currentLifeCycleState = source.getCurrentLifeCycleState();
249            ctx.setProperty(CoreEventConstants.DOC_LIFE_CYCLE, currentLifeCycleState);
250        }
251        if (comment != null) {
252            ctx.setProperty("comment", comment);
253        }
254        ctx.setProperty("category", category == null ? DocumentEventCategories.EVENT_DOCUMENT_CATEGORY : category);
255        // compat code: mark SAVE event as a commit event
256        Event event = ctx.newEvent(eventId);
257        if (DocumentEventTypes.SESSION_SAVED.equals(eventId)) {
258            event.setIsCommitEvent(true);
259        }
260        if (inline) {
261            event.setInline(true);
262        }
263        // compat code: set isLocal on event if JMS is blocked
264        if (source != null) {
265            Boolean blockJms = (Boolean) source.getContextData("BLOCK_JMS_PRODUCING");
266            if (blockJms != null && blockJms) {
267                event.setLocal(true);
268                event.setInline(true);
269            }
270        }
271        Framework.getLocalService(EventService.class).fireEvent(event);
272    }
273
274    /**
275     * Copied from obsolete VersionChangeNotifier.
276     * <p>
277     * Sends change notifications to core event listeners. The event contains info with older document (before version
278     * change) and newer doc (current document).
279     *
280     * @param oldDocument
281     * @param newDocument
282     * @param options additional info to pass to the event
283     */
284    protected void notifyVersionChange(DocumentModel oldDocument, DocumentModel newDocument,
285            Map<String, Serializable> options) {
286        final Map<String, Serializable> info = new HashMap<String, Serializable>();
287        if (options != null) {
288            info.putAll(options);
289        }
290        info.put(VersioningChangeNotifier.EVT_INFO_NEW_DOC_KEY, newDocument);
291        info.put(VersioningChangeNotifier.EVT_INFO_OLD_DOC_KEY, oldDocument);
292        notifyEvent(VersioningChangeNotifier.CORE_EVENT_ID_VERSIONING_CHANGE, newDocument, info,
293                DocumentEventCategories.EVENT_CLIENT_NOTIF_CATEGORY, null, false, false);
294    }
295
296    @Override
297    public boolean hasPermission(Principal principal, DocumentRef docRef, String permission) {
298        Document doc = resolveReference(docRef);
299        return hasPermission(principal, doc, permission);
300    }
301
302    protected final boolean hasPermission(Principal principal, Document doc, String permission) {
303        return getSecurityService().checkPermission(doc, principal, permission);
304    }
305
306    @Override
307    public boolean hasPermission(DocumentRef docRef, String permission) {
308        Document doc = resolveReference(docRef);
309        return hasPermission(doc, permission);
310    }
311
312    protected final boolean hasPermission(Document doc, String permission) {
313        // TODO: optimize this - usually ACP is already available when calling
314        // this method.
315        // -> cache ACP at securitymanager level or try to reuse the ACP when
316        // it is known
317        return getSecurityService().checkPermission(doc, getPrincipal(), permission);
318        // return doc.getSession().getSecurityManager().checkPermission(doc,
319        // getPrincipal().getName(), permission);
320    }
321
322    protected Document resolveReference(DocumentRef docRef) {
323        if (docRef == null) {
324            throw new IllegalArgumentException("null docRref");
325        }
326        Object ref = docRef.reference();
327        if (ref == null) {
328            throw new IllegalArgumentException("null reference");
329        }
330        int type = docRef.type();
331        switch (type) {
332        case DocumentRef.ID:
333            return getSession().getDocumentByUUID((String) ref);
334        case DocumentRef.PATH:
335            return getSession().resolvePath((String) ref);
336        case DocumentRef.INSTANCE:
337            return getSession().getDocumentByUUID(((DocumentModel)ref).getId());
338        default:
339            throw new IllegalArgumentException("Invalid type: " + type);
340        }
341    }
342
343    /**
344     * Gets the document model for the given core document.
345     *
346     * @param doc the document
347     * @return the document model
348     */
349    protected DocumentModel readModel(Document doc) {
350        return DocumentModelFactory.createDocumentModel(doc, getSessionId(), null);
351    }
352
353    /**
354     * Gets the document model for the given core document, preserving the contextData.
355     *
356     * @param doc the document
357     * @return the document model
358     */
359    protected DocumentModel readModel(Document doc, DocumentModel docModel) {
360        DocumentModel newModel = readModel(doc);
361        newModel.copyContextData(docModel);
362        return newModel;
363    }
364
365    protected DocumentModel writeModel(Document doc, DocumentModel docModel) {
366        return DocumentModelFactory.writeDocumentModel(docModel, doc);
367    }
368
369    @Override
370    public DocumentModel copy(DocumentRef src, DocumentRef dst, String name, boolean resetLifeCycle) {
371        Document dstDoc = resolveReference(dst);
372        checkPermission(dstDoc, ADD_CHILDREN);
373
374        Document srcDoc = resolveReference(src);
375        if (name == null) {
376            name = srcDoc.getName();
377        } else {
378            PathRef.checkName(name);
379        }
380
381        Map<String, Serializable> options = new HashMap<String, Serializable>();
382
383        // add the destination name, destination, resetLifeCycle flag and
384        // source references in
385        // the options of the event
386        options.put(CoreEventConstants.SOURCE_REF, src);
387        options.put(CoreEventConstants.DESTINATION_REF, dst);
388        options.put(CoreEventConstants.DESTINATION_PATH, dstDoc.getPath());
389        options.put(CoreEventConstants.DESTINATION_NAME, name);
390        options.put(CoreEventConstants.DESTINATION_EXISTS, dstDoc.hasChild(name));
391        options.put(CoreEventConstants.RESET_LIFECYCLE, resetLifeCycle);
392        DocumentModel srcDocModel = readModel(srcDoc);
393        notifyEvent(DocumentEventTypes.ABOUT_TO_COPY, srcDocModel, options, null, null, true, true);
394
395        name = (String) options.get(CoreEventConstants.DESTINATION_NAME);
396        Document doc = getSession().copy(srcDoc, dstDoc, name);
397        // no need to clear lock, locks table is not copied
398
399        // notify document created by copy
400        DocumentModel docModel = readModel(doc);
401
402        String comment = srcDoc.getRepositoryName() + ':' + src.toString();
403        notifyEvent(DocumentEventTypes.DOCUMENT_CREATED_BY_COPY, docModel, options, null, comment, true, false);
404        docModel = writeModel(doc, docModel);
405
406        // notify document copied
407        comment = doc.getRepositoryName() + ':' + docModel.getRef().toString();
408
409        notifyEvent(DocumentEventTypes.DOCUMENT_DUPLICATED, srcDocModel, options, null, comment, true, false);
410
411        return docModel;
412    }
413
414    @Override
415    public DocumentModel copy(DocumentRef src, DocumentRef dst, String name) {
416        return copy(src, dst, name, false);
417    }
418
419    @Override
420    public List<DocumentModel> copy(List<DocumentRef> src, DocumentRef dst, boolean resetLifeCycle) {
421        List<DocumentModel> newDocuments = new ArrayList<DocumentModel>();
422
423        for (DocumentRef ref : src) {
424            newDocuments.add(copy(ref, dst, null, resetLifeCycle));
425        }
426
427        return newDocuments;
428    }
429
430    @Override
431    public List<DocumentModel> copy(List<DocumentRef> src, DocumentRef dst) {
432        return copy(src, dst, false);
433    }
434
435    @Override
436    public DocumentModel copyProxyAsDocument(DocumentRef src, DocumentRef dst, String name, boolean resetLifeCycle) {
437        Document srcDoc = resolveReference(src);
438        if (!srcDoc.isProxy()) {
439            return copy(src, dst, name);
440        }
441        Document dstDoc = resolveReference(dst);
442        checkPermission(dstDoc, WRITE);
443
444        // create a new document using the expanded proxy
445        DocumentModel srcDocModel = readModel(srcDoc);
446        String docName = (name != null) ? name : srcDocModel.getName();
447        DocumentModel docModel = createDocumentModel(dstDoc.getPath(), docName, srcDocModel.getType());
448        docModel.copyContent(srcDocModel);
449        notifyEvent(DocumentEventTypes.ABOUT_TO_COPY, srcDocModel, null, null, null, true, true);
450        docModel = createDocument(docModel);
451        Document doc = resolveReference(docModel.getRef());
452
453        Map<String, Serializable> options = new HashMap<String, Serializable>();
454        // add resetLifeCycle flag to the event
455        options.put(CoreEventConstants.RESET_LIFECYCLE, resetLifeCycle);
456        // notify document created by copy
457        String comment = srcDoc.getRepositoryName() + ':' + src.toString();
458        notifyEvent(DocumentEventTypes.DOCUMENT_CREATED_BY_COPY, docModel, options, null, comment, true, false);
459
460        // notify document copied
461        comment = doc.getRepositoryName() + ':' + docModel.getRef().toString();
462        notifyEvent(DocumentEventTypes.DOCUMENT_DUPLICATED, srcDocModel, options, null, comment, true, false);
463
464        return docModel;
465    }
466
467    @Override
468    public DocumentModel copyProxyAsDocument(DocumentRef src, DocumentRef dst, String name) {
469        return copyProxyAsDocument(src, dst, name, false);
470    }
471
472    @Override
473    public List<DocumentModel> copyProxyAsDocument(List<DocumentRef> src, DocumentRef dst, boolean resetLifeCycle) {
474        List<DocumentModel> newDocuments = new ArrayList<DocumentModel>();
475
476        for (DocumentRef ref : src) {
477            newDocuments.add(copyProxyAsDocument(ref, dst, null, resetLifeCycle));
478        }
479
480        return newDocuments;
481    }
482
483    @Override
484    public List<DocumentModel> copyProxyAsDocument(List<DocumentRef> src, DocumentRef dst) {
485        return copyProxyAsDocument(src, dst, false);
486    }
487
488    @Override
489    public DocumentModel move(DocumentRef src, DocumentRef dst, String name) {
490        Document srcDoc = resolveReference(src);
491        Document dstDoc;
492        if (dst == null) {
493            // rename
494            dstDoc = srcDoc.getParent();
495            checkPermission(dstDoc, WRITE_PROPERTIES);
496        } else {
497            dstDoc = resolveReference(dst);
498            checkPermission(dstDoc, ADD_CHILDREN);
499            checkPermission(srcDoc.getParent(), REMOVE_CHILDREN);
500            checkPermission(srcDoc, REMOVE);
501        }
502
503        DocumentModel srcDocModel = readModel(srcDoc);
504        String originalName = srcDocModel.getName();
505        if (name == null) {
506            name = srcDocModel.getName();
507        } else {
508            PathRef.checkName(name);
509        }
510        Map<String, Serializable> options = getContextMapEventInfo(srcDocModel);
511        // add the destination name, destination and source references in
512        // the options of the event
513        options.put(CoreEventConstants.SOURCE_REF, src);
514        options.put(CoreEventConstants.DESTINATION_REF, dst);
515        options.put(CoreEventConstants.DESTINATION_PATH, dstDoc.getPath());
516        options.put(CoreEventConstants.DESTINATION_NAME, name);
517        options.put(CoreEventConstants.DESTINATION_EXISTS, dstDoc.hasChild(name));
518
519        notifyEvent(DocumentEventTypes.ABOUT_TO_MOVE, srcDocModel, options, null, null, true, true);
520
521        name = (String) options.get(CoreEventConstants.DESTINATION_NAME);
522
523        if (!originalName.equals(name)) {
524            options.put(CoreEventConstants.ORIGINAL_NAME, originalName);
525        }
526
527        String comment = srcDoc.getRepositoryName() + ':' + srcDoc.getParent().getUUID();
528
529        Document doc = getSession().move(srcDoc, dstDoc, name);
530
531        // notify document moved
532        DocumentModel docModel = readModel(doc);
533        options.put(CoreEventConstants.PARENT_PATH, srcDocModel.getParentRef());
534        notifyEvent(DocumentEventTypes.DOCUMENT_MOVED, docModel, options, null, comment, true, false);
535
536        return docModel;
537    }
538
539    @Override
540    public void move(List<DocumentRef> src, DocumentRef dst) {
541        for (DocumentRef ref : src) {
542            move(ref, dst, null);
543        }
544    }
545
546    @Override
547    public ACP getACP(DocumentRef docRef) {
548        Document doc = resolveReference(docRef);
549        checkPermission(doc, READ_SECURITY);
550        return getSession().getMergedACP(doc);
551    }
552
553    @Override
554    public void setACP(DocumentRef docRef, ACP newAcp, boolean overwrite) {
555        Document doc = resolveReference(docRef);
556        checkPermission(doc, WRITE_SECURITY);
557
558        setACP(doc, newAcp, overwrite, null);
559    }
560
561    protected void setACP(Document doc, ACP newAcp, boolean overwrite, Map<String, Serializable> options) {
562        DocumentModel docModel = readModel(doc);
563        if (options == null) {
564            options = new HashMap<>();
565        }
566        options.put(CoreEventConstants.OLD_ACP, docModel.getACP().clone());
567        options.put(CoreEventConstants.NEW_ACP, newAcp);
568
569        notifyEvent(DocumentEventTypes.BEFORE_DOC_SECU_UPDATE, docModel, options, null, null, true, true);
570        getSession().setACP(doc, newAcp, overwrite);
571        docModel = readModel(doc);
572        options.put(CoreEventConstants.NEW_ACP, newAcp.clone());
573        notifyEvent(DocumentEventTypes.DOCUMENT_SECURITY_UPDATED, docModel, options, null, null, true, false);
574    }
575
576    @Override
577    public void replaceACE(DocumentRef docRef, String aclName, ACE oldACE, ACE newACE) {
578        Document doc = resolveReference(docRef);
579        checkPermission(doc, WRITE_SECURITY);
580
581        ACP acp = getACP(docRef);
582        if (acp.replaceACE(aclName, oldACE, newACE)) {
583            Map<String, Serializable> options = new HashMap<>();
584            options.put(OLD_ACE, oldACE);
585            options.put(NEW_ACE, newACE);
586            options.put(CHANGED_ACL_NAME, aclName);
587            setACP(doc, acp, true, options);
588        }
589    }
590
591    @Override
592    public boolean isNegativeAclAllowed() {
593        return getSession().isNegativeAclAllowed();
594    }
595
596    @Override
597    public void cancel() {
598        // nothing
599    }
600
601    private DocumentModel createDocumentModelFromTypeName(String typeName, Map<String, Serializable> options) {
602        SchemaManager schemaManager = Framework.getLocalService(SchemaManager.class);
603        DocumentType docType = schemaManager.getDocumentType(typeName);
604        if (docType == null) {
605            throw new IllegalArgumentException(typeName + " is not a registered core type");
606        }
607        DocumentModel docModel = DocumentModelFactory.createDocumentModel(getSessionId(), docType);
608        if (options == null) {
609            options = new HashMap<String, Serializable>();
610        }
611        // do not forward this event on the JMS Bus
612        options.put("BLOCK_JMS_PRODUCING", true);
613        notifyEvent(DocumentEventTypes.EMPTY_DOCUMENTMODEL_CREATED, docModel, options, null, null, false, true);
614        return docModel;
615    }
616
617    @Override
618    public DocumentModel createDocumentModel(String typeName) {
619        Map<String, Serializable> options = new HashMap<String, Serializable>();
620        return createDocumentModelFromTypeName(typeName, options);
621    }
622
623    @Override
624    public DocumentModel createDocumentModel(String parentPath, String name, String typeName) {
625        Map<String, Serializable> options = new HashMap<String, Serializable>();
626        options.put(CoreEventConstants.PARENT_PATH, parentPath);
627        options.put(CoreEventConstants.DOCUMENT_MODEL_ID, name);
628        options.put(CoreEventConstants.DESTINATION_NAME, name);
629        DocumentModel model = createDocumentModelFromTypeName(typeName, options);
630        model.setPathInfo(parentPath, name);
631        return model;
632    }
633
634    @Override
635    public DocumentModel createDocumentModel(String typeName, Map<String, Object> options) {
636
637        Map<String, Serializable> serializableOptions = new HashMap<String, Serializable>();
638
639        for (Entry<String, Object> entry : options.entrySet()) {
640            serializableOptions.put(entry.getKey(), (Serializable) entry.getValue());
641        }
642        return createDocumentModelFromTypeName(typeName, serializableOptions);
643    }
644
645    @Override
646    public DocumentModel createDocument(DocumentModel docModel) {
647        if (docModel.getSessionId() == null) {
648            // docModel was created using constructor instead of CoreSession.createDocumentModel
649            docModel.attach(getSessionId());
650        }
651        String typeName = docModel.getType();
652        DocumentRef parentRef = docModel.getParentRef();
653        if (typeName == null) {
654            throw new NullPointerException("null typeName");
655        }
656        if (parentRef == null && !isAdministrator()) {
657            throw new NuxeoException("Only Administrators can create placeless documents");
658        }
659        String childName = docModel.getName();
660        Map<String, Serializable> options = getContextMapEventInfo(docModel);
661
662        // document validation
663        if (getValidationService().isActivated(DocumentValidationService.CTX_CREATEDOC, options)) {
664            DocumentValidationReport report = getValidationService().validate(docModel, true);
665            if (report.hasError()) {
666                throw new DocumentValidationException(report);
667            }
668        }
669
670        Document folder = fillCreateOptions(parentRef, childName, options);
671
672        // get initial life cycle state info
673        String initialLifecycleState = null;
674        Object lifecycleStateInfo = docModel.getContextData(LifeCycleConstants.INITIAL_LIFECYCLE_STATE_OPTION_NAME);
675        if (lifecycleStateInfo instanceof String) {
676            initialLifecycleState = (String) lifecycleStateInfo;
677        }
678        notifyEvent(DocumentEventTypes.ABOUT_TO_CREATE, docModel, options, null, null, false, true); // no lifecycle
679                                                                                                     // yet
680        childName = (String) options.get(CoreEventConstants.DESTINATION_NAME);
681        Document doc = folder.addChild(childName, typeName);
682
683        // update facets too since some of them may be dynamic
684        for (String facetName : docModel.getFacets()) {
685            if (!doc.getAllFacets().contains(facetName) && !FacetNames.IMMUTABLE.equals(facetName)) {
686                doc.addFacet(facetName);
687            }
688        }
689
690        // init document life cycle
691        NXCore.getLifeCycleService().initialize(doc, initialLifecycleState);
692
693        // init document with data from doc model
694        docModel = writeModel(doc, docModel);
695
696        if (!Boolean.TRUE.equals(docModel.getContextData(ScopeType.REQUEST, VersioningService.SKIP_VERSIONING))) {
697            // during remote publishing we want to skip versioning
698            // to avoid overwriting the version number
699            getVersioningService().doPostCreate(doc, options);
700            docModel = readModel(doc, docModel);
701        }
702
703        notifyEvent(DocumentEventTypes.DOCUMENT_CREATED, docModel, options, null, null, true, false);
704        docModel = writeModel(doc, docModel);
705
706        createDocumentCount.inc();
707        return docModel;
708    }
709
710    protected Document fillCreateOptions(DocumentRef parentRef, String childName, Map<String, Serializable> options)
711            throws DocumentSecurityException {
712        Document folder;
713        if (parentRef == null || EMPTY_PATH.equals(parentRef)) {
714            folder = getSession().getNullDocument();
715            options.put(CoreEventConstants.DESTINATION_REF, null);
716            options.put(CoreEventConstants.DESTINATION_PATH, null);
717            options.put(CoreEventConstants.DESTINATION_NAME, childName);
718            options.put(CoreEventConstants.DESTINATION_EXISTS, false);
719        } else {
720            folder = resolveReference(parentRef);
721            checkPermission(folder, ADD_CHILDREN);
722            options.put(CoreEventConstants.DESTINATION_REF, parentRef);
723            options.put(CoreEventConstants.DESTINATION_PATH, folder.getPath());
724            options.put(CoreEventConstants.DESTINATION_NAME, childName);
725            options.put(CoreEventConstants.DESTINATION_EXISTS, folder.hasChild(childName));
726        }
727        return folder;
728    }
729
730    @Override
731    public void importDocuments(List<DocumentModel> docModels) {
732        for (DocumentModel docModel : docModels) {
733            importDocument(docModel);
734        }
735    }
736
737    protected static final PathRef EMPTY_PATH = new PathRef("");
738
739    protected void importDocument(DocumentModel docModel) {
740        if (!isAdministrator()) {
741            throw new DocumentSecurityException("Only Administrator can import");
742        }
743        String name = docModel.getName();
744        if (name == null || name.length() == 0) {
745            throw new IllegalArgumentException("Invalid empty name");
746        }
747        String typeName = docModel.getType();
748        if (typeName == null || typeName.length() == 0) {
749            throw new IllegalArgumentException("Invalid empty type");
750        }
751        String id = docModel.getId();
752        if (id == null || id.length() == 0) {
753            throw new IllegalArgumentException("Invalid empty id");
754        }
755
756        DocumentRef parentRef = docModel.getParentRef();
757        Map<String, Serializable> props = getContextMapEventInfo(docModel);
758
759        // document validation
760        if (getValidationService().isActivated(DocumentValidationService.CTX_IMPORTDOC, props)) {
761            DocumentValidationReport report = getValidationService().validate(docModel, true);
762            if (report.hasError()) {
763                throw new DocumentValidationException(report);
764            }
765        }
766
767        if (parentRef != null && EMPTY_PATH.equals(parentRef)) {
768            parentRef = null;
769        }
770        Document parent = fillCreateOptions(parentRef, name, props);
771        notifyEvent(DocumentEventTypes.ABOUT_TO_IMPORT, docModel, props, null, null, false, true);
772        name = (String) props.get(CoreEventConstants.DESTINATION_NAME);
773
774        // create the document
775        Document doc = getSession().importDocument(id, parentRef == null ? null : parent, name, typeName, props);
776
777        if (typeName.equals(CoreSession.IMPORT_PROXY_TYPE)) {
778            // just reread the final document
779            docModel = readModel(doc);
780        } else {
781            // init document with data from doc model
782            docModel = writeModel(doc, docModel);
783        }
784
785        // send an event about the import
786        notifyEvent(DocumentEventTypes.DOCUMENT_IMPORTED, docModel, null, null, null, true, false);
787    }
788
789    @Override
790    public DocumentModel[] createDocument(DocumentModel[] docModels) {
791        DocumentModel[] models = new DocumentModel[docModels.length];
792        int i = 0;
793        // TODO: optimize this (do not call at each iteration createDocument())
794        for (DocumentModel docModel : docModels) {
795            models[i++] = createDocument(docModel);
796        }
797        return models;
798    }
799
800    @Override
801    public boolean exists(DocumentRef docRef) {
802        try {
803            Document doc = resolveReference(docRef);
804            return hasPermission(doc, BROWSE);
805        } catch (DocumentNotFoundException e) {
806            return false;
807        }
808    }
809
810    @Override
811    public DocumentModel getChild(DocumentRef parent, String name) {
812        Document doc = resolveReference(parent);
813        checkPermission(doc, READ_CHILDREN);
814        Document child = doc.getChild(name);
815        checkPermission(child, READ);
816        return readModel(child);
817    }
818
819    @Override
820    public boolean hasChild(DocumentRef parent, String name) {
821        Document doc = resolveReference(parent);
822        checkPermission(doc, READ_CHILDREN);
823        return doc.hasChild(name);
824    }
825
826    @Override
827    public DocumentModelList getChildren(DocumentRef parent) {
828        return getChildren(parent, null, READ, null, null);
829    }
830
831    @Override
832    public DocumentModelList getChildren(DocumentRef parent, String type) {
833        return getChildren(parent, type, READ, null, null);
834    }
835
836    @Override
837    public DocumentModelList getChildren(DocumentRef parent, String type, String perm) {
838        return getChildren(parent, type, perm, null, null);
839    }
840
841    @Override
842    public DocumentModelList getChildren(DocumentRef parent, String type, Filter filter, Sorter sorter) {
843        return getChildren(parent, type, null, filter, sorter);
844    }
845
846    @Override
847    public DocumentModelList getChildren(DocumentRef parent, String type, String perm, Filter filter, Sorter sorter) {
848        if (perm == null) {
849            perm = READ;
850        }
851        Document doc = resolveReference(parent);
852        checkPermission(doc, READ_CHILDREN);
853        DocumentModelList docs = new DocumentModelListImpl();
854        for (Document child : doc.getChildren()) {
855            if (hasPermission(child, perm)) {
856                if (child.getType() != null && (type == null || type.equals(child.getType().getName()))) {
857                    DocumentModel childModel = readModel(child);
858                    if (filter == null || filter.accept(childModel)) {
859                        docs.add(childModel);
860                    }
861                }
862            }
863        }
864        if (sorter != null) {
865            Collections.sort(docs, sorter);
866        }
867        return docs;
868    }
869
870    @Override
871    public List<DocumentRef> getChildrenRefs(DocumentRef parentRef, String perm) {
872        if (perm != null) {
873            // XXX TODO
874            throw new NullPointerException("perm != null not implemented");
875        }
876        Document parent = resolveReference(parentRef);
877        checkPermission(parent, READ_CHILDREN);
878        List<String> ids = parent.getChildrenIds();
879        List<DocumentRef> refs = new ArrayList<DocumentRef>(ids.size());
880        for (String id : ids) {
881            refs.add(new IdRef(id));
882        }
883        return refs;
884    }
885
886    @Override
887    public DocumentModelIterator getChildrenIterator(DocumentRef parent) {
888        return getChildrenIterator(parent, null, null, null);
889    }
890
891    @Override
892    public DocumentModelIterator getChildrenIterator(DocumentRef parent, String type) {
893        return getChildrenIterator(parent, type, null, null);
894    }
895
896    @Override
897    public DocumentModelIterator getChildrenIterator(DocumentRef parent, String type, String perm, Filter filter) {
898        // perm unused, kept for API compat
899        return new DocumentModelChildrenIterator(this, parent, type, filter);
900    }
901
902    @Override
903    public DocumentModel getDocument(DocumentRef docRef) {
904        Document doc = resolveReference(docRef);
905        checkPermission(doc, READ);
906        return readModel(doc);
907    }
908
909    @Override
910    public DocumentModelList getDocuments(DocumentRef[] docRefs) {
911        List<DocumentModel> docs = new ArrayList<DocumentModel>(docRefs.length);
912        for (DocumentRef docRef : docRefs) {
913            Document doc;
914            try {
915                doc = resolveReference(docRef);
916                checkPermission(doc, READ);
917            } catch (DocumentSecurityException e) {
918                // no permission
919                continue;
920            }
921            docs.add(readModel(doc));
922        }
923        return new DocumentModelListImpl(docs);
924    }
925
926    @Override
927    public DocumentModelList getFiles(DocumentRef parent) {
928        Document doc = resolveReference(parent);
929        checkPermission(doc, READ_CHILDREN);
930        DocumentModelList docs = new DocumentModelListImpl();
931        for (Document child : doc.getChildren()) {
932            if (!child.isFolder() && hasPermission(child, READ)) {
933                docs.add(readModel(child));
934            }
935        }
936        return docs;
937    }
938
939    @Override
940    public DocumentModelList getFiles(DocumentRef parent, Filter filter, Sorter sorter) {
941        Document doc = resolveReference(parent);
942        checkPermission(doc, READ_CHILDREN);
943        DocumentModelList docs = new DocumentModelListImpl();
944        for (Document child : doc.getChildren()) {
945            if (!child.isFolder() && hasPermission(child, READ)) {
946                DocumentModel docModel = readModel(doc);
947                if (filter == null || filter.accept(docModel)) {
948                    docs.add(readModel(child));
949                }
950            }
951        }
952        if (sorter != null) {
953            Collections.sort(docs, sorter);
954        }
955        return docs;
956    }
957
958    @Override
959    public DocumentModelList getFolders(DocumentRef parent) {
960        Document doc = resolveReference(parent);
961        checkPermission(doc, READ_CHILDREN);
962        DocumentModelList docs = new DocumentModelListImpl();
963        for (Document child : doc.getChildren()) {
964            if (child.isFolder() && hasPermission(child, READ)) {
965                docs.add(readModel(child));
966            }
967        }
968        return docs;
969    }
970
971    @Override
972    public DocumentModelList getFolders(DocumentRef parent, Filter filter, Sorter sorter) {
973        Document doc = resolveReference(parent);
974        checkPermission(doc, READ_CHILDREN);
975        DocumentModelList docs = new DocumentModelListImpl();
976        for (Document child : doc.getChildren()) {
977            if (child.isFolder() && hasPermission(child, READ)) {
978                DocumentModel childModel = readModel(child);
979                if (filter == null || filter.accept(childModel)) {
980                    docs.add(childModel);
981                }
982            }
983        }
984        if (sorter != null) {
985            Collections.sort(docs, sorter);
986        }
987        return docs;
988    }
989
990    @Override
991    public DocumentRef getParentDocumentRef(DocumentRef docRef) {
992        final Document doc = resolveReference(docRef);
993        Document parentDoc = doc.getParent();
994        return parentDoc != null ? new IdRef(parentDoc.getUUID()) : null;
995    }
996
997    @Override
998    public DocumentModel getParentDocument(DocumentRef docRef) {
999        Document doc = resolveReference(docRef);
1000        Document parentDoc = doc.getParent();
1001        if (parentDoc == null) {
1002            return null;
1003        }
1004        if (!hasPermission(parentDoc, READ)) {
1005            throw new DocumentSecurityException("Privilege READ is not granted to " + getPrincipal().getName());
1006        }
1007        return readModel(parentDoc);
1008    }
1009
1010    @Override
1011    public List<DocumentModel> getParentDocuments(final DocumentRef docRef) {
1012
1013        if (null == docRef) {
1014            throw new IllegalArgumentException("null docRef");
1015        }
1016
1017        final List<DocumentModel> docsList = new ArrayList<DocumentModel>();
1018        Document doc = resolveReference(docRef);
1019        while (doc != null && !"/".equals(doc.getPath())) {
1020            // XXX OG: shouldn't we check BROWSE and READ_PROPERTIES
1021            // instead?
1022            if (!hasPermission(doc, READ)) {
1023                break;
1024            }
1025            docsList.add(readModel(doc));
1026            doc = doc.getParent();
1027        }
1028        Collections.reverse(docsList);
1029        return docsList;
1030    }
1031
1032    @Override
1033    public DocumentModel getRootDocument() {
1034        return readModel(getSession().getRootDocument());
1035    }
1036
1037    @Override
1038    public boolean hasChildren(DocumentRef docRef) {
1039        // TODO: validate permission check with td
1040        Document doc = resolveReference(docRef);
1041        checkPermission(doc, BROWSE);
1042        return doc.hasChildren();
1043    }
1044
1045    @Override
1046    public DocumentModelList query(String query) {
1047        return query(query, null, 0, 0, false);
1048    }
1049
1050    @Override
1051    public DocumentModelList query(String query, int max) {
1052        return query(query, null, max, 0, false);
1053    }
1054
1055    @Override
1056    public DocumentModelList query(String query, Filter filter) {
1057        return query(query, filter, 0, 0, false);
1058    }
1059
1060    @Override
1061    public DocumentModelList query(String query, Filter filter, int max) {
1062        return query(query, filter, max, 0, false);
1063    }
1064
1065    @Override
1066    public DocumentModelList query(String query, Filter filter, long limit, long offset, boolean countTotal) {
1067        return query(query, NXQL.NXQL, filter, limit, offset, countTotal);
1068    }
1069
1070    @Override
1071    public DocumentModelList query(String query, String queryType, Filter filter, long limit, long offset,
1072            boolean countTotal) {
1073        long countUpTo;
1074        if (!countTotal) {
1075            countUpTo = 0;
1076        } else {
1077            if (isLimitedResults()) {
1078                countUpTo = getMaxResults();
1079            } else {
1080                countUpTo = -1;
1081            }
1082        }
1083        return query(query, queryType, filter, limit, offset, countUpTo);
1084    }
1085
1086    protected long getMaxResults() {
1087        if (maxResults == null) {
1088            maxResults = Long.parseLong(Framework.getProperty(MAX_RESULTS_PROPERTY, DEFAULT_MAX_RESULTS));
1089        }
1090        return maxResults;
1091    }
1092
1093    protected boolean isLimitedResults() {
1094        if (limitedResults == null) {
1095            limitedResults = Boolean.parseBoolean(Framework.getProperty(LIMIT_RESULTS_PROPERTY));
1096        }
1097        return limitedResults;
1098    }
1099
1100    protected void setMaxResults(long maxResults) {
1101        this.maxResults = maxResults;
1102    }
1103
1104    protected void setLimitedResults(boolean limitedResults) {
1105        this.limitedResults = limitedResults;
1106    }
1107
1108    @Override
1109    public DocumentModelList query(String query, Filter filter, long limit, long offset, long countUpTo) {
1110        return query(query, NXQL.NXQL, filter, limit, offset, countUpTo);
1111    }
1112
1113    @Override
1114    public DocumentModelList query(String query, String queryType, Filter filter, long limit, long offset,
1115            long countUpTo) {
1116        SecurityService securityService = getSecurityService();
1117        Principal principal = getPrincipal();
1118        try {
1119            String permission = BROWSE;
1120            String repoName = getRepositoryName();
1121            boolean postFilterPolicies = !securityService.arePoliciesExpressibleInQuery(repoName);
1122            boolean postFilterFilter = filter != null && !(filter instanceof FacetFilter);
1123            boolean postFilter = postFilterPolicies || postFilterFilter;
1124            String[] principals;
1125            if (isAdministrator()) {
1126                principals = null; // means: no security check needed
1127            } else {
1128                principals = SecurityService.getPrincipalsToCheck(principal);
1129            }
1130            String[] permissions = securityService.getPermissionsToCheck(permission);
1131            QueryFilter queryFilter = new QueryFilter(principal, principals, permissions,
1132                    filter instanceof FacetFilter ? (FacetFilter) filter : null,
1133                    securityService.getPoliciesQueryTransformers(repoName), postFilter ? 0 : limit,
1134                    postFilter ? 0 : offset);
1135
1136            // get document list with total size
1137            PartialList<Document> pl = getSession().query(query, queryType, queryFilter, postFilter ? -1 : countUpTo);
1138            // convert to DocumentModelList
1139            DocumentModelListImpl dms = new DocumentModelListImpl(pl.list.size());
1140            dms.setTotalSize(pl.totalSize);
1141            for (Document doc : pl.list) {
1142                dms.add(readModel(doc));
1143            }
1144
1145            if (!postFilter) {
1146                // the backend has done all the needed filtering
1147                return dms;
1148            }
1149
1150            // post-filter the results "by hand", the backend couldn't do it
1151            long start = limit == 0 || offset < 0 ? 0 : offset;
1152            long stop = start + (limit == 0 ? dms.size() : limit);
1153            int n = 0;
1154            DocumentModelListImpl docs = new DocumentModelListImpl();
1155            for (DocumentModel model : dms) {
1156                if (postFilterPolicies) {
1157                    if (!hasPermission(model.getRef(), permission)) {
1158                        continue;
1159                    }
1160                }
1161                if (postFilterFilter) {
1162                    if (!filter.accept(model)) {
1163                        continue;
1164                    }
1165                }
1166                if (n < start) {
1167                    n++;
1168                    continue;
1169                }
1170                if (n >= stop) {
1171                    if (countUpTo == 0) {
1172                        // can break early
1173                        break;
1174                    }
1175                    n++;
1176                    continue;
1177                }
1178                n++;
1179                docs.add(model);
1180            }
1181            if (countUpTo != 0) {
1182                docs.setTotalSize(n);
1183            }
1184            return docs;
1185        } catch (QueryParseException e) {
1186            e.addInfo("Failed to execute query: " + query);
1187            throw e;
1188        }
1189    }
1190
1191    @Override
1192    public IterableQueryResult queryAndFetch(String query, String queryType, Object... params) {
1193        try {
1194            SecurityService securityService = getSecurityService();
1195            Principal principal = getPrincipal();
1196            String[] principals;
1197            if (isAdministrator()) {
1198                principals = null; // means: no security check needed
1199            } else {
1200                principals = SecurityService.getPrincipalsToCheck(principal);
1201            }
1202            String permission = BROWSE;
1203            String[] permissions = securityService.getPermissionsToCheck(permission);
1204            Collection<Transformer> transformers;
1205            if (NXQL.NXQL.equals(queryType)) {
1206                String repoName = getRepositoryName();
1207                transformers = securityService.getPoliciesQueryTransformers(repoName);
1208            } else {
1209                transformers = Collections.emptyList();
1210            }
1211            QueryFilter queryFilter = new QueryFilter(principal, principals, permissions, null, transformers, 0, 0);
1212            IterableQueryResult result = getSession().queryAndFetch(query, queryType, queryFilter, params);
1213            return result;
1214        } catch (QueryParseException e) {
1215            e.addInfo("Failed to execute query: " + queryType + ": " + query);
1216            throw e;
1217        }
1218    }
1219
1220    @Override
1221    public void removeChildren(DocumentRef docRef) {
1222        // TODO: check req permissions with td
1223        Document doc = resolveReference(docRef);
1224        checkPermission(doc, REMOVE_CHILDREN);
1225        List<Document> children = doc.getChildren();
1226        // remove proxies first, otherwise they could become dangling
1227        for (Document child : children) {
1228            if (child.isProxy()) {
1229                if (hasPermission(child, REMOVE)) {
1230                    removeNotifyOneDoc(child);
1231                }
1232            }
1233        }
1234        // then remove regular docs or versions, both of which could be proxies targets
1235        for (Document child : children) {
1236            if (!child.isProxy()) {
1237                if (hasPermission(child, REMOVE)) {
1238                    removeNotifyOneDoc(child);
1239                }
1240            }
1241        }
1242    }
1243
1244    @Override
1245    public boolean canRemoveDocument(DocumentRef docRef) {
1246        Document doc = resolveReference(docRef);
1247        return canRemoveDocument(doc) == null;
1248    }
1249
1250    /**
1251     * Checks if a document can be removed, and returns a failure reason if not.
1252     */
1253    protected String canRemoveDocument(Document doc) {
1254        // TODO must also check for proxies on live docs
1255        if (doc.isVersion()) {
1256            // TODO a hasProxies method would be more efficient
1257            Collection<Document> proxies = getSession().getProxies(doc, null);
1258            if (!proxies.isEmpty()) {
1259                return "Proxy " + proxies.iterator().next().getUUID() + " targets version " + doc.getUUID();
1260            }
1261            // find a working document to check security
1262            Document working = doc.getSourceDocument();
1263            if (working != null) {
1264                Document baseVersion = working.getBaseVersion();
1265                if (baseVersion != null && !baseVersion.isCheckedOut() && baseVersion.getUUID().equals(doc.getUUID())) {
1266                    return "Working copy " + working.getUUID() + " is checked in with base version " + doc.getUUID();
1267                }
1268                return hasPermission(working, WRITE_VERSION) ? null
1269                        : "Missing permission '" + WRITE_VERSION + "' on working copy " + working.getUUID();
1270            } else {
1271                // no working document, only admins can remove
1272                return isAdministrator() ? null : "No working copy and not an Administrator";
1273            }
1274        } else {
1275            if (isAdministrator()) {
1276                return null; // ok
1277            }
1278            if (!hasPermission(doc, REMOVE)) {
1279                return "Missing permission '" + REMOVE + "' on document " + doc.getUUID();
1280            }
1281            Document parent = doc.getParent();
1282            if (parent == null) {
1283                return null; // ok
1284            }
1285            return hasPermission(parent, REMOVE_CHILDREN) ? null
1286                    : "Missing permission '" + REMOVE_CHILDREN + "' on parent document " + parent.getUUID();
1287        }
1288    }
1289
1290    @Override
1291    public void removeDocument(DocumentRef docRef) {
1292        Document doc = resolveReference(docRef);
1293        removeDocument(doc);
1294    }
1295
1296    protected void removeDocument(Document doc) {
1297        try {
1298            String reason = canRemoveDocument(doc);
1299            if (reason != null) {
1300                throw new DocumentSecurityException(
1301                        "Permission denied: cannot remove document " + doc.getUUID() + ", " + reason);
1302            }
1303            removeNotifyOneDoc(doc);
1304
1305        } catch (ConcurrentUpdateException e) {
1306            e.addInfo("Failed to remove document " + doc.getUUID());
1307            throw e;
1308        }
1309        deleteDocumentCount.inc();
1310    }
1311
1312    protected void removeNotifyOneDoc(Document doc) {
1313        // XXX notify with options if needed
1314        DocumentModel docModel = readModel(doc);
1315        Map<String, Serializable> options = new HashMap<String, Serializable>();
1316        if (docModel != null) {
1317            options.put("docTitle", docModel.getTitle());
1318        }
1319        String versionLabel = "";
1320        Document sourceDoc = null;
1321        // notify different events depending on wether the document is a
1322        // version or not
1323        if (!doc.isVersion()) {
1324            notifyEvent(DocumentEventTypes.ABOUT_TO_REMOVE, docModel, options, null, null, true, true);
1325            CoreService coreService = Framework.getLocalService(CoreService.class);
1326            coreService.getVersionRemovalPolicy().removeVersions(getSession(), doc, this);
1327        } else {
1328            versionLabel = docModel.getVersionLabel();
1329            sourceDoc = doc.getSourceDocument();
1330            notifyEvent(DocumentEventTypes.ABOUT_TO_REMOVE_VERSION, docModel, options, null, null, true, true);
1331
1332        }
1333        doc.remove();
1334        if (doc.isVersion()) {
1335            if (sourceDoc != null) {
1336                DocumentModel sourceDocModel = readModel(sourceDoc);
1337                if (sourceDocModel != null) {
1338                    options.put("comment", versionLabel); // to be used by
1339                                                          // audit
1340                    // service
1341                    notifyEvent(DocumentEventTypes.VERSION_REMOVED, sourceDocModel, options, null, null, false, false);
1342                    options.remove("comment");
1343                }
1344                options.put("docSource", sourceDoc.getUUID());
1345            }
1346        }
1347        notifyEvent(DocumentEventTypes.DOCUMENT_REMOVED, docModel, options, null, null, false, false);
1348    }
1349
1350    /**
1351     * Implementation uses the fact that the lexicographic ordering of paths is a refinement of the "contains" partial
1352     * ordering.
1353     */
1354    @Override
1355    public void removeDocuments(DocumentRef[] docRefs) {
1356        Document[] docs = new Document[docRefs.length];
1357
1358        for (int i = 0; i < docs.length; i++) {
1359            docs[i] = resolveReference(docRefs[i]);
1360        }
1361        // TODO OPTIM: it's not guaranteed that getPath is cheap and
1362        // we call it a lot. Should use an object for pairs (document, path)
1363        // to call it just once per doc.
1364        Arrays.sort(docs, pathComparator);
1365        String[] paths = new String[docs.length];
1366        for (int i = 0; i < docs.length; i++) {
1367            paths[i] = docs[i].getPath();
1368        }
1369        String latestRemoved = null;
1370        for (int i = 0; i < docs.length; i++) {
1371            if (i == 0 || !paths[i].startsWith(latestRemoved + "/")) {
1372                removeDocument(docs[i]);
1373                latestRemoved = paths[i];
1374            }
1375        }
1376    }
1377
1378    @Override
1379    public void save() {
1380        try {
1381            final Map<String, Serializable> options = new HashMap<String, Serializable>();
1382            getSession().save();
1383            notifyEvent(DocumentEventTypes.SESSION_SAVED, null, options, null, null, true, false);
1384        } catch (ConcurrentUpdateException e) {
1385            e.addInfo("Failed to save session");
1386            throw e;
1387        }
1388    }
1389
1390    @Override
1391    public DocumentModel saveDocument(DocumentModel docModel) {
1392        if (docModel.getRef() == null) {
1393            throw new IllegalArgumentException(String.format(
1394                    "cannot save document '%s' with null reference: " + "document has probably not yet been created "
1395                            + "in the repository with " + "'CoreSession.createDocument(docModel)'",
1396                    docModel.getTitle()));
1397        }
1398        Document doc = resolveReference(docModel.getRef());
1399        checkPermission(doc, WRITE_PROPERTIES);
1400
1401        Map<String, Serializable> options = getContextMapEventInfo(docModel);
1402
1403        boolean dirty = docModel.isDirty();
1404
1405        // document validation
1406        if (dirty && getValidationService().isActivated(DocumentValidationService.CTX_SAVEDOC, options)) {
1407            DocumentValidationReport report = getValidationService().validate(docModel, true);
1408            if (report.hasError()) {
1409                throw new DocumentValidationException(report);
1410            }
1411        }
1412
1413        options.put(CoreEventConstants.PREVIOUS_DOCUMENT_MODEL, readModel(doc));
1414        // regular event, last chance to modify docModel
1415        options.put(CoreEventConstants.DESTINATION_NAME, docModel.getName());
1416        options.put(CoreEventConstants.DOCUMENT_DIRTY, dirty);
1417        notifyEvent(DocumentEventTypes.BEFORE_DOC_UPDATE, docModel, options, null, null, true, true);
1418        String name = (String) options.get(CoreEventConstants.DESTINATION_NAME);
1419        // did the event change the name? not applicable to Root whose
1420        // name is null/empty
1421        if (name != null && !name.equals(docModel.getName())) {
1422            doc = getSession().move(doc, doc.getParent(), name);
1423        }
1424
1425        VersioningOption versioningOption = (VersioningOption) docModel.getContextData(
1426                VersioningService.VERSIONING_OPTION);
1427        docModel.putContextData(VersioningService.VERSIONING_OPTION, null);
1428        String checkinComment = (String) docModel.getContextData(VersioningService.CHECKIN_COMMENT);
1429        docModel.putContextData(VersioningService.CHECKIN_COMMENT, null);
1430        Boolean disableAutoCheckOut = (Boolean) docModel.getContextData(VersioningService.DISABLE_AUTO_CHECKOUT);
1431        docModel.putContextData(VersioningService.DISABLE_AUTO_CHECKOUT, null);
1432        options.put(VersioningService.DISABLE_AUTO_CHECKOUT, disableAutoCheckOut);
1433        // compat
1434        boolean snapshot = Boolean.TRUE.equals(
1435                docModel.getContextData(ScopeType.REQUEST, VersioningDocument.CREATE_SNAPSHOT_ON_SAVE_KEY));
1436        docModel.putContextData(ScopeType.REQUEST, VersioningDocument.CREATE_SNAPSHOT_ON_SAVE_KEY, null);
1437        if (versioningOption == null && snapshot && dirty) {
1438            String key = String.valueOf(
1439                    docModel.getContextData(ScopeType.REQUEST, VersioningDocument.KEY_FOR_INC_OPTION));
1440            docModel.putContextData(ScopeType.REQUEST, VersioningDocument.KEY_FOR_INC_OPTION, null);
1441            versioningOption = "inc_major".equals(key) ? VersioningOption.MAJOR : VersioningOption.MINOR;
1442        }
1443
1444        if (!docModel.isImmutable()) {
1445            // pre-save versioning
1446            boolean checkout = getVersioningService().isPreSaveDoingCheckOut(doc, dirty, versioningOption, options);
1447            if (checkout) {
1448                notifyEvent(DocumentEventTypes.ABOUT_TO_CHECKOUT, docModel, options, null, null, true, true);
1449            }
1450            versioningOption = getVersioningService().doPreSave(doc, dirty, versioningOption, checkinComment, options);
1451            if (checkout) {
1452                DocumentModel checkedOutDoc = readModel(doc);
1453                notifyEvent(DocumentEventTypes.DOCUMENT_CHECKEDOUT, checkedOutDoc, options, null, null, true, false);
1454            }
1455        }
1456
1457        boolean allowVersionWrite = Boolean.TRUE.equals(docModel.getContextData(ALLOW_VERSION_WRITE));
1458        docModel.putContextData(ALLOW_VERSION_WRITE, null);
1459        boolean setReadWrite = allowVersionWrite && doc.isVersion() && doc.isReadOnly();
1460
1461        // actual save
1462        if (setReadWrite) {
1463            doc.setReadOnly(false);
1464        }
1465        docModel = writeModel(doc, docModel);
1466        if (setReadWrite) {
1467            doc.setReadOnly(true);
1468        }
1469
1470        Document checkedInDoc = null;
1471        if (!docModel.isImmutable()) {
1472            // post-save versioning
1473            boolean checkin = getVersioningService().isPostSaveDoingCheckIn(doc, versioningOption, options);
1474            if (checkin) {
1475                notifyEvent(DocumentEventTypes.ABOUT_TO_CHECKIN, docModel, options, null, null, true, true);
1476            }
1477            checkedInDoc = getVersioningService().doPostSave(doc, versioningOption, checkinComment, options);
1478        }
1479
1480        // post-save events
1481        docModel = readModel(doc);
1482        if (checkedInDoc != null) {
1483            DocumentRef checkedInVersionRef = new IdRef(checkedInDoc.getUUID());
1484            notifyCheckedInVersion(docModel, checkedInVersionRef, options, checkinComment);
1485        }
1486        notifyEvent(DocumentEventTypes.DOCUMENT_UPDATED, docModel, options, null, null, true, false);
1487        updateDocumentCount.inc();
1488        return docModel;
1489    }
1490
1491    @Override
1492    @Deprecated
1493    public boolean isDirty(DocumentRef docRef) {
1494        return resolveReference(docRef).isCheckedOut();
1495    }
1496
1497    @Override
1498    public void saveDocuments(DocumentModel[] docModels) {
1499        // TODO: optimize this - avoid calling at each iteration saveDoc...
1500        for (DocumentModel docModel : docModels) {
1501            saveDocument(docModel);
1502        }
1503    }
1504
1505    @Override
1506    public DocumentModel getSourceDocument(DocumentRef docRef) {
1507        assert null != docRef;
1508
1509        Document doc = resolveReference(docRef);
1510        checkPermission(doc, READ_VERSION);
1511        Document headDocument = doc.getSourceDocument();
1512        if (headDocument == null) {
1513            throw new DocumentNotFoundException("Source document has been deleted");
1514        }
1515        return readModel(headDocument);
1516    }
1517
1518    protected VersionModel getVersionModel(Document version) {
1519        VersionModel versionModel = new VersionModelImpl();
1520        versionModel.setId(version.getUUID());
1521        versionModel.setCreated(version.getVersionCreationDate());
1522        versionModel.setDescription(version.getCheckinComment());
1523        versionModel.setLabel(version.getVersionLabel());
1524        return versionModel;
1525    }
1526
1527    @Override
1528    public VersionModel getLastVersion(DocumentRef docRef) {
1529        Document doc = resolveReference(docRef);
1530        checkPermission(doc, READ_VERSION);
1531        Document version = doc.getLastVersion();
1532        return version == null ? null : getVersionModel(version);
1533    }
1534
1535    @Override
1536    public DocumentModel getLastDocumentVersion(DocumentRef docRef) {
1537        Document doc = resolveReference(docRef);
1538        checkPermission(doc, READ_VERSION);
1539        Document version = doc.getLastVersion();
1540        return version == null ? null : readModel(version);
1541    }
1542
1543    @Override
1544    public DocumentRef getLastDocumentVersionRef(DocumentRef docRef) {
1545        Document doc = resolveReference(docRef);
1546        checkPermission(doc, READ_VERSION);
1547        Document version = doc.getLastVersion();
1548        return version == null ? null : new IdRef(version.getUUID());
1549    }
1550
1551    @Override
1552    public List<DocumentRef> getVersionsRefs(DocumentRef docRef) {
1553        Document doc = resolveReference(docRef);
1554        checkPermission(doc, READ_VERSION);
1555        List<String> ids = doc.getVersionsIds();
1556        List<DocumentRef> refs = new ArrayList<DocumentRef>(ids.size());
1557        for (String id : ids) {
1558            refs.add(new IdRef(id));
1559        }
1560        return refs;
1561    }
1562
1563    @Override
1564    public List<DocumentModel> getVersions(DocumentRef docRef) {
1565        Document doc = resolveReference(docRef);
1566        checkPermission(doc, READ_VERSION);
1567        List<Document> docVersions = doc.getVersions();
1568        List<DocumentModel> versions = new ArrayList<DocumentModel>(docVersions.size());
1569        for (Document version : docVersions) {
1570            versions.add(readModel(version));
1571        }
1572        return versions;
1573    }
1574
1575    @Override
1576    public List<VersionModel> getVersionsForDocument(DocumentRef docRef) {
1577        Document doc = resolveReference(docRef);
1578        checkPermission(doc, READ_VERSION);
1579        List<Document> docVersions = doc.getVersions();
1580        List<VersionModel> versions = new ArrayList<VersionModel>(docVersions.size());
1581        for (Document version : docVersions) {
1582            versions.add(getVersionModel(version));
1583        }
1584        return versions;
1585
1586    }
1587
1588    @Override
1589    public DocumentModel restoreToVersion(DocumentRef docRef, DocumentRef versionRef) {
1590        Document doc = resolveReference(docRef);
1591        Document ver = resolveReference(versionRef);
1592        return restoreToVersion(doc, ver, false, true);
1593    }
1594
1595    @Override
1596    @Deprecated
1597    public DocumentModel restoreToVersion(DocumentRef docRef, VersionModel version) {
1598        return restoreToVersion(docRef, version, false);
1599    }
1600
1601    @Override
1602    @Deprecated
1603    public DocumentModel restoreToVersion(DocumentRef docRef, VersionModel version, boolean skipSnapshotCreation) {
1604        Document doc = resolveReference(docRef);
1605        Document ver = doc.getVersion(version.getLabel());
1606        return restoreToVersion(doc, ver, skipSnapshotCreation, false);
1607    }
1608
1609    @Override
1610    public DocumentModel restoreToVersion(DocumentRef docRef, DocumentRef versionRef, boolean skipSnapshotCreation,
1611            boolean skipCheckout) {
1612        Document doc = resolveReference(docRef);
1613        Document ver = resolveReference(versionRef);
1614        return restoreToVersion(doc, ver, skipSnapshotCreation, skipCheckout);
1615    }
1616
1617    protected DocumentModel restoreToVersion(Document doc, Document version, boolean skipSnapshotCreation,
1618            boolean skipCheckout) {
1619        checkPermission(doc, WRITE_VERSION);
1620
1621        DocumentModel docModel = readModel(doc);
1622
1623        Map<String, Serializable> options = new HashMap<String, Serializable>();
1624
1625        // we're about to overwrite the document, make sure it's archived
1626        if (!skipSnapshotCreation && doc.isCheckedOut()) {
1627            String checkinComment = (String) docModel.getContextData(VersioningService.CHECKIN_COMMENT);
1628            docModel.putContextData(VersioningService.CHECKIN_COMMENT, null);
1629            notifyEvent(DocumentEventTypes.ABOUT_TO_CHECKIN, docModel, options, null, null, true, true);
1630            Document ver = getVersioningService().doCheckIn(doc, null, checkinComment);
1631            docModel.refresh(DocumentModel.REFRESH_STATE, null);
1632            notifyCheckedInVersion(docModel, new IdRef(ver.getUUID()), null, checkinComment);
1633        }
1634
1635        // FIXME: the fields are hardcoded. should be moved in versioning
1636        // component
1637        // HOW?
1638        final Long majorVer = (Long) doc.getPropertyValue("major_version");
1639        final Long minorVer = (Long) doc.getPropertyValue("minor_version");
1640        if (majorVer != null || minorVer != null) {
1641            options.put(VersioningDocument.CURRENT_DOCUMENT_MAJOR_VERSION_KEY, majorVer);
1642            options.put(VersioningDocument.CURRENT_DOCUMENT_MINOR_VERSION_KEY, minorVer);
1643        }
1644        // add the uuid of the version being restored
1645        String versionUUID = version.getUUID();
1646        options.put(VersioningDocument.RESTORED_VERSION_UUID_KEY, versionUUID);
1647
1648        notifyEvent(DocumentEventTypes.BEFORE_DOC_RESTORE, docModel, options, null, null, true, true);
1649        writeModel(doc, docModel);
1650
1651        doc.restore(version);
1652        // re-read doc model after restoration
1653        docModel = readModel(doc);
1654        notifyEvent(DocumentEventTypes.DOCUMENT_RESTORED, docModel, options, null, docModel.getVersionLabel(), true,
1655                false);
1656        docModel = writeModel(doc, docModel);
1657
1658        if (!skipCheckout) {
1659            // restore gives us a checked in document, so do a checkout
1660            notifyEvent(DocumentEventTypes.ABOUT_TO_CHECKOUT, docModel, options, null, null, true, true);
1661            getVersioningService().doCheckOut(doc);
1662            docModel = readModel(doc);
1663            notifyEvent(DocumentEventTypes.DOCUMENT_CHECKEDOUT, docModel, options, null, null, true, false);
1664        }
1665
1666        log.debug("Document restored to version:" + version.getUUID());
1667        return docModel;
1668    }
1669
1670    @Override
1671    public DocumentRef getBaseVersion(DocumentRef docRef) {
1672        Document doc = resolveReference(docRef);
1673        checkPermission(doc, READ);
1674        Document ver = doc.getBaseVersion();
1675        if (ver == null) {
1676            return null;
1677        }
1678        checkPermission(ver, READ);
1679        return new IdRef(ver.getUUID());
1680    }
1681
1682    @Override
1683    @Deprecated
1684    public DocumentModel checkIn(DocumentRef docRef, VersionModel ver) {
1685        DocumentRef verRef = checkIn(docRef, VersioningOption.MINOR, ver == null ? null : ver.getDescription());
1686        return readModel(resolveReference(verRef));
1687    }
1688
1689    @Override
1690    public DocumentRef checkIn(DocumentRef docRef, VersioningOption option, String checkinComment) {
1691        Document doc = resolveReference(docRef);
1692        checkPermission(doc, WRITE_PROPERTIES);
1693        DocumentModel docModel = readModel(doc);
1694
1695        Map<String, Serializable> options = new HashMap<String, Serializable>();
1696        notifyEvent(DocumentEventTypes.ABOUT_TO_CHECKIN, docModel, options, null, null, true, true);
1697        writeModel(doc, docModel);
1698
1699        Document version = getVersioningService().doCheckIn(doc, option, checkinComment);
1700
1701        docModel = readModel(doc);
1702        DocumentRef checkedInVersionRef = new IdRef(version.getUUID());
1703        notifyCheckedInVersion(docModel, checkedInVersionRef, options, checkinComment);
1704        writeModel(doc, docModel);
1705
1706        return checkedInVersionRef;
1707    }
1708
1709    /**
1710     * Send a core event for the creation of a new check in version. The source document is the live document model used
1711     * as the source for the checkin, not the archived version it-self.
1712     *
1713     * @param docModel work document that has been checked-in as a version
1714     * @param checkedInVersionRef document ref of the new checked-in version
1715     * @param options initial option map, or null
1716     * @param checkinComment
1717     */
1718    protected void notifyCheckedInVersion(DocumentModel docModel, DocumentRef checkedInVersionRef,
1719            Map<String, Serializable> options, String checkinComment) {
1720        String label = getVersioningService().getVersionLabel(docModel);
1721        Map<String, Serializable> props = new HashMap<String, Serializable>();
1722        if (options != null) {
1723            props.putAll(options);
1724        }
1725        props.put("versionLabel", label);
1726        props.put("checkInComment", checkinComment);
1727        props.put("checkedInVersionRef", checkedInVersionRef);
1728        if (checkinComment == null && options != null) {
1729            // check if there's a comment already in options
1730            Object optionsComment = options.get("comment");
1731            if (optionsComment instanceof String) {
1732                checkinComment = (String) optionsComment;
1733            }
1734        }
1735        String comment = checkinComment == null ? label : label + ' ' + checkinComment;
1736        props.put("comment", comment); // compat, used in audit
1737        // notify checkin on live document
1738        notifyEvent(DocumentEventTypes.DOCUMENT_CHECKEDIN, docModel, props, null, null, true, false);
1739        // notify creation on version document
1740        notifyEvent(DocumentEventTypes.DOCUMENT_CREATED, getDocument(checkedInVersionRef), props, null, null, true,
1741                false);
1742
1743    }
1744
1745    @Override
1746    public void checkOut(DocumentRef docRef) {
1747        Document doc = resolveReference(docRef);
1748        // TODO: add a new permission names CHECKOUT and use it instead of
1749        // WRITE_PROPERTIES
1750        checkPermission(doc, WRITE_PROPERTIES);
1751        DocumentModel docModel = readModel(doc);
1752        Map<String, Serializable> options = new HashMap<String, Serializable>();
1753
1754        notifyEvent(DocumentEventTypes.ABOUT_TO_CHECKOUT, docModel, options, null, null, true, true);
1755
1756        getVersioningService().doCheckOut(doc);
1757        docModel = readModel(doc);
1758
1759        notifyEvent(DocumentEventTypes.DOCUMENT_CHECKEDOUT, docModel, options, null, null, true, false);
1760        writeModel(doc, docModel);
1761    }
1762
1763    @Override
1764    public boolean isCheckedOut(DocumentRef docRef) {
1765        assert null != docRef;
1766        Document doc = resolveReference(docRef);
1767        checkPermission(doc, BROWSE);
1768        return doc.isCheckedOut();
1769    }
1770
1771    @Override
1772    public String getVersionSeriesId(DocumentRef docRef) {
1773        Document doc = resolveReference(docRef);
1774        checkPermission(doc, READ);
1775        return doc.getVersionSeriesId();
1776    }
1777
1778    @Override
1779    public DocumentModel getWorkingCopy(DocumentRef docRef) {
1780        Document doc = resolveReference(docRef);
1781        checkPermission(doc, READ_VERSION);
1782        Document pwc = doc.getWorkingCopy();
1783        checkPermission(pwc, READ);
1784        return pwc == null ? null : readModel(pwc);
1785    }
1786
1787    @Override
1788    public DocumentModel getVersion(String versionableId, VersionModel versionModel) {
1789        String id = versionModel.getId();
1790        if (id != null) {
1791            return getDocument(new IdRef(id));
1792        }
1793        Document doc = getSession().getVersion(versionableId, versionModel);
1794        if (doc == null) {
1795            return null;
1796        }
1797        checkPermission(doc, READ_PROPERTIES);
1798        checkPermission(doc, READ_VERSION);
1799        return readModel(doc);
1800    }
1801
1802    @Override
1803    public String getVersionLabel(DocumentModel docModel) {
1804        return getVersioningService().getVersionLabel(docModel);
1805    }
1806
1807    @Override
1808    public DocumentModel getDocumentWithVersion(DocumentRef docRef, VersionModel version) {
1809        String id = version.getId();
1810        if (id != null) {
1811            return getDocument(new IdRef(id));
1812        }
1813        Document doc = resolveReference(docRef);
1814        checkPermission(doc, READ_PROPERTIES);
1815        checkPermission(doc, READ_VERSION);
1816        String docPath = doc.getPath();
1817        doc = doc.getVersion(version.getLabel());
1818        if (doc == null) {
1819            // SQL Storage uses to return null if version not found
1820            log.debug("Version " + version.getLabel() + " does not exist for " + docPath);
1821            return null;
1822        }
1823        log.debug("Retrieved the version " + version.getLabel() + " of the document " + docPath);
1824        return readModel(doc);
1825    }
1826
1827    @Override
1828    public DocumentModel createProxy(DocumentRef docRef, DocumentRef folderRef) {
1829        Document doc = resolveReference(docRef);
1830        Document fold = resolveReference(folderRef);
1831        checkPermission(doc, READ);
1832        checkPermission(fold, ADD_CHILDREN);
1833        return createProxyInternal(doc, fold, new HashMap<String, Serializable>());
1834    }
1835
1836    protected DocumentModel createProxyInternal(Document doc, Document folder, Map<String, Serializable> options) {
1837        // create the new proxy
1838        Document proxy = getSession().createProxy(doc, folder);
1839        DocumentModel proxyModel = readModel(proxy);
1840
1841        notifyEvent(DocumentEventTypes.DOCUMENT_CREATED, proxyModel, options, null, null, true, false);
1842        notifyEvent(DocumentEventTypes.DOCUMENT_PROXY_PUBLISHED, proxyModel, options, null, null, true, false);
1843        DocumentModel folderModel = readModel(folder);
1844        notifyEvent(DocumentEventTypes.SECTION_CONTENT_PUBLISHED, folderModel, options, null, null, true, false);
1845        return proxyModel;
1846    }
1847
1848    /**
1849     * Remove proxies for the same base document in the folder. doc may be a normal document or a proxy.
1850     */
1851    protected List<String> removeExistingProxies(Document doc, Document folder) {
1852        Collection<Document> otherProxies = getSession().getProxies(doc, folder);
1853        List<String> removedProxyIds = new ArrayList<String>(otherProxies.size());
1854        for (Document otherProxy : otherProxies) {
1855            removedProxyIds.add(otherProxy.getUUID());
1856            removeNotifyOneDoc(otherProxy);
1857        }
1858        return removedProxyIds;
1859    }
1860
1861    /**
1862     * Update the proxy for doc in the given section to point to the new target. Do nothing if there are several
1863     * proxies.
1864     *
1865     * @return the proxy if it was updated, or {@code null} if none or several were found
1866     */
1867    protected DocumentModel updateExistingProxies(Document doc, Document folder, Document target) {
1868        Collection<Document> proxies = getSession().getProxies(doc, folder);
1869        try {
1870            if (proxies.size() == 1) {
1871                for (Document proxy : proxies) {
1872                    proxy.setTargetDocument(target);
1873                    return readModel(proxy);
1874                }
1875            }
1876        } catch (UnsupportedOperationException e) {
1877            log.error("Cannot update proxy, try to remove");
1878        }
1879        return null;
1880    }
1881
1882    @Override
1883    public DocumentModelList getProxies(DocumentRef docRef, DocumentRef folderRef) {
1884        Document folder = null;
1885        if (folderRef != null) {
1886            folder = resolveReference(folderRef);
1887            checkPermission(folder, READ_CHILDREN);
1888        }
1889        Document doc = resolveReference(docRef);
1890        Collection<Document> children = getSession().getProxies(doc, folder);
1891        DocumentModelList docs = new DocumentModelListImpl();
1892        for (Document child : children) {
1893            if (hasPermission(child, READ)) {
1894                docs.add(readModel(child));
1895            }
1896        }
1897        return docs;
1898    }
1899
1900    @Override
1901    public String[] getProxyVersions(DocumentRef docRef, DocumentRef folderRef) {
1902        Document folder = resolveReference(folderRef);
1903        Document doc = resolveReference(docRef);
1904        checkPermission(folder, READ_CHILDREN);
1905        Collection<Document> children = getSession().getProxies(doc, folder);
1906        if (children.isEmpty()) {
1907            return null;
1908        }
1909        List<String> versions = new ArrayList<String>();
1910        for (Document child : children) {
1911            if (hasPermission(child, READ)) {
1912                Document target = child.getTargetDocument();
1913                if (target.isVersion()) {
1914                    versions.add(target.getVersionLabel());
1915                } else {
1916                    // live proxy
1917                    versions.add("");
1918                }
1919            }
1920        }
1921        return versions.toArray(new String[versions.size()]);
1922    }
1923
1924    @Override
1925    public List<String> getAvailableSecurityPermissions() {
1926        // XXX: add security check?
1927        return Arrays.asList(getSecurityService().getPermissionProvider().getPermissions());
1928    }
1929
1930    @Override
1931    public DataModel getDataModel(DocumentRef docRef, Schema schema) {
1932        Document doc = resolveReference(docRef);
1933        checkPermission(doc, READ);
1934        return DocumentModelFactory.createDataModel(doc, schema);
1935    }
1936
1937    protected Object getDataModelField(DocumentRef docRef, String schema, String field) {
1938        Document doc = resolveReference(docRef);
1939        if (doc != null) {
1940            checkPermission(doc, READ);
1941            Schema docSchema = doc.getType().getSchema(schema);
1942            if (docSchema != null) {
1943                String prefix = docSchema.getNamespace().prefix;
1944                if (prefix != null && prefix.length() > 0) {
1945                    field = prefix + ':' + field;
1946                }
1947                return doc.getPropertyValue(field);
1948            } else {
1949                log.warn("Cannot find schema with name=" + schema);
1950            }
1951        } else {
1952            log.warn("Cannot resolve docRef=" + docRef);
1953        }
1954        return null;
1955    }
1956
1957    @Override
1958    public String getCurrentLifeCycleState(DocumentRef docRef) {
1959        Document doc = resolveReference(docRef);
1960        checkPermission(doc, READ_LIFE_CYCLE);
1961        return doc.getLifeCycleState();
1962    }
1963
1964    @Override
1965    public String getLifeCyclePolicy(DocumentRef docRef) {
1966        Document doc = resolveReference(docRef);
1967        checkPermission(doc, READ_LIFE_CYCLE);
1968        return doc.getLifeCyclePolicy();
1969    }
1970
1971    /**
1972     * Make a document follow a transition.
1973     *
1974     * @param docRef a {@link DocumentRef}
1975     * @param transition the transition to follow
1976     * @param options an option map than can be used by callers to pass additional params
1977     * @return
1978     * @since 5.9.3
1979     */
1980    private boolean followTransition(DocumentRef docRef, String transition, ScopedMap options)
1981            throws LifeCycleException {
1982        Document doc = resolveReference(docRef);
1983        checkPermission(doc, WRITE_LIFE_CYCLE);
1984
1985        if (!doc.isVersion() && !doc.isProxy() && !doc.isCheckedOut()) {
1986            checkOut(docRef);
1987            doc = resolveReference(docRef);
1988        }
1989        String formerStateName = doc.getLifeCycleState();
1990        doc.followTransition(transition);
1991
1992        // Construct a map holding meta information about the event.
1993        Map<String, Serializable> eventOptions = new HashMap<String, Serializable>();
1994        eventOptions.put(org.nuxeo.ecm.core.api.LifeCycleConstants.TRANSTION_EVENT_OPTION_FROM, formerStateName);
1995        eventOptions.put(org.nuxeo.ecm.core.api.LifeCycleConstants.TRANSTION_EVENT_OPTION_TO, doc.getLifeCycleState());
1996        eventOptions.put(org.nuxeo.ecm.core.api.LifeCycleConstants.TRANSTION_EVENT_OPTION_TRANSITION, transition);
1997        String comment = (String) options.getScopedValue("comment");
1998        DocumentModel docModel = readModel(doc);
1999        notifyEvent(org.nuxeo.ecm.core.api.LifeCycleConstants.TRANSITION_EVENT, docModel, eventOptions,
2000                DocumentEventCategories.EVENT_LIFE_CYCLE_CATEGORY, comment, true, false);
2001        if (!docModel.isImmutable()) {
2002            writeModel(doc, docModel);
2003        }
2004        return true; // throws if error
2005    }
2006
2007    @Override
2008    public boolean followTransition(DocumentModel docModel, String transition) throws LifeCycleException {
2009        return followTransition(docModel.getRef(), transition, docModel.getContextData());
2010    }
2011
2012    @Override
2013    public boolean followTransition(DocumentRef docRef, String transition) throws LifeCycleException {
2014        return followTransition(docRef, transition, new ScopedMap());
2015    }
2016
2017    @Override
2018    public Collection<String> getAllowedStateTransitions(DocumentRef docRef) {
2019        Document doc = resolveReference(docRef);
2020        checkPermission(doc, READ_LIFE_CYCLE);
2021        return doc.getAllowedStateTransitions();
2022    }
2023
2024    @Override
2025    public void reinitLifeCycleState(DocumentRef docRef) {
2026        Document doc = resolveReference(docRef);
2027        checkPermission(doc, WRITE_LIFE_CYCLE);
2028        LifeCycleService service = NXCore.getLifeCycleService();
2029        service.reinitLifeCycle(doc);
2030    }
2031
2032    @Override
2033    public Object[] getDataModelsField(DocumentRef[] docRefs, String schema, String field) {
2034
2035        assert docRefs != null;
2036        assert schema != null;
2037        assert field != null;
2038
2039        final Object[] values = new Object[docRefs.length];
2040        int i = 0;
2041        for (DocumentRef docRef : docRefs) {
2042            final Object value = getDataModelField(docRef, schema, field);
2043            values[i++] = value;
2044        }
2045
2046        return values;
2047    }
2048
2049    @Override
2050    public DocumentRef[] getParentDocumentRefs(DocumentRef docRef) {
2051        final List<DocumentRef> docRefs = new ArrayList<DocumentRef>();
2052        final Document doc = resolveReference(docRef);
2053        Document parentDoc = doc.getParent();
2054        while (parentDoc != null) {
2055            final DocumentRef parentDocRef = new IdRef(parentDoc.getUUID());
2056            docRefs.add(parentDocRef);
2057            parentDoc = parentDoc.getParent();
2058        }
2059        DocumentRef[] refs = new DocumentRef[docRefs.size()];
2060        return docRefs.toArray(refs);
2061    }
2062
2063    @Override
2064    public Object[] getDataModelsFieldUp(DocumentRef docRef, String schema, String field) {
2065
2066        final DocumentRef[] parentRefs = getParentDocumentRefs(docRef);
2067        final DocumentRef[] allRefs = new DocumentRef[parentRefs.length + 1];
2068        allRefs[0] = docRef;
2069        System.arraycopy(parentRefs, 0, allRefs, 1, parentRefs.length);
2070
2071        return getDataModelsField(allRefs, schema, field);
2072    }
2073
2074    protected String oldLockKey(Lock lock) {
2075        if (lock == null) {
2076            return null;
2077        }
2078        // return deprecated format, like "someuser:Nov 29, 2010"
2079        String lockCreationDate = (lock.getCreated() == null) ? null
2080                : DateFormat.getDateInstance(DateFormat.MEDIUM).format(new Date(lock.getCreated().getTimeInMillis()));
2081        return lock.getOwner() + ':' + lockCreationDate;
2082    }
2083
2084    @Override
2085    @Deprecated
2086    public String getLock(DocumentRef docRef) {
2087        Lock lock = getLockInfo(docRef);
2088        return oldLockKey(lock);
2089    }
2090
2091    @Override
2092    @Deprecated
2093    public void setLock(DocumentRef docRef, String key) {
2094        setLock(docRef);
2095    }
2096
2097    @Override
2098    @Deprecated
2099    public String unlock(DocumentRef docRef) {
2100        Lock lock = removeLock(docRef);
2101        return oldLockKey(lock);
2102    }
2103
2104    @Override
2105    public Lock setLock(DocumentRef docRef) throws LockException {
2106        Document doc = resolveReference(docRef);
2107        // TODO: add a new permission named LOCK and use it instead of
2108        // WRITE_PROPERTIES
2109        checkPermission(doc, WRITE_PROPERTIES);
2110        Lock lock = new Lock(getPrincipal().getName(), new GregorianCalendar());
2111        Lock oldLock = doc.setLock(lock);
2112        if (oldLock != null) {
2113            throw new LockException("Document already locked by " + oldLock.getOwner() + ": " + docRef);
2114        }
2115        DocumentModel docModel = readModel(doc);
2116        Map<String, Serializable> options = new HashMap<String, Serializable>();
2117        options.put("lock", lock);
2118        notifyEvent(DocumentEventTypes.DOCUMENT_LOCKED, docModel, options, null, null, true, false);
2119        return lock;
2120    }
2121
2122    @Override
2123    public Lock getLockInfo(DocumentRef docRef) {
2124        Document doc = resolveReference(docRef);
2125        checkPermission(doc, READ);
2126        return doc.getLock();
2127    }
2128
2129    @Override
2130    public Lock removeLock(DocumentRef docRef) throws LockException {
2131        Document doc = resolveReference(docRef);
2132        String owner;
2133        if (hasPermission(docRef, UNLOCK)) {
2134            // always unlock
2135            owner = null;
2136        } else {
2137            owner = getPrincipal().getName();
2138        }
2139        Lock lock = doc.removeLock(owner);
2140        if (lock == null) {
2141            // there was no lock, we're done
2142            return null;
2143        }
2144        if (lock.getFailed()) {
2145            // lock removal failed due to owner check
2146            throw new LockException("Document already locked by " + lock.getOwner() + ": " + docRef);
2147        }
2148        DocumentModel docModel = readModel(doc);
2149        Map<String, Serializable> options = new HashMap<String, Serializable>();
2150        options.put("lock", lock);
2151        notifyEvent(DocumentEventTypes.DOCUMENT_UNLOCKED, docModel, options, null, null, true, false);
2152        return lock;
2153    }
2154
2155    protected boolean isAdministrator() {
2156        Principal principal = getPrincipal();
2157        // FIXME: this is inconsistent with NuxeoPrincipal#isAdministrator
2158        // method because it allows hardcoded Administrator user
2159        if (Framework.isTestModeSet()) {
2160            if (SecurityConstants.ADMINISTRATOR.equals(principal.getName())) {
2161                return true;
2162            }
2163        }
2164        if (SYSTEM_USERNAME.equals(principal.getName())) {
2165            return true;
2166        }
2167        if (principal instanceof NuxeoPrincipal) {
2168            return ((NuxeoPrincipal) principal).isAdministrator();
2169        }
2170        return false;
2171    }
2172
2173    @Override
2174    public void applyDefaultPermissions(String userOrGroupName) {
2175        if (userOrGroupName == null) {
2176            throw new NullPointerException("null userOrGroupName");
2177        }
2178        if (!isAdministrator()) {
2179            throw new DocumentSecurityException("You need to be an Administrator to do this.");
2180        }
2181        DocumentModel rootDocument = getRootDocument();
2182        ACP acp = new ACPImpl();
2183
2184        UserEntry userEntry = new UserEntryImpl(userOrGroupName);
2185        userEntry.addPrivilege(READ);
2186
2187        acp.setRules(new UserEntry[] { userEntry });
2188
2189        setACP(rootDocument.getRef(), acp, false);
2190    }
2191
2192    @Override
2193    public DocumentModel publishDocument(DocumentModel docToPublish, DocumentModel section) {
2194        return publishDocument(docToPublish, section, true);
2195    }
2196
2197    @Override
2198    public DocumentModel publishDocument(DocumentModel docModel, DocumentModel section,
2199            boolean overwriteExistingProxy) {
2200        Document doc = resolveReference(docModel.getRef());
2201        Document sec = resolveReference(section.getRef());
2202        checkPermission(doc, READ);
2203        checkPermission(sec, ADD_CHILDREN);
2204
2205        Map<String, Serializable> options = new HashMap<String, Serializable>();
2206        DocumentModel proxy = null;
2207        Document target;
2208        if (docModel.isProxy() || docModel.isVersion()) {
2209            target = doc;
2210            if (overwriteExistingProxy) {
2211                if (docModel.isVersion()) {
2212                    Document base = resolveReference(new IdRef(doc.getVersionSeriesId()));
2213                    proxy = updateExistingProxies(base, sec, target);
2214                }
2215                if (proxy == null) {
2216                    // remove previous
2217                    List<String> removedProxyIds = removeExistingProxies(doc, sec);
2218                    options.put(CoreEventConstants.REPLACED_PROXY_IDS, (Serializable) removedProxyIds);
2219                }
2220            }
2221
2222        } else {
2223            String checkinComment = (String) docModel.getContextData(VersioningService.CHECKIN_COMMENT);
2224            docModel.putContextData(VersioningService.CHECKIN_COMMENT, null);
2225            if (doc.isCheckedOut() || doc.getLastVersion() == null) {
2226                if (!doc.isCheckedOut()) {
2227                    // last version was deleted while leaving a checked in
2228                    // doc. recreate a version
2229                    notifyEvent(DocumentEventTypes.ABOUT_TO_CHECKOUT, docModel, options, null, null, true, true);
2230                    getVersioningService().doCheckOut(doc);
2231                    docModel = readModel(doc);
2232                    notifyEvent(DocumentEventTypes.DOCUMENT_CHECKEDOUT, docModel, options, null, null, true, false);
2233                }
2234                notifyEvent(DocumentEventTypes.ABOUT_TO_CHECKIN, docModel, options, null, null, true, true);
2235                Document version = getVersioningService().doCheckIn(doc, null, checkinComment);
2236                docModel.refresh(DocumentModel.REFRESH_STATE | DocumentModel.REFRESH_CONTENT_LAZY, null);
2237                notifyCheckedInVersion(docModel, new IdRef(version.getUUID()), null, checkinComment);
2238            }
2239            // NXP-12921: use base version because we could need to publish
2240            // a previous version (after restoring for example)
2241            target = doc.getBaseVersion();
2242            if (overwriteExistingProxy) {
2243                proxy = updateExistingProxies(doc, sec, target);
2244                if (proxy == null) {
2245                    // no or several proxies, remove them
2246                    List<String> removedProxyIds = removeExistingProxies(doc, sec);
2247                    options.put(CoreEventConstants.REPLACED_PROXY_IDS, (Serializable) removedProxyIds);
2248                } else {
2249                    // notify proxy updates
2250                    notifyEvent(DocumentEventTypes.DOCUMENT_PROXY_UPDATED, proxy, options, null, null, true, false);
2251                    notifyEvent(DocumentEventTypes.DOCUMENT_PROXY_PUBLISHED, proxy, options, null, null, true, false);
2252                    notifyEvent(DocumentEventTypes.SECTION_CONTENT_PUBLISHED, section, options, null, null, true,
2253                            false);
2254                }
2255            }
2256        }
2257        if (proxy == null) {
2258            proxy = createProxyInternal(target, sec, options);
2259        }
2260        return proxy;
2261    }
2262
2263    @Override
2264    public String getSuperParentType(DocumentModel doc) {
2265        DocumentModel superSpace = getSuperSpace(doc);
2266        if (superSpace == null) {
2267            return null;
2268        } else {
2269            return superSpace.getType();
2270        }
2271    }
2272
2273    @Override
2274    public DocumentModel getSuperSpace(DocumentModel doc) {
2275        if (doc == null) {
2276            throw new IllegalArgumentException("null document");
2277        }
2278        if (doc.hasFacet(FacetNames.SUPER_SPACE)) {
2279            return doc;
2280        } else {
2281
2282            DocumentModel parent = getDirectAccessibleParent(doc.getRef());
2283            if (parent == null || "/".equals(parent.getPathAsString())) {
2284                // return Root instead of null
2285                return getRootDocument();
2286            } else {
2287                return getSuperSpace(parent);
2288            }
2289        }
2290    }
2291
2292    // walk the tree up until a accessible doc is found
2293    private DocumentModel getDirectAccessibleParent(DocumentRef docRef) {
2294        Document doc = resolveReference(docRef);
2295        Document parentDoc = doc.getParent();
2296        if (parentDoc == null) {
2297            return readModel(doc);
2298        }
2299        if (!hasPermission(parentDoc, READ)) {
2300            String parentPath = parentDoc.getPath();
2301            if ("/".equals(parentPath)) {
2302                return getRootDocument();
2303            } else {
2304                // try on parent
2305                return getDirectAccessibleParent(new PathRef(parentDoc.getPath()));
2306            }
2307        }
2308        return readModel(parentDoc);
2309    }
2310
2311    @Override
2312    public <T extends Serializable> T getDocumentSystemProp(DocumentRef ref, String systemProperty, Class<T> type) {
2313        Document doc = resolveReference(ref);
2314        return doc.getSystemProp(systemProperty, type);
2315    }
2316
2317    @Override
2318    public <T extends Serializable> void setDocumentSystemProp(DocumentRef ref, String systemProperty, T value) {
2319        Document doc = resolveReference(ref);
2320        if (systemProperty != null && systemProperty.startsWith(BINARY_TEXT_SYS_PROP)) {
2321            DocumentModel docModel = readModel(doc);
2322            Map<String, Serializable> options = new HashMap<String, Serializable>();
2323            options.put(systemProperty, value != null);
2324            notifyEvent(DocumentEventTypes.BINARYTEXT_UPDATED, docModel, options, null, null, false, true);
2325        }
2326        doc.setSystemProp(systemProperty, value);
2327    }
2328
2329    @Override
2330    public void orderBefore(DocumentRef parent, String src, String dest) {
2331        if ((src == null) || (src.equals(dest))) {
2332            return;
2333        }
2334        Document doc = resolveReference(parent);
2335        doc.orderBefore(src, dest);
2336        Map<String, Serializable> options = new HashMap<String, Serializable>();
2337
2338        // send event on container passing the reordered child as parameter
2339        DocumentModel docModel = readModel(doc);
2340        String comment = src;
2341        options.put(CoreEventConstants.REORDERED_CHILD, src);
2342        notifyEvent(DocumentEventTypes.DOCUMENT_CHILDREN_ORDER_CHANGED, docModel, options, null, comment, true, false);
2343    }
2344
2345    @Override
2346    public DocumentModelRefresh refreshDocument(DocumentRef ref, int refreshFlags, String[] schemas) {
2347        Document doc = resolveReference(ref);
2348
2349        // permission checks
2350        if ((refreshFlags & (DocumentModel.REFRESH_PREFETCH | DocumentModel.REFRESH_STATE
2351                | DocumentModel.REFRESH_CONTENT)) != 0) {
2352            checkPermission(doc, READ);
2353        }
2354        if ((refreshFlags & DocumentModel.REFRESH_ACP) != 0) {
2355            checkPermission(doc, READ_SECURITY);
2356        }
2357
2358        DocumentModelRefresh refresh = DocumentModelFactory.refreshDocumentModel(doc, refreshFlags, schemas);
2359
2360        // ACPs need the session, so aren't done in the factory method
2361        if ((refreshFlags & DocumentModel.REFRESH_ACP) != 0) {
2362            refresh.acp = getSession().getMergedACP(doc);
2363        }
2364
2365        return refresh;
2366    }
2367
2368    @Override
2369    public String[] getPermissionsToCheck(String permission) {
2370        return getSecurityService().getPermissionsToCheck(permission);
2371    }
2372
2373    @Override
2374    public <T extends DetachedAdapter> T adaptFirstMatchingDocumentWithFacet(DocumentRef docRef, String facet,
2375            Class<T> adapterClass) {
2376        Document doc = getFirstParentDocumentWithFacet(docRef, facet);
2377        if (doc != null) {
2378            DocumentModel docModel = readModel(doc);
2379            loadDataModelsForFacet(docModel, doc, facet);
2380            docModel.detach(false);
2381            return docModel.getAdapter(adapterClass);
2382        }
2383        return null;
2384    }
2385
2386    protected void loadDataModelsForFacet(DocumentModel docModel, Document doc, String facetName) {
2387        // Load all the data related to facet's schemas
2388        SchemaManager schemaManager = Framework.getLocalService(SchemaManager.class);
2389        CompositeType facet = schemaManager.getFacet(facetName);
2390        if (facet == null) {
2391            return;
2392        }
2393
2394        String[] facetSchemas = facet.getSchemaNames();
2395        for (String schema : facetSchemas) {
2396            DataModel dm = DocumentModelFactory.createDataModel(doc, schemaManager.getSchema(schema));
2397            docModel.getDataModels().put(schema, dm);
2398        }
2399    }
2400
2401    /**
2402     * Returns the first {@code Document} with the given {@code facet}, recursively going up the parent hierarchy.
2403     * Returns {@code null} if there is no more parent.
2404     * <p>
2405     * This method does not check security rights.
2406     */
2407    protected Document getFirstParentDocumentWithFacet(DocumentRef docRef, String facet) {
2408        Document doc = resolveReference(docRef);
2409        while (doc != null && !doc.hasFacet(facet)) {
2410            doc = doc.getParent();
2411        }
2412        return doc;
2413    }
2414
2415    @Override
2416    public Map<String, String> getBinaryFulltext(DocumentRef ref) {
2417        Document doc = resolveReference(ref);
2418        checkPermission(doc, READ);
2419        return getSession().getBinaryFulltext(doc.getUUID());
2420    }
2421
2422}