001/* 002 * (C) Copyright 2006-2018 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; 020 021import static org.nuxeo.ecm.core.api.CoreSession.BINARY_FULLTEXT_MAIN_KEY; 022import static org.nuxeo.ecm.core.model.Session.PROP_ALLOW_DELETE_UNDELETABLE_DOCUMENTS; 023 024import java.io.IOException; 025import java.io.Serializable; 026import java.text.Normalizer; 027import java.util.ArrayList; 028import java.util.Arrays; 029import java.util.Calendar; 030import java.util.Collection; 031import java.util.Collections; 032import java.util.HashMap; 033import java.util.HashSet; 034import java.util.LinkedList; 035import java.util.List; 036import java.util.Map; 037import java.util.Map.Entry; 038import java.util.Set; 039import java.util.function.Consumer; 040import java.util.stream.Collectors; 041 042import org.apache.commons.logging.Log; 043import org.apache.commons.logging.LogFactory; 044import org.nuxeo.ecm.core.api.Blob; 045import org.nuxeo.ecm.core.api.ConcurrentUpdateException; 046import org.nuxeo.ecm.core.api.DocumentExistsException; 047import org.nuxeo.ecm.core.api.IterableQueryResult; 048import org.nuxeo.ecm.core.api.NuxeoException; 049import org.nuxeo.ecm.core.api.PartialList; 050import org.nuxeo.ecm.core.api.PropertyException; 051import org.nuxeo.ecm.core.api.ScrollResult; 052import org.nuxeo.ecm.core.api.lock.LockManager; 053import org.nuxeo.ecm.core.api.repository.FulltextConfiguration; 054import org.nuxeo.ecm.core.api.repository.RepositoryManager; 055import org.nuxeo.ecm.core.api.security.ACL; 056import org.nuxeo.ecm.core.api.security.SecurityConstants; 057import org.nuxeo.ecm.core.blob.BlobInfo; 058import org.nuxeo.ecm.core.blob.DocumentBlobManager; 059import org.nuxeo.ecm.core.model.Document; 060import org.nuxeo.ecm.core.query.QueryFilter; 061import org.nuxeo.ecm.core.query.sql.NXQL; 062import org.nuxeo.ecm.core.schema.DocumentType; 063import org.nuxeo.ecm.core.schema.SchemaManager; 064import org.nuxeo.ecm.core.storage.BaseDocument; 065import org.nuxeo.ecm.core.storage.FulltextDescriptor; 066import org.nuxeo.ecm.core.storage.FulltextExtractorWork; 067import org.nuxeo.ecm.core.storage.sql.PersistenceContext.PathAndId; 068import org.nuxeo.ecm.core.storage.sql.RowMapper.NodeInfo; 069import org.nuxeo.ecm.core.storage.sql.RowMapper.RowBatch; 070import org.nuxeo.ecm.core.work.api.Work; 071import org.nuxeo.ecm.core.work.api.WorkManager; 072import org.nuxeo.ecm.core.work.api.WorkManager.Scheduling; 073import org.nuxeo.runtime.api.Framework; 074import org.nuxeo.runtime.metrics.MetricsService; 075import org.nuxeo.runtime.transaction.TransactionHelper; 076 077import io.dropwizard.metrics5.MetricName; 078import io.dropwizard.metrics5.MetricRegistry; 079import io.dropwizard.metrics5.SharedMetricRegistries; 080import io.dropwizard.metrics5.Timer; 081 082/** 083 * The session is the main high level access point to data from the underlying database. 084 */ 085public class SessionImpl implements Session { 086 087 private static final Log log = LogFactory.getLog(SessionImpl.class); 088 089 /** 090 * Set this system property to false if you don't want repositories to be looked up under the compatibility name 091 * "default" in the "repositories" table. 092 * <p> 093 * Only do this if you start from an empty database, or if you have migrated the "repositories" table by hand, or if 094 * you need to create a new repository in a database already containing a "default" repository (table sharing, not 095 * recommended). 096 */ 097 public static final String COMPAT_REPOSITORY_NAME_KEY = "org.nuxeo.vcs.repository.name.default.compat"; 098 099 private static final boolean COMPAT_REPOSITORY_NAME = Boolean.parseBoolean( 100 Framework.getProperty(COMPAT_REPOSITORY_NAME_KEY, "true")); 101 102 protected final RepositoryImpl repository; 103 104 private final Mapper mapper; 105 106 private final Model model; 107 108 // public because used by unit tests 109 public final PersistenceContext context; 110 111 protected final boolean changeTokenEnabled; 112 113 protected final FulltextDescriptor fulltextDescriptor; 114 115 private boolean inTransaction; 116 117 private Serializable rootNodeId; 118 119 private boolean readAclsChanged; 120 121 // @since 5.7 122 protected final MetricRegistry registry = SharedMetricRegistries.getOrCreate(MetricsService.class.getName()); 123 124 private final Timer saveTimer; 125 126 private final Timer queryTimer; 127 128 private final Timer aclrUpdateTimer; 129 130 private static final java.lang.String LOG_MIN_DURATION_KEY = "org.nuxeo.vcs.query.log_min_duration_ms"; 131 132 private static final long LOG_MIN_DURATION_NS = Long.parseLong(Framework.getProperty(LOG_MIN_DURATION_KEY, "-1")) 133 * 1000000; 134 135 public SessionImpl(RepositoryImpl repository, Model model, Mapper mapper) { 136 this.repository = repository; 137 this.mapper = mapper; 138 this.model = model; 139 context = new PersistenceContext(model, mapper, this); 140 changeTokenEnabled = repository.isChangeTokenEnabled(); 141 fulltextDescriptor = repository.getRepositoryDescriptor().getFulltextDescriptor(); 142 readAclsChanged = false; 143 144 saveTimer = registry.timer(MetricName.build("nuxeo", "repositories", "repository", "save", "timer") 145 .tagged("repository", repository.getName())); 146 queryTimer = registry.timer(MetricName.build("nuxeo", "repositories", "repository", "query", "timer") 147 .tagged("repository", repository.getName())); 148 aclrUpdateTimer = registry.timer(MetricName.build("nuxeo", "repositories", "repository", "aclr-update", "timer") 149 .tagged("repository", repository.getName())); 150 computeRootNode(); 151 } 152 153 // called by NetServlet when forwarding remote NetMapper calls. 154 @Override 155 public Mapper getMapper() { 156 return mapper; 157 } 158 159 /** 160 * Clears all the caches. Called by RepositoryManagement. 161 */ 162 protected int clearCaches() { 163 if (inTransaction) { 164 // avoid potential multi-threaded access to active session 165 return 0; 166 } 167 return context.clearCaches(); 168 } 169 170 protected PersistenceContext getContext() { 171 return context; 172 } 173 174 /** 175 * Generates a new id, or used a pre-generated one (import). 176 */ 177 protected Serializable generateNewId(Serializable id) { 178 return context.generateNewId(id); 179 } 180 181 protected boolean isIdNew(Serializable id) { 182 return context.isIdNew(id); 183 } 184 185 @Override 186 public void close() { 187 try { 188 closeSession(); 189 } finally { 190 repository.closeSession(this); 191 } 192 } 193 194 protected void closeSession() { 195 context.clearCaches(); 196 // close the mapper and therefore the connection 197 mapper.close(); 198 // don't clean the caches, we keep the pristine cache around 199 // TODO this is getting destroyed, we can clean everything 200 } 201 202 /* 203 * ----- Session ----- 204 */ 205 206 @Override 207 public String getRepositoryName() { 208 return repository.getName(); 209 } 210 211 @Override 212 public Model getModel() { 213 return model; 214 } 215 216 @Override 217 public Node getRootNode() { 218 return getNodeById(rootNodeId); 219 } 220 221 @Override 222 public void save() { 223 @SuppressWarnings("resource") 224 final Timer.Context timerContext = saveTimer.time(); 225 try { 226 flush(); 227 if (!inTransaction) { 228 sendInvalidationsToOthers(); 229 // as we don't have a way to know when the next 230 // non-transactional 231 // statement will start, process invalidations immediately 232 } 233 processReceivedInvalidations(); 234 } finally { 235 timerContext.stop(); 236 } 237 } 238 239 protected void flush() { 240 List<Work> works; 241 if (!fulltextDescriptor.getFulltextDisabled()) { 242 works = getFulltextWorks(); 243 } else { 244 works = Collections.emptyList(); 245 } 246 doFlush(); 247 if (readAclsChanged) { 248 updateReadAcls(); 249 } 250 scheduleWork(works); 251 checkInvalidationsConflict(); 252 } 253 254 protected void scheduleWork(List<Work> works) { 255 // do async fulltext indexing only if high-level sessions are available 256 RepositoryManager repositoryManager = Framework.getService(RepositoryManager.class); 257 if (repositoryManager != null && !works.isEmpty()) { 258 WorkManager workManager = Framework.getService(WorkManager.class); 259 for (Work work : works) { 260 // schedule work post-commit 261 // in non-tx mode, this may execute it nearly immediately 262 workManager.schedule(work, Scheduling.IF_NOT_SCHEDULED, true); 263 } 264 } 265 } 266 267 protected void doFlush() { 268 List<Fragment> fragmentsToClearDirty = new ArrayList<>(0); 269 RowBatch batch = context.getSaveBatch(fragmentsToClearDirty); 270 if (!batch.isEmpty()) { 271 log.debug("Saving session"); 272 // execute the batch 273 try { 274 mapper.write(batch); 275 log.debug("End of save"); 276 } finally { 277 // callers must never observe a DeltaLong in the fragments 278 for (Fragment fragment : fragmentsToClearDirty) { 279 fragment.clearDirty(); 280 } 281 } 282 } 283 } 284 285 protected Serializable getContainingDocument(Serializable id) { 286 return context.getContainingDocument(id); 287 } 288 289 /** 290 * Gets the fulltext updates to do. Called at save() time. 291 * 292 * @return a list of {@link Work} instances to schedule post-commit. 293 */ 294 protected List<Work> getFulltextWorks() { 295 Set<Serializable> dirtyStrings = new HashSet<>(); 296 Set<Serializable> dirtyBinaries = new HashSet<>(); 297 context.findDirtyDocuments(dirtyStrings, dirtyBinaries); 298 if (model.getFulltextConfiguration().fulltextSearchDisabled) { 299 // We only need to update dirty simple strings if fulltext search is not disabled 300 // because in that case Elasticsearch will do its own extraction/indexing. 301 // We need to detect dirty binary strings in all cases, because Elasticsearch 302 // will need them even if the repository itself doesn't use them for search. 303 dirtyStrings = Collections.emptySet(); 304 } 305 Set<Serializable> dirtyIds = new HashSet<>(); 306 dirtyIds.addAll(dirtyStrings); 307 dirtyIds.addAll(dirtyBinaries); 308 if (dirtyIds.isEmpty()) { 309 return Collections.emptyList(); 310 } 311 markIndexingInProgress(dirtyIds); 312 List<Work> works = new ArrayList<>(dirtyIds.size()); 313 for (Serializable id : dirtyIds) { 314 boolean updateSimpleText = dirtyStrings.contains(id); 315 boolean updateBinaryText = dirtyBinaries.contains(id); 316 Work work = new FulltextExtractorWork(repository.getName(), model.idToString(id), updateSimpleText, 317 updateBinaryText, true); 318 works.add(work); 319 } 320 return works; 321 } 322 323 /** 324 * Mark indexing in progress, so that future copies (including versions) will be indexed as well. 325 */ 326 protected void markIndexingInProgress(Set<Serializable> dirtyIds) { 327 FulltextConfiguration fulltextConfiguration = model.getFulltextConfiguration(); 328 for (Node node : getNodesByIds(dirtyIds)) { 329 if (!fulltextConfiguration.isFulltextIndexable(node.getPrimaryType())) { 330 continue; 331 } 332 node.getSimpleProperty(Model.FULLTEXT_JOBID_PROP).setValue(model.idToString(node.getId())); 333 } 334 } 335 336 /** 337 * Post-transaction invalidations notification. 338 * <p> 339 * Called post-transaction by session commit/rollback or transactionless save. 340 */ 341 protected void sendInvalidationsToOthers() { 342 context.sendInvalidationsToOthers(); 343 } 344 345 /** 346 * Processes all invalidations accumulated. 347 * <p> 348 * Called pre-transaction by start or transactionless save; 349 */ 350 protected void processReceivedInvalidations() { 351 context.processReceivedInvalidations(); 352 } 353 354 /** 355 * Post transaction check invalidations processing. 356 */ 357 protected void checkInvalidationsConflict() { 358 // repository.receiveClusterInvalidations(this); 359 context.checkInvalidationsConflict(); 360 } 361 362 /* 363 * ------------------------------------------------------------- 364 * ------------------------------------------------------------- 365 * ------------------------------------------------------------- 366 */ 367 368 protected Node getNodeById(Serializable id, boolean prefetch) { 369 List<Node> nodes = getNodesByIds(Collections.singletonList(id), prefetch); 370 Node node = nodes.get(0); 371 // ((JDBCMapper) ((CachingMapper) 372 // mapper).mapper).logger.log("getNodeById " + id + " -> " + (node == 373 // null ? "missing" : "found")); 374 return node; 375 } 376 377 @Override 378 public Node getNodeById(Serializable id) { 379 if (id == null) { 380 throw new IllegalArgumentException("Illegal null id"); 381 } 382 return getNodeById(id, true); 383 } 384 385 public List<Node> getNodesByIds(Collection<Serializable> ids, boolean prefetch) { 386 // get hier fragments 387 List<RowId> hierRowIds = new ArrayList<>(ids.size()); 388 for (Serializable id : ids) { 389 hierRowIds.add(new RowId(Model.HIER_TABLE_NAME, id)); 390 } 391 392 List<Fragment> hierFragments = context.getMulti(hierRowIds, false); 393 394 // find available paths 395 Map<Serializable, String> paths = new HashMap<>(); 396 Set<Serializable> parentIds = new HashSet<>(); 397 for (Fragment fragment : hierFragments) { 398 Serializable id = fragment.getId(); 399 PathAndId pathOrId = context.getPathOrMissingParentId((SimpleFragment) fragment, false); 400 // find missing fragments 401 if (pathOrId.path != null) { 402 paths.put(id, pathOrId.path); 403 } else { 404 parentIds.add(pathOrId.id); 405 } 406 } 407 // fetch the missing parents and their ancestors in bulk 408 if (!parentIds.isEmpty()) { 409 // fetch them in the context 410 getHierarchyAndAncestors(parentIds); 411 // compute missing paths using context 412 for (Fragment fragment : hierFragments) { 413 Serializable id = fragment.getId(); 414 if (paths.containsKey(id)) { 415 continue; 416 } 417 String path = context.getPath((SimpleFragment) fragment); 418 paths.put(id, path); 419 } 420 } 421 422 // prepare fragment groups to build nodes 423 Map<Serializable, FragmentGroup> fragmentGroups = new HashMap<>(ids.size()); 424 for (Fragment fragment : hierFragments) { 425 Serializable id = fragment.row.id; 426 fragmentGroups.put(id, new FragmentGroup((SimpleFragment) fragment, new FragmentsMap())); 427 } 428 429 if (prefetch) { 430 List<RowId> bulkRowIds = new ArrayList<>(); 431 Set<Serializable> proxyIds = new HashSet<>(); 432 433 // get rows to prefetch for hier fragments 434 for (Fragment fragment : hierFragments) { 435 findPrefetchedFragments((SimpleFragment) fragment, bulkRowIds, proxyIds); 436 } 437 438 // proxies 439 440 // get proxies fragments 441 List<RowId> proxiesRowIds = new ArrayList<>(proxyIds.size()); 442 for (Serializable id : proxyIds) { 443 proxiesRowIds.add(new RowId(Model.PROXY_TABLE_NAME, id)); 444 } 445 List<Fragment> proxiesFragments = context.getMulti(proxiesRowIds, true); 446 Set<Serializable> targetIds = new HashSet<>(); 447 for (Fragment fragment : proxiesFragments) { 448 Serializable targetId = ((SimpleFragment) fragment).get(Model.PROXY_TARGET_KEY); 449 targetIds.add(targetId); 450 } 451 452 // get hier fragments for proxies' targets 453 targetIds.removeAll(ids); // only those we don't have already 454 hierRowIds = new ArrayList<>(targetIds.size()); 455 for (Serializable id : targetIds) { 456 hierRowIds.add(new RowId(Model.HIER_TABLE_NAME, id)); 457 } 458 hierFragments = context.getMulti(hierRowIds, true); 459 for (Fragment fragment : hierFragments) { 460 findPrefetchedFragments((SimpleFragment) fragment, bulkRowIds, null); 461 } 462 463 // we have everything to be prefetched 464 465 // fetch all the prefetches in bulk 466 List<Fragment> fragments = context.getMulti(bulkRowIds, true); 467 468 // put each fragment in the map of the proper group 469 for (Fragment fragment : fragments) { 470 FragmentGroup fragmentGroup = fragmentGroups.get(fragment.row.id); 471 if (fragmentGroup != null) { 472 fragmentGroup.fragments.put(fragment.row.tableName, fragment); 473 } 474 } 475 } 476 477 // assemble nodes from the fragment groups 478 List<Node> nodes = new ArrayList<>(ids.size()); 479 for (Serializable id : ids) { 480 FragmentGroup fragmentGroup = fragmentGroups.get(id); 481 // null if deleted/absent 482 Node node = fragmentGroup == null ? null : new Node(context, fragmentGroup, paths.get(id)); 483 nodes.add(node); 484 } 485 486 return nodes; 487 } 488 489 /** 490 * Finds prefetched fragments for a hierarchy fragment, takes note of the ones that are proxies. 491 */ 492 protected void findPrefetchedFragments(SimpleFragment hierFragment, List<RowId> bulkRowIds, 493 Set<Serializable> proxyIds) { 494 Serializable id = hierFragment.row.id; 495 496 // find type 497 String typeName = (String) hierFragment.get(Model.MAIN_PRIMARY_TYPE_KEY); 498 if (Model.PROXY_TYPE.equals(typeName)) { 499 if (proxyIds != null) { 500 proxyIds.add(id); 501 } 502 return; 503 } 504 505 // find table names 506 Set<String> tableNames = model.getTypePrefetchedFragments(typeName); 507 if (tableNames == null) { 508 return; // unknown (obsolete) type 509 } 510 511 // add row id for each table name 512 Serializable parentId = hierFragment.get(Model.HIER_PARENT_KEY); 513 for (String tableName : tableNames) { 514 if (Model.HIER_TABLE_NAME.equals(tableName)) { 515 continue; // already fetched 516 } 517 if (parentId != null && Model.VERSION_TABLE_NAME.equals(tableName)) { 518 continue; // not a version, don't fetch this table 519 // TODO incorrect if we have filed versions 520 } 521 bulkRowIds.add(new RowId(tableName, id)); 522 } 523 } 524 525 @Override 526 public List<Node> getNodesByIds(Collection<Serializable> ids) { 527 return getNodesByIds(ids, true); 528 } 529 530 @Override 531 public Node getParentNode(Node node) { 532 if (node == null) { 533 throw new IllegalArgumentException("Illegal null node"); 534 } 535 Serializable id = node.getHierFragment().get(Model.HIER_PARENT_KEY); 536 return id == null ? null : getNodeById(id); 537 } 538 539 @Override 540 public String getPath(Node node) { 541 String path = node.getPath(); 542 if (path == null) { 543 path = context.getPath(node.getHierFragment()); 544 } 545 return path; 546 } 547 548 /* 549 * Normalize using NFC to avoid decomposed characters (like 'e' + COMBINING ACUTE ACCENT instead of LATIN SMALL 550 * LETTER E WITH ACUTE). NFKC (normalization using compatibility decomposition) is not used, because compatibility 551 * decomposition turns some characters (LATIN SMALL LIGATURE FFI, TRADE MARK SIGN, FULLWIDTH SOLIDUS) into a series 552 * of characters ('f'+'f'+'i', 'T'+'M', '/') that cannot be re-composed into the original, and therefore loses 553 * information. 554 */ 555 protected String normalize(String path) { 556 return Normalizer.normalize(path, Normalizer.Form.NFC); 557 } 558 559 /* Does not apply to properties for now (no use case). */ 560 @Override 561 public Node getNodeByPath(String path, Node node) { 562 // TODO optimize this to use a dedicated path-based table 563 if (path == null) { 564 throw new IllegalArgumentException("Illegal null path"); 565 } 566 path = normalize(path); 567 int i; 568 if (path.startsWith("/")) { 569 node = getRootNode(); 570 if (path.equals("/")) { 571 return node; 572 } 573 i = 1; 574 } else { 575 if (node == null) { 576 throw new IllegalArgumentException("Illegal relative path with null node: " + path); 577 } 578 i = 0; 579 } 580 String[] names = path.split("/", -1); 581 for (; i < names.length; i++) { 582 String name = names[i]; 583 if (name.length() == 0) { 584 throw new IllegalArgumentException("Illegal path with empty component: " + path); 585 } 586 node = getChildNode(node, name, false); 587 if (node == null) { 588 return null; 589 } 590 } 591 return node; 592 } 593 594 @Override 595 public boolean addMixinType(Node node, String mixin) { 596 if (model.getMixinPropertyInfos(mixin) == null) { 597 throw new IllegalArgumentException("No such mixin: " + mixin); 598 } 599 if (model.getDocumentTypeFacets(node.getPrimaryType()).contains(mixin)) { 600 return false; // already present in type 601 } 602 List<String> list = new ArrayList<>(Arrays.asList(node.getMixinTypes())); 603 if (list.contains(mixin)) { 604 return false; // already present in node 605 } 606 Set<String> otherChildrenNames = getChildrenNames(node.getPrimaryType(), list); 607 list.add(mixin); 608 String[] mixins = list.toArray(new String[list.size()]); 609 node.hierFragment.put(Model.MAIN_MIXIN_TYPES_KEY, mixins); 610 // immediately create child nodes (for complex properties) in order 611 // to avoid concurrency issue later on 612 Map<String, String> childrenTypes = model.getMixinComplexChildren(mixin); 613 for (Entry<String, String> es : childrenTypes.entrySet()) { 614 String childName = es.getKey(); 615 String childType = es.getValue(); 616 // child may already exist if the schema is part of the primary type or another facet 617 if (otherChildrenNames.contains(childName)) { 618 continue; 619 } 620 addChildNode(node, childName, null, childType, true); 621 } 622 return true; 623 } 624 625 @Override 626 public boolean removeMixinType(Node node, String mixin) { 627 List<String> list = new ArrayList<>(Arrays.asList(node.getMixinTypes())); 628 if (!list.remove(mixin)) { 629 return false; // not present in node 630 } 631 String[] mixins = list.toArray(new String[list.size()]); 632 if (mixins.length == 0) { 633 mixins = null; 634 } 635 node.hierFragment.put(Model.MAIN_MIXIN_TYPES_KEY, mixins); 636 Set<String> otherChildrenNames = getChildrenNames(node.getPrimaryType(), list); 637 Map<String, String> childrenTypes = model.getMixinComplexChildren(mixin); 638 for (String childName : childrenTypes.keySet()) { 639 // child must be kept if the schema is part of primary type or another facet 640 if (otherChildrenNames.contains(childName)) { 641 continue; 642 } 643 Node child = getChildNode(node, childName, true); 644 removePropertyNode(child); 645 } 646 node.clearCache(); 647 return true; 648 } 649 650 @Override 651 public ScrollResult<String> scroll(String query, int batchSize, int keepAliveSeconds) { 652 return mapper.scroll(query, batchSize, keepAliveSeconds); 653 } 654 655 @Override 656 public ScrollResult<String> scroll(String query, QueryFilter queryFilter, int batchSize, int keepAliveSeconds) { 657 return mapper.scroll(query, queryFilter, batchSize, keepAliveSeconds); 658 } 659 660 @Override 661 public ScrollResult<String> scroll(String scrollId) { 662 return mapper.scroll(scrollId); 663 } 664 665 /** 666 * Gets complex children names defined by the primary type and the list of mixins. 667 */ 668 protected Set<String> getChildrenNames(String primaryType, List<String> mixins) { 669 Map<String, String> cc = model.getTypeComplexChildren(primaryType); 670 if (cc == null) { 671 cc = Collections.emptyMap(); 672 } 673 Set<String> childrenNames = new HashSet<>(cc.keySet()); 674 for (String mixin : mixins) { 675 cc = model.getMixinComplexChildren(mixin); 676 if (cc != null) { 677 childrenNames.addAll(cc.keySet()); 678 } 679 } 680 return childrenNames; 681 } 682 683 @Override 684 public Node addChildNode(Node parent, String name, Long pos, String typeName, boolean complexProp) { 685 if (pos == null && !complexProp && parent != null) { 686 pos = context.getNextPos(parent.getId(), complexProp); 687 } 688 return addChildNode(null, parent, name, pos, typeName, complexProp); 689 } 690 691 @Override 692 public Node addChildNode(Serializable id, Node parent, String name, Long pos, String typeName, 693 boolean complexProp) { 694 if (name == null) { 695 throw new IllegalArgumentException("Illegal null name"); 696 } 697 name = normalize(name); 698 if (name.contains("/") || name.equals(".") || name.equals("..")) { 699 throw new IllegalArgumentException("Illegal name: " + name); 700 } 701 if (!model.isType(typeName)) { 702 throw new IllegalArgumentException("Unknown type: " + typeName); 703 } 704 id = generateNewId(id); 705 Serializable parentId = parent == null ? null : parent.hierFragment.getId(); 706 Node node = addNode(id, parentId, name, pos, typeName, complexProp); 707 // immediately create child nodes (for complex properties) in order 708 // to avoid concurrency issue later on 709 Map<String, String> childrenTypes = model.getTypeComplexChildren(typeName); 710 for (Entry<String, String> es : childrenTypes.entrySet()) { 711 String childName = es.getKey(); 712 String childType = es.getValue(); 713 addChildNode(node, childName, null, childType, true); 714 } 715 return node; 716 } 717 718 protected Node addNode(Serializable id, Serializable parentId, String name, Long pos, String typeName, 719 boolean complexProp) { 720 requireReadAclsUpdate(); 721 // main info 722 Row hierRow = new Row(Model.HIER_TABLE_NAME, id); 723 hierRow.putNew(Model.HIER_PARENT_KEY, parentId); 724 hierRow.putNew(Model.HIER_CHILD_NAME_KEY, name); 725 hierRow.putNew(Model.HIER_CHILD_POS_KEY, pos); 726 hierRow.putNew(Model.MAIN_PRIMARY_TYPE_KEY, typeName); 727 hierRow.putNew(Model.HIER_CHILD_ISPROPERTY_KEY, Boolean.valueOf(complexProp)); 728 if (changeTokenEnabled) { 729 hierRow.putNew(Model.MAIN_SYS_CHANGE_TOKEN_KEY, Model.INITIAL_SYS_CHANGE_TOKEN); 730 } 731 SimpleFragment hierFragment = context.createHierarchyFragment(hierRow); 732 FragmentGroup fragmentGroup = new FragmentGroup(hierFragment, new FragmentsMap()); 733 return new Node(context, fragmentGroup, context.getPath(hierFragment)); 734 } 735 736 @Override 737 public Node addProxy(Serializable targetId, Serializable versionableId, Node parent, String name, Long pos) { 738 if (!repository.getRepositoryDescriptor().getProxiesEnabled()) { 739 throw new NuxeoException("Proxies are disabled by configuration"); 740 } 741 Node proxy = addChildNode(parent, name, pos, Model.PROXY_TYPE, false); 742 proxy.setSimpleProperty(Model.PROXY_TARGET_PROP, targetId); 743 proxy.setSimpleProperty(Model.PROXY_VERSIONABLE_PROP, versionableId); 744 if (changeTokenEnabled) { 745 proxy.setSimpleProperty(Model.MAIN_SYS_CHANGE_TOKEN_PROP, Model.INITIAL_SYS_CHANGE_TOKEN); 746 proxy.setSimpleProperty(Model.MAIN_CHANGE_TOKEN_PROP, Model.INITIAL_CHANGE_TOKEN); 747 } 748 SimpleFragment proxyFragment = (SimpleFragment) proxy.fragments.get(Model.PROXY_TABLE_NAME); 749 context.createdProxyFragment(proxyFragment); 750 return proxy; 751 } 752 753 @Override 754 public void setProxyTarget(Node proxy, Serializable targetId) { 755 if (!repository.getRepositoryDescriptor().getProxiesEnabled()) { 756 throw new NuxeoException("Proxies are disabled by configuration"); 757 } 758 SimpleProperty prop = proxy.getSimpleProperty(Model.PROXY_TARGET_PROP); 759 Serializable oldTargetId = prop.getValue(); 760 if (!oldTargetId.equals(targetId)) { 761 SimpleFragment proxyFragment = (SimpleFragment) proxy.fragments.get(Model.PROXY_TABLE_NAME); 762 context.removedProxyTarget(proxyFragment); 763 proxy.setSimpleProperty(Model.PROXY_TARGET_PROP, targetId); 764 context.addedProxyTarget(proxyFragment); 765 } 766 } 767 768 @Override 769 public boolean hasChildNode(Node parent, String name, boolean complexProp) { 770 // TODO could optimize further by not fetching the fragment at all 771 SimpleFragment fragment = context.getChildHierByName(parent.getId(), normalize(name), complexProp); 772 return fragment != null; 773 } 774 775 @Override 776 public Node getChildNode(Node parent, String name, boolean complexProp) { 777 if (name == null || name.contains("/") || name.equals(".") || name.equals("..")) { 778 throw new IllegalArgumentException("Illegal name: " + name); 779 } 780 SimpleFragment fragment = context.getChildHierByName(parent.getId(), name, complexProp); 781 return fragment == null ? null : getNodeById(fragment.getId()); 782 } 783 784 // TODO optimize with dedicated backend call 785 @Override 786 public boolean hasChildren(Node parent, boolean complexProp) { 787 List<SimpleFragment> children = context.getChildren(parent.getId(), null, complexProp); 788 if (complexProp) { 789 return !children.isEmpty(); 790 } 791 if (children.isEmpty()) { 792 return false; 793 } 794 // we have to check that type names are not obsolete, as they wouldn't be returned 795 // by getChildren and we must be consistent 796 SchemaManager schemaManager = Framework.getService(SchemaManager.class); 797 for (SimpleFragment simpleFragment : children) { 798 String primaryType = simpleFragment.getString(Model.MAIN_PRIMARY_TYPE_KEY); 799 if (primaryType.equals(Model.PROXY_TYPE)) { 800 Node node = getNodeById(simpleFragment.getId(), false); 801 Serializable targetId = node.getSimpleProperty(Model.PROXY_TARGET_PROP).getValue(); 802 if (targetId == null) { 803 // missing target, should not happen, ignore 804 continue; 805 } 806 Node target = getNodeById(targetId, false); 807 if (target == null) { 808 continue; 809 } 810 primaryType = target.getPrimaryType(); 811 } 812 DocumentType type = schemaManager.getDocumentType(primaryType); 813 if (type == null) { 814 // obsolete type, ignored in getChildren 815 continue; 816 } 817 return true; 818 } 819 return false; 820 } 821 822 @Override 823 public List<Node> getChildren(Node parent, String name, boolean complexProp) { 824 List<SimpleFragment> fragments = context.getChildren(parent.getId(), name, complexProp); 825 List<Node> nodes = new ArrayList<>(fragments.size()); 826 for (SimpleFragment fragment : fragments) { 827 Node node = getNodeById(fragment.getId()); 828 if (node == null) { 829 // cannot happen 830 log.error("Child node cannot be created: " + fragment.getId()); 831 continue; 832 } 833 nodes.add(node); 834 } 835 return nodes; 836 } 837 838 @Override 839 public void orderBefore(Node parent, Node source, Node dest) { 840 context.orderBefore(parent.getId(), source.getId(), dest == null ? null : dest.getId()); 841 } 842 843 @Override 844 public Node move(Node source, Node parent, String name) { 845 if (!parent.getId().equals(source.getParentId())) { 846 flush(); // needed when doing many moves for circular stuff 847 } 848 context.move(source, parent.getId(), name); 849 requireReadAclsUpdate(); 850 return source; 851 } 852 853 @Override 854 public Node copy(Node source, Node parent, String name, Consumer<Node> afterRecordCopy) { 855 flush(); 856 Consumer<Serializable> afterRecordCopyWithId = afterRecordCopy == null ? null 857 : recId -> afterRecordCopy.accept(getNodeById(recId)); 858 Serializable id = context.copy(source, parent.getId(), name, afterRecordCopyWithId); 859 requireReadAclsUpdate(); 860 return getNodeById(id); 861 } 862 863 @Override 864 public void removeNode(Node node, Consumer<Node> beforeRecordRemove) { 865 flush(); 866 // remove the lock using the lock manager 867 // TODO children locks? 868 Serializable id = node.getId(); 869 getLockManager().removeLock(model.idToString(id), null); 870 // find all descendants 871 List<NodeInfo> nodeInfos = context.getNodeAndDescendantsInfo(node.getHierFragment()); 872 873 // check that there is no retention / hold 874 Set<Serializable> undeletableIds = nodeInfos.stream() // 875 .filter(info -> info.isUndeletable) 876 .map(info -> info.id) 877 .collect(Collectors.toSet()); 878 if (!undeletableIds.isEmpty()) { 879 // in tests we may want to delete everything 880 boolean allowDeleteUndeletable = Framework.isBooleanPropertyTrue(PROP_ALLOW_DELETE_UNDELETABLE_DOCUMENTS); 881 if (!allowDeleteUndeletable) { 882 if (undeletableIds.contains(id)) { 883 throw new DocumentExistsException("Cannot remove " + id + ", it is under retention / hold"); 884 } else { 885 throw new DocumentExistsException("Cannot remove " + id + ", subdocument " 886 + undeletableIds.iterator().next() + " is under retention / hold"); 887 } 888 } 889 } 890 891 // pre-processing before record removal (notify the record blob manager) 892 if (beforeRecordRemove != null) { 893 nodeInfos.stream() // 894 .filter(info -> info.isRecord) 895 .map(info -> getNodeById(info.id)) 896 .forEach(beforeRecordRemove::accept); 897 } 898 899 // if a proxy target is removed, check that all proxies to it are removed 900 if (repository.getRepositoryDescriptor().getProxiesEnabled()) { 901 Set<Serializable> removedIds = nodeInfos.stream().map(info -> info.id).collect(Collectors.toSet()); 902 // find proxies pointing to any removed document 903 Set<Serializable> proxyIds = context.getTargetProxies(removedIds); 904 for (Serializable proxyId : proxyIds) { 905 if (!removedIds.contains(proxyId)) { 906 Node proxy = getNodeById(proxyId); 907 Serializable targetId = (Serializable) proxy.getSingle(Model.PROXY_TARGET_PROP); 908 throw new DocumentExistsException( 909 "Cannot remove " + id + ", subdocument " + targetId + " is the target of proxy " + proxyId); 910 } 911 } 912 } 913 914 // remove all nodes 915 context.removeNode(node.getHierFragment(), nodeInfos); 916 } 917 918 @Override 919 public void removePropertyNode(Node node) { 920 // no flush needed 921 context.removePropertyNode(node.getHierFragment()); 922 } 923 924 @Override 925 public Node checkIn(Node node, String label, String checkinComment) { 926 flush(); 927 Serializable id = context.checkIn(node, label, checkinComment); 928 requireReadAclsUpdate(); 929 // save to reflect changes immediately in database 930 flush(); 931 return getNodeById(id); 932 } 933 934 @Override 935 public void checkOut(Node node) { 936 context.checkOut(node); 937 requireReadAclsUpdate(); 938 } 939 940 @Override 941 public void restore(Node node, Node version) { 942 // save done inside method 943 context.restoreVersion(node, version); 944 requireReadAclsUpdate(); 945 } 946 947 @Override 948 public Node getVersionByLabel(Serializable versionSeriesId, String label) { 949 if (label == null) { 950 return null; 951 } 952 List<Node> versions = getVersions(versionSeriesId); 953 for (Node node : versions) { 954 String l = (String) node.getSimpleProperty(Model.VERSION_LABEL_PROP).getValue(); 955 if (label.equals(l)) { 956 return node; 957 } 958 } 959 return null; 960 } 961 962 @Override 963 public Node getLastVersion(Serializable versionSeriesId) { 964 List<Serializable> ids = context.getVersionIds(versionSeriesId); 965 return ids.isEmpty() ? null : getNodeById(ids.get(ids.size() - 1)); 966 } 967 968 @Override 969 public List<Node> getVersions(Serializable versionSeriesId) { 970 List<Serializable> ids = context.getVersionIds(versionSeriesId); 971 List<Node> nodes = new ArrayList<>(ids.size()); 972 for (Serializable id : ids) { 973 nodes.add(getNodeById(id)); 974 } 975 return nodes; 976 } 977 978 @Override 979 public List<Node> getProxies(Node document, Node parent) { 980 if (!repository.getRepositoryDescriptor().getProxiesEnabled()) { 981 return Collections.emptyList(); 982 } 983 984 List<Serializable> ids; 985 if (document.isVersion()) { 986 ids = context.getTargetProxyIds(document.getId()); 987 } else { 988 Serializable versionSeriesId; 989 if (document.isProxy()) { 990 versionSeriesId = document.getSimpleProperty(Model.PROXY_VERSIONABLE_PROP).getValue(); 991 } else { 992 versionSeriesId = document.getId(); 993 } 994 ids = context.getSeriesProxyIds(versionSeriesId); 995 } 996 997 List<Node> nodes = getNodes(ids); 998 999 if (parent != null) { 1000 // filter by parent 1001 Serializable parentId = parent.getId(); 1002 nodes.removeIf(node -> !parentId.equals(node.getParentId())); 1003 } 1004 1005 return nodes; 1006 } 1007 1008 protected List<Node> getNodes(List<Serializable> ids) { 1009 List<Node> nodes = new LinkedList<>(); 1010 for (Serializable id : ids) { 1011 Node node = getNodeById(id); 1012 if (node != null || Boolean.TRUE.booleanValue()) { // XXX 1013 // null if deleted, which means selection wasn't correctly 1014 // updated 1015 nodes.add(node); 1016 } 1017 } 1018 return nodes; 1019 } 1020 1021 @Override 1022 public List<Node> getProxies(Node document) { 1023 if (!repository.getRepositoryDescriptor().getProxiesEnabled()) { 1024 return Collections.emptyList(); 1025 } 1026 List<Serializable> ids = context.getTargetProxyIds(document.getId()); 1027 return getNodes(ids); 1028 } 1029 1030 /** 1031 * Fetches the hierarchy fragment for the given rows and all their ancestors. 1032 * 1033 * @param ids the fragment ids 1034 */ 1035 protected List<Fragment> getHierarchyAndAncestors(Collection<Serializable> ids) { 1036 Set<Serializable> allIds = mapper.getAncestorsIds(ids); 1037 allIds.addAll(ids); 1038 List<RowId> rowIds = new ArrayList<>(allIds.size()); 1039 for (Serializable id : allIds) { 1040 rowIds.add(new RowId(Model.HIER_TABLE_NAME, id)); 1041 } 1042 return context.getMulti(rowIds, true); 1043 } 1044 1045 @SuppressWarnings("resource") // Time.Context closed by stop() 1046 @Override 1047 public PartialList<Serializable> query(String query, QueryFilter queryFilter, boolean countTotal) { 1048 final Timer.Context timerContext = queryTimer.time(); 1049 try { 1050 return mapper.query(query, NXQL.NXQL, queryFilter, countTotal); 1051 } finally { 1052 timerContext.stop(); 1053 } 1054 } 1055 1056 @SuppressWarnings("resource") // Time.Context closed by stop() 1057 @Override 1058 public PartialList<Serializable> query(String query, String queryType, QueryFilter queryFilter, long countUpTo) { 1059 final Timer.Context timerContext = queryTimer.time(); 1060 try { 1061 return mapper.query(query, queryType, queryFilter, countUpTo); 1062 } finally { 1063 long duration = timerContext.stop(); 1064 if ((LOG_MIN_DURATION_NS >= 0) && (duration > LOG_MIN_DURATION_NS)) { 1065 String msg = String.format("duration_ms:\t%.2f\t%s %s\tquery\t%s", duration / 1000000.0, queryFilter, 1066 countUpToAsString(countUpTo), query); 1067 if (log.isTraceEnabled()) { 1068 log.info(msg, new Throwable("Slow query stack trace")); 1069 } else { 1070 log.info(msg); 1071 } 1072 } 1073 } 1074 } 1075 1076 private String countUpToAsString(long countUpTo) { 1077 if (countUpTo > 0) { 1078 return String.format("count total results up to %d", countUpTo); 1079 } 1080 return countUpTo == -1 ? "count total results UNLIMITED" : ""; 1081 } 1082 1083 protected static class QueryResultContext extends Exception { 1084 1085 private static final long serialVersionUID = 1L; 1086 1087 public final IterableQueryResult queryResult; 1088 1089 public QueryResultContext(IterableQueryResult queryResult) { 1090 super("queryAndFetch call context"); 1091 this.queryResult = queryResult; 1092 } 1093 } 1094 1095 protected final Set<QueryResultContext> queryResults = new HashSet<>(); 1096 1097 protected void noteQueryResult(IterableQueryResult result) { 1098 queryResults.add(new QueryResultContext(result)); 1099 } 1100 1101 protected void closeQueryResults() { 1102 for (QueryResultContext ctx : queryResults) { 1103 if (!ctx.queryResult.mustBeClosed()) { 1104 continue; 1105 } 1106 try { 1107 ctx.queryResult.close(); 1108 } catch (RuntimeException e) { 1109 log.error("Cannot close query result", e); 1110 } finally { 1111 log.warn("Closing a query results for you, check stack trace for allocating point", ctx); 1112 } 1113 } 1114 queryResults.clear(); 1115 } 1116 1117 @Override 1118 public IterableQueryResult queryAndFetch(String query, String queryType, QueryFilter queryFilter, 1119 Object... params) { 1120 return queryAndFetch(query, queryType, queryFilter, false, params); 1121 } 1122 1123 @SuppressWarnings("resource") // Time.Context closed by stop() 1124 @Override 1125 public IterableQueryResult queryAndFetch(String query, String queryType, QueryFilter queryFilter, 1126 boolean distinctDocuments, Object... params) { 1127 final Timer.Context timerContext = queryTimer.time(); 1128 try { 1129 IterableQueryResult result = mapper.queryAndFetch(query, queryType, queryFilter, distinctDocuments, params); 1130 noteQueryResult(result); 1131 return result; 1132 } finally { 1133 long duration = timerContext.stop(); 1134 if ((LOG_MIN_DURATION_NS >= 0) && (duration > LOG_MIN_DURATION_NS)) { 1135 String msg = String.format("duration_ms:\t%.2f\t%s\tqueryAndFetch\t%s", duration / 1000000.0, 1136 queryFilter, query); 1137 if (log.isTraceEnabled()) { 1138 log.info(msg, new Throwable("Slow query stack trace")); 1139 } else { 1140 log.info(msg); 1141 } 1142 } 1143 } 1144 } 1145 1146 @SuppressWarnings("resource") // Time.Context closed by stop() 1147 @Override 1148 public PartialList<Map<String, Serializable>> queryProjection(String query, String queryType, 1149 QueryFilter queryFilter, boolean distinctDocuments, long countUpTo, Object... params) { 1150 final Timer.Context timerContext = queryTimer.time(); 1151 try { 1152 return mapper.queryProjection(query, queryType, queryFilter, distinctDocuments, countUpTo, params); 1153 } finally { 1154 long duration = timerContext.stop(); 1155 if ((LOG_MIN_DURATION_NS >= 0) && (duration > LOG_MIN_DURATION_NS)) { 1156 String msg = String.format("duration_ms:\t%.2f\t%s\tqueryProjection\t%s", duration / 1000000.0, 1157 queryFilter, query); 1158 if (log.isTraceEnabled()) { 1159 log.info(msg, new Throwable("Slow query stack trace")); 1160 } else { 1161 log.info(msg); 1162 } 1163 } 1164 } 1165 } 1166 1167 @Override 1168 public LockManager getLockManager() { 1169 return repository.getLockManager(); 1170 } 1171 1172 @Override 1173 public void requireReadAclsUpdate() { 1174 readAclsChanged = true; 1175 } 1176 1177 @Override 1178 public void updateReadAcls() { 1179 @SuppressWarnings("resource") 1180 final Timer.Context timerContext = aclrUpdateTimer.time(); 1181 try { 1182 mapper.updateReadAcls(); 1183 readAclsChanged = false; 1184 } finally { 1185 timerContext.stop(); 1186 } 1187 } 1188 1189 @Override 1190 public void rebuildReadAcls() { 1191 mapper.rebuildReadAcls(); 1192 readAclsChanged = false; 1193 } 1194 1195 private void computeRootNode() { 1196 String repositoryId = repository.getName(); 1197 Serializable rootId = mapper.getRootId(repositoryId); 1198 if (rootId == null && COMPAT_REPOSITORY_NAME) { 1199 // compat, old repositories had fixed id "default" 1200 rootId = mapper.getRootId("default"); 1201 } 1202 if (rootId == null) { 1203 log.debug("Creating root"); 1204 addRootNode(); 1205 save(); 1206 // record information about the root id 1207 mapper.setRootId(repositoryId, rootNodeId); 1208 } else { 1209 rootNodeId = rootId; 1210 } 1211 } 1212 1213 // TODO factor with addChildNode 1214 private Node addRootNode() { 1215 rootNodeId = generateNewId(null); 1216 Node rootNode = addNode(rootNodeId, null, "", null, Model.ROOT_TYPE, false); 1217 addRootACP(rootNode); 1218 return rootNode; 1219 } 1220 1221 private void addRootACP(Node rootNode) { 1222 ACLRow[] aclrows = new ACLRow[3]; 1223 // TODO put groups in their proper place. like that now for consistency. 1224 aclrows[0] = new ACLRow(0, ACL.LOCAL_ACL, true, SecurityConstants.EVERYTHING, SecurityConstants.ADMINISTRATORS, 1225 null); 1226 aclrows[1] = new ACLRow(1, ACL.LOCAL_ACL, true, SecurityConstants.EVERYTHING, SecurityConstants.ADMINISTRATOR, 1227 null); 1228 aclrows[2] = new ACLRow(2, ACL.LOCAL_ACL, true, SecurityConstants.READ, SecurityConstants.MEMBERS, null); 1229 rootNode.setCollectionProperty(Model.ACL_PROP, aclrows); 1230 requireReadAclsUpdate(); 1231 } 1232 1233 public void markReferencedBinaries() { 1234 mapper.markReferencedBinaries(); 1235 } 1236 1237 public int cleanupDeletedDocuments(int max, Calendar beforeTime) { 1238 if (!repository.getRepositoryDescriptor().getSoftDeleteEnabled()) { 1239 return 0; 1240 } 1241 return mapper.cleanupDeletedRows(max, beforeTime); 1242 } 1243 1244 /* 1245 * ----- Transaction management ----- 1246 */ 1247 1248 public void start() { 1249 inTransaction = true; 1250 processReceivedInvalidations(); 1251 } 1252 1253 public void end() { 1254 closeQueryResults(); 1255 try { 1256 flush(); 1257 } catch (ConcurrentUpdateException e) { 1258 TransactionHelper.setTransactionRollbackOnly(); 1259 throw e; 1260 } 1261 } 1262 1263 public void commit() { 1264 try { 1265 sendInvalidationsToOthers(); 1266 } finally { 1267 inTransaction = false; 1268 } 1269 } 1270 1271 public void rollback() { 1272 try { 1273 try { 1274 mapper.rollback(); 1275 } finally { 1276 context.clearCaches(); 1277 } 1278 } finally { 1279 inTransaction = false; 1280 } 1281 } 1282 1283 public long getCacheSize() { 1284 return context.getCacheSize(); 1285 } 1286 1287 public long getCacheMapperSize() { 1288 return context.getCacheMapperSize(); 1289 } 1290 1291 public long getCachePristineSize() { 1292 return context.getCachePristineSize(); 1293 } 1294 1295 public long getCacheSelectionSize() { 1296 return context.getCacheSelectionSize(); 1297 } 1298 1299 @Override 1300 public boolean isFulltextStoredInBlob() { 1301 return fulltextDescriptor.getFulltextStoredInBlob(); 1302 } 1303 1304 @Override 1305 public Map<String, String> getBinaryFulltext(Serializable id, Document doc) { 1306 if (fulltextDescriptor.getFulltextDisabled()) { 1307 return null; 1308 } 1309 RowId rowId = new RowId(Model.FULLTEXT_TABLE_NAME, id); 1310 Map<String, String> map = mapper.getBinaryFulltext(rowId); 1311 String fulltext = map.get(BINARY_FULLTEXT_MAIN_KEY); 1312 if (fulltextDescriptor.getFulltextStoredInBlob() && fulltext != null) { 1313 if (doc == null) { 1314 // could not find doc (shouldn't happen) 1315 fulltext = null; 1316 } else { 1317 // fulltext is actually the blob key 1318 // now retrieve the actual fulltext from the blob content 1319 DocumentBlobManager blobManager = Framework.getService(DocumentBlobManager.class); 1320 try { 1321 BlobInfo blobInfo = new BlobInfo(); 1322 blobInfo.key = fulltext; 1323 String xpath = BaseDocument.FULLTEXT_BINARYTEXT_PROP; 1324 Blob blob = blobManager.readBlob(blobInfo, doc, xpath); 1325 fulltext = blob.getString(); 1326 } catch (IOException e) { 1327 throw new PropertyException("Cannot read fulltext blob for doc: " + id, e); 1328 } 1329 } 1330 map.put(BINARY_FULLTEXT_MAIN_KEY, fulltext); 1331 } 1332 return map; 1333 } 1334 1335 @Override 1336 public boolean isChangeTokenEnabled() { 1337 return changeTokenEnabled; 1338 } 1339 1340 @Override 1341 public void markUserChange(Serializable id) { 1342 context.markUserChange(id); 1343 } 1344 1345}