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