001/* 002 * (C) Copyright 2006-2019 Nuxeo (http://nuxeo.com/) and others. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 * 016 * Contributors: 017 * Florent Guillaume 018 */ 019package org.nuxeo.ecm.core.storage.sql.coremodel; 020 021import java.io.Serializable; 022import java.util.ArrayList; 023import java.util.Calendar; 024import java.util.Collection; 025import java.util.Collections; 026import java.util.HashMap; 027import java.util.List; 028import java.util.Map; 029import java.util.Set; 030import java.util.function.Consumer; 031 032import org.nuxeo.ecm.core.api.CoreSession; 033import org.nuxeo.ecm.core.api.DocumentNotFoundException; 034import org.nuxeo.ecm.core.api.LifeCycleException; 035import org.nuxeo.ecm.core.api.Lock; 036import org.nuxeo.ecm.core.api.NuxeoException; 037import org.nuxeo.ecm.core.api.NuxeoPrincipal; 038import org.nuxeo.ecm.core.api.PropertyException; 039import org.nuxeo.ecm.core.api.model.DocumentPart; 040import org.nuxeo.ecm.core.api.model.Property; 041import org.nuxeo.ecm.core.api.model.PropertyNotFoundException; 042import org.nuxeo.ecm.core.api.model.impl.ComplexProperty; 043import org.nuxeo.ecm.core.blob.DocumentBlobManager; 044import org.nuxeo.ecm.core.lifecycle.LifeCycle; 045import org.nuxeo.ecm.core.lifecycle.LifeCycleService; 046import org.nuxeo.ecm.core.model.Document; 047import org.nuxeo.ecm.core.schema.DocumentType; 048import org.nuxeo.ecm.core.schema.SchemaManager; 049import org.nuxeo.ecm.core.schema.types.ComplexType; 050import org.nuxeo.ecm.core.schema.types.Field; 051import org.nuxeo.ecm.core.schema.types.ListType; 052import org.nuxeo.ecm.core.schema.types.Schema; 053import org.nuxeo.ecm.core.schema.types.Type; 054import org.nuxeo.ecm.core.storage.BaseDocument; 055import org.nuxeo.ecm.core.storage.sql.Model; 056import org.nuxeo.ecm.core.storage.sql.Node; 057import org.nuxeo.runtime.api.Framework; 058 059public class SQLDocumentLive extends BaseDocument<Node> implements SQLDocument { 060 061 protected final Node node; 062 063 protected final Type type; 064 065 protected SQLSession session; 066 067 /** Proxy-induced types. */ 068 protected final List<Schema> proxySchemas; 069 070 /** 071 * Read-only flag, used to allow/disallow writes on versions. 072 */ 073 protected boolean readonly; 074 075 protected SQLDocumentLive(Node node, ComplexType type, SQLSession session, boolean readonly) { 076 this.node = node; 077 this.type = type; 078 this.session = session; 079 if (node != null && node.isProxy()) { 080 SchemaManager schemaManager = Framework.getService(SchemaManager.class); 081 proxySchemas = schemaManager.getProxySchemas(type.getName()); 082 } else { 083 proxySchemas = null; 084 } 085 this.readonly = readonly; 086 } 087 088 @Override 089 public void setReadOnly(boolean readonly) { 090 this.readonly = readonly; 091 } 092 093 @Override 094 public boolean isReadOnly() { 095 return readonly; 096 } 097 098 @Override 099 public Node getNode() { 100 return node; 101 } 102 103 @Override 104 public String getName() { 105 return getNode() == null ? null : getNode().getName(); 106 } 107 108 @Override 109 public Long getPos() { 110 return getNode().getPos(); 111 } 112 113 /* 114 * ----- org.nuxeo.ecm.core.model.Document ----- 115 */ 116 117 @Override 118 public DocumentType getType() { 119 return (DocumentType) type; 120 } 121 122 @Override 123 public SQLSession getSession() { 124 return session; 125 } 126 127 @Override 128 public boolean isFolder() { 129 return type == null // null document 130 || ((DocumentType) type).isFolder(); 131 } 132 133 @Override 134 public String getUUID() { 135 return session.idToString(getNode().getId()); 136 } 137 138 @Override 139 public Document getParent() { 140 return session.getParent(getNode()); 141 } 142 143 @Override 144 public String getPath() { 145 return session.getPath(getNode()); 146 } 147 148 @Override 149 public boolean isProxy() { 150 return false; 151 } 152 153 @Override 154 public String getRepositoryName() { 155 return session.getRepositoryName(); 156 } 157 158 @Override 159 protected List<Schema> getProxySchemas() { 160 return proxySchemas; 161 } 162 163 @Override 164 public void remove() { 165 session.remove(getNode()); 166 } 167 168 @Override 169 public void remove(NuxeoPrincipal principal) { 170 // principal is not yet used in SQL document 171 remove(); 172 } 173 174 @Override 175 public void removeSingleton() { 176 throw new UnsupportedOperationException("Not implemented yet"); 177 } 178 179 /** 180 * Reads into the {@link DocumentPart} the values from this {@link SQLDocument}. 181 */ 182 @Override 183 public void readDocumentPart(DocumentPart dp) throws PropertyException { 184 readComplexProperty(getNode(), (ComplexProperty) dp); 185 } 186 187 @Override 188 public boolean writeDocumentPart(DocumentPart dp, WriteContext writeContext, boolean create) 189 throws PropertyException { 190 boolean changed = writeDocumentPart(getNode(), dp, writeContext, create); 191 clearDirtyFlags(dp); 192 return changed; 193 } 194 195 @Override 196 protected Node getChild(Node node, String name, Type type) throws PropertyException { 197 return session.getChildProperty(node, name, type.getName()); 198 } 199 200 @Override 201 protected Node getChildForWrite(Node node, String name, Type type) throws PropertyException { 202 return session.getChildPropertyForWrite(node, name, type.getName()); 203 } 204 205 @Override 206 protected List<Node> getChildAsList(Node node, String name) throws PropertyException { 207 return session.getComplexList(node, name); 208 } 209 210 @Override 211 protected void updateList(Node node, String name, Field field, String xpath, List<Object> values) 212 throws PropertyException { 213 List<Node> childNodes = getChildAsList(node, name); 214 int oldSize = childNodes.size(); 215 int newSize = values.size(); 216 // remove extra list elements 217 if (oldSize > newSize) { 218 for (int i = oldSize - 1; i >= newSize; i--) { 219 session.removeProperty(childNodes.remove(i)); 220 } 221 } 222 // add new list elements 223 if (oldSize < newSize) { 224 String typeName = field.getType().getName(); 225 for (int i = oldSize; i < newSize; i++) { 226 Node childNode = session.addChildProperty(node, name, Long.valueOf(i), typeName); 227 childNodes.add(childNode); 228 } 229 } 230 // write values 231 int i = 0; 232 for (Object v : values) { 233 Node childNode = childNodes.get(i); 234 setValueComplex(childNode, field, xpath + '/' + i, v); 235 i++; 236 } 237 } 238 239 @Override 240 protected List<Node> updateList(Node node, String name, Property property) throws PropertyException { 241 Collection<Property> properties = property.getChildren(); 242 List<Node> childNodes = getChildAsList(node, name); 243 int oldSize = childNodes.size(); 244 int newSize = properties.size(); 245 // remove extra list elements 246 if (oldSize > newSize) { 247 for (int i = oldSize - 1; i >= newSize; i--) { 248 session.removeProperty(childNodes.remove(i)); 249 } 250 } 251 // add new list elements 252 if (oldSize < newSize) { 253 String typeName = ((ListType) property.getType()).getFieldType().getName(); 254 for (int i = oldSize; i < newSize; i++) { 255 Node childNode = session.addChildProperty(node, name, Long.valueOf(i), typeName); 256 childNodes.add(childNode); 257 } 258 } 259 return childNodes; 260 } 261 262 @Override 263 protected String internalName(String name) { 264 return name; 265 } 266 267 @Override 268 public Object getValue(String xpath) throws PropertyException { 269 return getValueObject(getNode(), xpath); 270 } 271 272 @Override 273 public void setValue(String xpath, Object value) throws PropertyException { 274 setValueObject(getNode(), xpath, value); 275 } 276 277 @Override 278 public void visitBlobs(Consumer<BlobAccessor> blobVisitor) throws PropertyException { 279 visitBlobs(getNode(), blobVisitor, NO_DIRTY); 280 } 281 282 @Override 283 public Serializable getPropertyValue(String name) { 284 return getNode().getSimpleProperty(name).getValue(); 285 } 286 287 @Override 288 public void setPropertyValue(String name, Serializable value) { 289 getNode().setSimpleProperty(name, value); 290 } 291 292 protected static final Map<String, String> systemPropNameMap; 293 294 static { 295 systemPropNameMap = new HashMap<>(); 296 systemPropNameMap.put(FULLTEXT_JOBID_SYS_PROP, Model.FULLTEXT_JOBID_PROP); 297 systemPropNameMap.put(IS_TRASHED_SYS_PROP, Model.MAIN_IS_TRASHED_PROP); 298 } 299 300 @Override 301 public void setSystemProp(String name, Serializable value) { 302 String propertyName; 303 if (name.startsWith(SIMPLE_TEXT_SYS_PROP)) { 304 propertyName = name.replace(SIMPLE_TEXT_SYS_PROP, Model.FULLTEXT_SIMPLETEXT_PROP); 305 if (session.isFulltextStoredInBlob()) { 306 // if binary fulltext is stored in blob, there is no simple fulltext available 307 return; 308 } 309 } else if (name.startsWith(BINARY_TEXT_SYS_PROP)) { 310 propertyName = name.replace(BINARY_TEXT_SYS_PROP, Model.FULLTEXT_BINARYTEXT_PROP); 311 if (session.isFulltextStoredInBlob()) { 312 if (!(value instanceof String)) { 313 throw new PropertyException("Property " + name + " must be a string"); 314 } 315 setPropertyBlobData(propertyName, (String) value); 316 return; 317 } 318 } else { 319 propertyName = systemPropNameMap.get(name); 320 } 321 if (propertyName == null) { 322 throw new PropertyNotFoundException(name, "Unknown system property"); 323 } 324 setPropertyValue(propertyName, value); 325 } 326 327 @Override 328 @SuppressWarnings("unchecked") 329 public <T extends Serializable> T getSystemProp(String name, Class<T> type) { 330 String propertyName = systemPropNameMap.get(name); 331 if (propertyName == null) { 332 throw new PropertyNotFoundException(name, "Unknown system property"); 333 } 334 Serializable value = getPropertyValue(propertyName); 335 if (value == null) { 336 if (type == Boolean.class) { 337 value = Boolean.FALSE; 338 } else if (type == Long.class) { 339 value = Long.valueOf(0); 340 } 341 } 342 return (T) value; 343 } 344 345 @Override 346 public String getChangeToken() { 347 if (session.isChangeTokenEnabled()) { 348 Long sysChangeToken = (Long) getPropertyValue(Model.MAIN_SYS_CHANGE_TOKEN_PROP); 349 Long changeToken = (Long) getPropertyValue(Model.MAIN_CHANGE_TOKEN_PROP); 350 return buildUserVisibleChangeToken(sysChangeToken, changeToken); 351 } else { 352 Calendar modified; 353 try { 354 modified = (Calendar) getPropertyValue(DC_MODIFIED); 355 } catch (PropertyNotFoundException e) { 356 modified = null; 357 } 358 return getLegacyChangeToken(modified); 359 } 360 } 361 362 @Override 363 public boolean validateUserVisibleChangeToken(String userVisibleChangeToken) { 364 if (userVisibleChangeToken == null) { 365 return true; 366 } 367 if (session.isChangeTokenEnabled()) { 368 Long sysChangeToken = (Long) getPropertyValue(Model.MAIN_SYS_CHANGE_TOKEN_PROP); 369 Long changeToken = (Long) getPropertyValue(Model.MAIN_CHANGE_TOKEN_PROP); 370 return validateUserVisibleChangeToken(sysChangeToken, changeToken, userVisibleChangeToken); 371 } else { 372 Calendar modified; 373 try { 374 modified = (Calendar) getPropertyValue(DC_MODIFIED); 375 } catch (PropertyNotFoundException e) { 376 modified = null; 377 } 378 return validateLegacyChangeToken(modified, userVisibleChangeToken); 379 } 380 } 381 382 @Override 383 public void markUserChange() { 384 session.markUserChange(getNode().getId()); 385 } 386 387 /* 388 * ----- Retention and Hold ----- 389 */ 390 391 protected DocumentBlobManager getDocumentBlobManager() { 392 return Framework.getService(DocumentBlobManager.class); 393 } 394 395 @Override 396 public void makeRecord() { 397 setPropertyValue(Model.MAIN_IS_RECORD_PROP, Boolean.TRUE); 398 getDocumentBlobManager().notifyMakeRecord(this); 399 } 400 401 @Override 402 public boolean isRecord() { 403 return Boolean.TRUE.equals(getPropertyValue(Model.MAIN_IS_RECORD_PROP)); 404 } 405 406 @Override 407 public void setRetainUntil(Calendar retainUntil) { 408 Calendar current = (Calendar) getPropertyValue(Model.MAIN_RETAIN_UNTIL_PROP); 409 if (!allowNewRetention(current, retainUntil)) { 410 throw new PropertyException( 411 "Cannot reduce retention time from: " + (current == null ? "null" : current.toInstant()) + " to: " 412 + (retainUntil == null ? "null" : retainUntil.toInstant())); 413 } 414 setPropertyValue(Model.MAIN_RETAIN_UNTIL_PROP, retainUntil); 415 getDocumentBlobManager().notifySetRetainUntil(this, retainUntil); 416 } 417 418 @Override 419 public Calendar getRetainUntil() { 420 return (Calendar) getPropertyValue(Model.MAIN_RETAIN_UNTIL_PROP); 421 } 422 423 @Override 424 public void setLegalHold(boolean hold) { 425 setPropertyValue(Model.MAIN_HAS_LEGAL_HOLD_PROP, hold ? Boolean.TRUE : null); 426 getDocumentBlobManager().notifySetLegalHold(this, hold); 427 } 428 429 @Override 430 public boolean hasLegalHold() { 431 return Boolean.TRUE.equals(getPropertyValue(Model.MAIN_HAS_LEGAL_HOLD_PROP)); 432 } 433 434 @Override 435 public void setRetentionActive(boolean retentionActive) { 436 setPropertyValue(Model.MAIN_IS_RETENTION_ACTIVE_PROP, retentionActive ? Boolean.TRUE : null); 437 } 438 439 @Override 440 public boolean isRetentionActive() { 441 return Boolean.TRUE.equals(getPropertyValue(Model.MAIN_IS_RETENTION_ACTIVE_PROP)); 442 } 443 444 /* 445 * ----- LifeCycle ----- 446 */ 447 448 @Override 449 public String getLifeCyclePolicy() { 450 return (String) getPropertyValue(Model.MISC_LIFECYCLE_POLICY_PROP); 451 } 452 453 @Override 454 public void setLifeCyclePolicy(String policy) { 455 setPropertyValue(Model.MISC_LIFECYCLE_POLICY_PROP, policy); 456 getDocumentBlobManager().notifyChanges(this, Collections.singleton(Model.MISC_LIFECYCLE_POLICY_PROP)); 457 } 458 459 @Override 460 public String getLifeCycleState() { 461 return (String) getPropertyValue(Model.MISC_LIFECYCLE_STATE_PROP); 462 } 463 464 @Override 465 public void setCurrentLifeCycleState(String state) { 466 setPropertyValue(Model.MISC_LIFECYCLE_STATE_PROP, state); 467 getDocumentBlobManager().notifyChanges(this, Collections.singleton(Model.MISC_LIFECYCLE_STATE_PROP)); 468 } 469 470 @Override 471 public void followTransition(String transition) throws LifeCycleException { 472 LifeCycleService service = Framework.getService(LifeCycleService.class); 473 if (service == null) { 474 throw new NuxeoException("LifeCycleService not available"); 475 } 476 service.followTransition(this, transition); 477 } 478 479 @Override 480 public Collection<String> getAllowedStateTransitions() { 481 LifeCycleService service = Framework.getService(LifeCycleService.class); 482 if (service == null) { 483 throw new NuxeoException("LifeCycleService not available"); 484 } 485 LifeCycle lifeCycle = service.getLifeCycleFor(this); 486 if (lifeCycle == null) { 487 return Collections.emptyList(); 488 } 489 return lifeCycle.getAllowedStateTransitionsFrom(getLifeCycleState()); 490 } 491 492 /* 493 * ----- org.nuxeo.ecm.core.versioning.VersionableDocument ----- 494 */ 495 496 @Override 497 public boolean isVersion() { 498 return false; 499 } 500 501 @Override 502 public Document getBaseVersion() { 503 if (isCheckedOut()) { 504 return null; 505 } 506 Serializable id = getPropertyValue(Model.MAIN_BASE_VERSION_PROP); 507 if (id == null) { 508 // shouldn't happen 509 return null; 510 } 511 return session.getDocumentById(id); 512 } 513 514 @Override 515 public String getVersionSeriesId() { 516 return getUUID(); 517 } 518 519 @Override 520 public Document getSourceDocument() { 521 return this; 522 } 523 524 @Override 525 public Document checkIn(String label, String checkinComment) { 526 if (isRecord()) { 527 throw new PropertyException("Record cannot be checked in: " + getUUID()); 528 } 529 Document version = session.checkIn(getNode(), label, checkinComment); 530 getDocumentBlobManager().freezeVersion(version); 531 return version; 532 } 533 534 @Override 535 public void checkOut() { 536 session.checkOut(getNode()); 537 } 538 539 @Override 540 public boolean isCheckedOut() { 541 return !Boolean.TRUE.equals(getPropertyValue(Model.MAIN_CHECKED_IN_PROP)); 542 } 543 544 @Override 545 public boolean isMajorVersion() { 546 return false; 547 } 548 549 @Override 550 public boolean isLatestVersion() { 551 return false; 552 } 553 554 @Override 555 public boolean isLatestMajorVersion() { 556 return false; 557 } 558 559 @Override 560 public boolean isVersionSeriesCheckedOut() { 561 return isCheckedOut(); 562 } 563 564 @Override 565 public String getVersionLabel() { 566 return (String) getPropertyValue(Model.VERSION_LABEL_PROP); 567 } 568 569 @Override 570 public String getCheckinComment() { 571 return (String) getPropertyValue(Model.VERSION_DESCRIPTION_PROP); 572 } 573 574 @Override 575 public Document getWorkingCopy() { 576 return this; 577 } 578 579 @Override 580 public Calendar getVersionCreationDate() { 581 return (Calendar) getPropertyValue(Model.VERSION_CREATED_PROP); 582 } 583 584 @Override 585 public void restore(Document version) { 586 if (!version.isVersion()) { 587 throw new NuxeoException("Cannot restore a non-version: " + version); 588 } 589 session.restore(getNode(), ((SQLDocument) version).getNode()); 590 } 591 592 @Override 593 public List<String> getVersionsIds() { 594 String versionSeriesId = getVersionSeriesId(); 595 Collection<Document> versions = session.getVersions(versionSeriesId); 596 List<String> ids = new ArrayList<>(versions.size()); 597 for (Document version : versions) { 598 ids.add(version.getUUID()); 599 } 600 return ids; 601 } 602 603 @Override 604 public Document getVersion(String label) { 605 String versionSeriesId = getVersionSeriesId(); 606 return session.getVersionByLabel(versionSeriesId, label); 607 } 608 609 @Override 610 public List<Document> getVersions() { 611 String versionSeriesId = getVersionSeriesId(); 612 return session.getVersions(versionSeriesId); 613 } 614 615 @Override 616 public Document getLastVersion() { 617 String versionSeriesId = getVersionSeriesId(); 618 return session.getLastVersion(versionSeriesId); 619 } 620 621 @Override 622 public Document getChild(String name) { 623 return session.getChild(getNode(), name); 624 } 625 626 @Override 627 public List<Document> getChildren() { 628 return session.getChildren(getNode()); // newly allocated 629 } 630 631 @Override 632 public List<String> getChildrenIds() { 633 // not optimized as this method doesn't seem to be used 634 List<Document> children = session.getChildren(getNode()); 635 List<String> ids = new ArrayList<>(children.size()); 636 for (Document child : children) { 637 ids.add(child.getUUID()); 638 } 639 return ids; 640 } 641 642 @Override 643 public boolean hasChild(String name) { 644 return session.hasChild(getNode(), name); 645 } 646 647 @Override 648 public boolean hasChildren() { 649 return session.hasChildren(getNode()); 650 } 651 652 @Override 653 public Document addChild(String name, String typeName) { 654 return session.addChild(getNode(), name, null, typeName); 655 } 656 657 @Override 658 public void orderBefore(String src, String dest) { 659 SQLDocument srcDoc = (SQLDocument) getChild(src); 660 if (srcDoc == null) { 661 throw new DocumentNotFoundException("Document " + this + " has no child: " + src); 662 } 663 SQLDocument destDoc; 664 if (dest == null) { 665 destDoc = null; 666 } else { 667 destDoc = (SQLDocument) getChild(dest); 668 if (destDoc == null) { 669 throw new DocumentNotFoundException("Document " + this + " has no child: " + dest); 670 } 671 } 672 session.orderBefore(getNode(), srcDoc.getNode(), destDoc == null ? null : destDoc.getNode()); 673 } 674 675 @Override 676 public Set<String> getAllFacets() { 677 return getNode().getAllMixinTypes(); 678 } 679 680 @Override 681 public String[] getFacets() { 682 return getNode().getMixinTypes(); 683 } 684 685 @Override 686 public boolean hasFacet(String facet) { 687 return getNode().hasMixinType(facet); 688 } 689 690 @Override 691 public boolean addFacet(String facet) { 692 return session.addMixinType(getNode(), facet); 693 } 694 695 @Override 696 public boolean removeFacet(String facet) { 697 return session.removeMixinType(getNode(), facet); 698 } 699 700 /* 701 * ----- PropertyContainer inherited from SQLComplexProperty ----- 702 */ 703 704 /* 705 * ----- toString/equals/hashcode ----- 706 */ 707 708 @Override 709 public String toString() { 710 return getClass().getSimpleName() + '(' + getName() + ',' + getUUID() + ')'; 711 } 712 713 @Override 714 public boolean equals(Object other) { 715 if (other == this) { 716 return true; 717 } 718 if (other == null) { 719 return false; 720 } 721 if (other.getClass() == this.getClass()) { 722 return equals((SQLDocumentLive) other); 723 } 724 return false; 725 } 726 727 private boolean equals(SQLDocumentLive other) { 728 return getNode().equals(other.getNode()); 729 } 730 731 @Override 732 public int hashCode() { 733 return getNode().hashCode(); 734 } 735 736 @Override 737 public Document getTargetDocument() { 738 return null; 739 } 740 741 @Override 742 public void setTargetDocument(Document target) { 743 throw new NuxeoException("Not a proxy"); 744 } 745 746 @Override 747 protected Lock getDocumentLock() { 748 // lock manager can get the lock even on a recently created and unsaved document 749 throw new UnsupportedOperationException(); 750 } 751 752 @Override 753 protected Lock setDocumentLock(Lock lock) { 754 // lock manager can set the lock even on a recently created and unsaved document 755 throw new UnsupportedOperationException(); 756 } 757 758 @Override 759 protected Lock removeDocumentLock(String owner) { 760 // lock manager can remove the lock even on a recently created and unsaved document 761 throw new UnsupportedOperationException(); 762 } 763 764}