001/* 002 * (C) Copyright 2014 Nuxeo SA (http://nuxeo.com/) and others. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 * 016 * Contributors: 017 * Florent Guillaume 018 */ 019package org.nuxeo.ecm.core.storage.dbs; 020 021import static java.lang.Boolean.FALSE; 022import static java.lang.Boolean.TRUE; 023import static org.nuxeo.ecm.core.api.security.SecurityConstants.BROWSE; 024import static org.nuxeo.ecm.core.api.security.SecurityConstants.EVERYONE; 025import static org.nuxeo.ecm.core.api.security.SecurityConstants.UNSUPPORTED_ACL; 026import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_ACE_GRANT; 027import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_ACE_PERMISSION; 028import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_ACE_USER; 029import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_ACL; 030import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_ACP; 031import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_ANCESTOR_IDS; 032import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_BASE_VERSION_ID; 033import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_FULLTEXT_BINARY; 034import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_FULLTEXT_JOBID; 035import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_FULLTEXT_SIMPLE; 036import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_ID; 037import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_IS_PROXY; 038import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_IS_VERSION; 039import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_MIXIN_TYPES; 040import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_NAME; 041import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_PARENT_ID; 042import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_POS; 043import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_PREFIX; 044import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_PRIMARY_TYPE; 045import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_PROXY_IDS; 046import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_PROXY_TARGET_ID; 047import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_PROXY_VERSION_SERIES_ID; 048import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_READ_ACL; 049import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_VERSION_SERIES_ID; 050 051import java.io.Serializable; 052import java.util.ArrayList; 053import java.util.Arrays; 054import java.util.Collections; 055import java.util.HashMap; 056import java.util.HashSet; 057import java.util.LinkedHashSet; 058import java.util.LinkedList; 059import java.util.List; 060import java.util.Map; 061import java.util.Map.Entry; 062import java.util.Set; 063 064import org.apache.commons.lang.StringUtils; 065import org.apache.commons.logging.Log; 066import org.apache.commons.logging.LogFactory; 067import org.nuxeo.ecm.core.api.ConcurrentUpdateException; 068import org.nuxeo.ecm.core.api.repository.RepositoryManager; 069import org.nuxeo.ecm.core.schema.SchemaManager; 070import org.nuxeo.ecm.core.schema.types.Schema; 071import org.nuxeo.ecm.core.security.SecurityService; 072import org.nuxeo.ecm.core.storage.State.ListDiff; 073import org.nuxeo.ecm.core.storage.State.StateDiff; 074import org.nuxeo.ecm.core.storage.StateHelper; 075import org.nuxeo.ecm.core.storage.DefaultFulltextParser; 076import org.nuxeo.ecm.core.storage.FulltextConfiguration; 077import org.nuxeo.ecm.core.storage.FulltextParser; 078import org.nuxeo.ecm.core.storage.FulltextUpdaterWork; 079import org.nuxeo.ecm.core.storage.State; 080import org.nuxeo.ecm.core.storage.FulltextUpdaterWork.IndexAndText; 081import org.nuxeo.ecm.core.work.api.Work; 082import org.nuxeo.ecm.core.work.api.WorkManager; 083import org.nuxeo.ecm.core.work.api.WorkManager.Scheduling; 084import org.nuxeo.runtime.api.Framework; 085 086/** 087 * Transactional state for a session. 088 * <p> 089 * Until {@code save()} is called, data lives in the transient map. 090 * <p> 091 * Upon save, data is written to the repository, even though it has not yet been committed (this means that other 092 * sessions can read uncommitted data). It's also kept in an undo log in order for rollback to be possible. 093 * <p> 094 * On commit, the undo log is forgotten. On rollback, the undo log is replayed. 095 * 096 * @since 5.9.4 097 */ 098public class DBSTransactionState { 099 100 private static final Log log = LogFactory.getLog(DBSTransactionState.class); 101 102 private static final String KEY_UNDOLOG_CREATE = "__UNDOLOG_CREATE__\0\0"; 103 104 protected final DBSRepository repository; 105 106 protected final DBSSession session; 107 108 /** Retrieved and created document state. */ 109 protected Map<String, DBSDocumentState> transientStates = new HashMap<String, DBSDocumentState>(); 110 111 /** Ids of documents created but not yet saved. */ 112 protected Set<String> transientCreated = new LinkedHashSet<String>(); 113 114 /** 115 * Undo log. 116 * <p> 117 * A map of document ids to null or State. The value is null when the document has to be deleted when applying the 118 * undo log. Otherwise the value is a State. If the State contains the key {@link #KEY_UNDOLOG_CREATE} then the 119 * state must be re-created completely when applying the undo log, otherwise just applied as an update. 120 * <p> 121 * Null when there is no active transaction. 122 */ 123 protected Map<String, State> undoLog; 124 125 protected final Set<String> browsePermissions; 126 127 public DBSTransactionState(DBSRepository repository, DBSSession session) { 128 this.repository = repository; 129 this.session = session; 130 SecurityService securityService = Framework.getLocalService(SecurityService.class); 131 browsePermissions = new HashSet<>(Arrays.asList(securityService.getPermissionsToCheck(BROWSE))); 132 } 133 134 /** 135 * New transient state for something just read from the repository. 136 */ 137 protected DBSDocumentState newTransientState(State state) { 138 if (state == null) { 139 return null; 140 } 141 String id = (String) state.get(KEY_ID); 142 if (transientStates.containsKey(id)) { 143 throw new IllegalStateException("Already transient: " + id); 144 } 145 DBSDocumentState docState = new DBSDocumentState(state); // copy 146 transientStates.put(id, docState); 147 return docState; 148 } 149 150 /** 151 * Returns a state and marks it as transient, because it's about to be modified or returned to user code (where it 152 * may be modified). 153 */ 154 public DBSDocumentState getStateForUpdate(String id) { 155 // check transient state 156 DBSDocumentState docState = transientStates.get(id); 157 if (docState != null) { 158 return docState; 159 } 160 // fetch from repository 161 State state = repository.readState(id); 162 return newTransientState(state); 163 } 164 165 /** 166 * Returns a state which won't be modified. 167 */ 168 // TODO in some cases it's good to have this kept in memory instead of 169 // rereading from database every time 170 // XXX getStateForReadOneShot 171 public State getStateForRead(String id) { 172 // check transient state 173 DBSDocumentState docState = transientStates.get(id); 174 if (docState != null) { 175 return docState.getState(); 176 } 177 // fetch from repository 178 return repository.readState(id); 179 } 180 181 /** 182 * Returns states and marks them transient, because they're about to be returned to user code (where they may be 183 * modified). 184 */ 185 public List<DBSDocumentState> getStatesForUpdate(List<String> ids) { 186 // check which ones we have to fetch from repository 187 List<String> idsToFetch = new LinkedList<String>(); 188 for (String id : ids) { 189 // check transient state 190 DBSDocumentState docState = transientStates.get(id); 191 if (docState != null) { 192 continue; 193 } 194 // will have to fetch it 195 idsToFetch.add(id); 196 } 197 if (!idsToFetch.isEmpty()) { 198 List<State> states = repository.readStates(idsToFetch); 199 for (State state : states) { 200 newTransientState(state); 201 } 202 } 203 // everything now fetched in transient 204 List<DBSDocumentState> docStates = new ArrayList<DBSDocumentState>(ids.size()); 205 for (String id : ids) { 206 DBSDocumentState docState = transientStates.get(id); 207 if (docState != null) { 208 docStates.add(docState); 209 } else { 210 log.warn("Cannot fetch document with id: " + id, new Throwable("debug stack trace")); 211 } 212 } 213 return docStates; 214 } 215 216 // XXX TODO for update or for read? 217 public DBSDocumentState getChildState(String parentId, String name) { 218 Set<String> seen = new HashSet<String>(); 219 // check transient state 220 for (DBSDocumentState docState : transientStates.values()) { 221 seen.add(docState.getId()); 222 if (!parentId.equals(docState.getParentId())) { 223 continue; 224 } 225 if (!name.equals(docState.getName())) { 226 continue; 227 } 228 return docState; 229 } 230 // fetch from repository 231 State state = repository.readChildState(parentId, name, seen); 232 return newTransientState(state); 233 } 234 235 public boolean hasChild(String parentId, String name) { 236 Set<String> seen = new HashSet<String>(); 237 // check transient state 238 for (DBSDocumentState docState : transientStates.values()) { 239 seen.add(docState.getId()); 240 if (!parentId.equals(docState.getParentId())) { 241 continue; 242 } 243 if (!name.equals(docState.getName())) { 244 continue; 245 } 246 return true; 247 } 248 // check repository 249 return repository.hasChild(parentId, name, seen); 250 } 251 252 public List<DBSDocumentState> getChildrenStates(String parentId) { 253 List<DBSDocumentState> docStates = new LinkedList<DBSDocumentState>(); 254 Set<String> seen = new HashSet<String>(); 255 // check transient state 256 for (DBSDocumentState docState : transientStates.values()) { 257 seen.add(docState.getId()); 258 if (!parentId.equals(docState.getParentId())) { 259 continue; 260 } 261 docStates.add(docState); 262 } 263 // fetch from repository 264 List<State> states = repository.queryKeyValue(KEY_PARENT_ID, parentId, seen); 265 for (State state : states) { 266 docStates.add(newTransientState(state)); 267 } 268 return docStates; 269 } 270 271 public List<String> getChildrenIds(String parentId) { 272 List<String> children = new ArrayList<String>(); 273 Set<String> seen = new HashSet<String>(); 274 // check transient state 275 for (DBSDocumentState docState : transientStates.values()) { 276 String id = docState.getId(); 277 seen.add(id); 278 if (!parentId.equals(docState.getParentId())) { 279 continue; 280 } 281 children.add(id); 282 } 283 // fetch from repository 284 List<State> states = repository.queryKeyValue(KEY_PARENT_ID, parentId, seen); 285 for (State state : states) { 286 children.add((String) state.get(KEY_ID)); 287 } 288 return new ArrayList<String>(children); 289 } 290 291 public boolean hasChildren(String parentId) { 292 Set<String> seen = new HashSet<String>(); 293 // check transient state 294 for (DBSDocumentState docState : transientStates.values()) { 295 seen.add(docState.getId()); 296 if (!parentId.equals(docState.getParentId())) { 297 continue; 298 } 299 return true; 300 } 301 // check repository 302 return repository.queryKeyValuePresence(KEY_PARENT_ID, parentId, seen); 303 } 304 305 public DBSDocumentState createChild(String id, String parentId, String name, Long pos, String typeName) { 306 // id may be not-null for import 307 if (id == null) { 308 id = repository.generateNewId(); 309 } 310 transientCreated.add(id); 311 DBSDocumentState docState = new DBSDocumentState(); 312 transientStates.put(id, docState); 313 docState.put(KEY_ID, id); 314 docState.put(KEY_PARENT_ID, parentId); 315 docState.put(KEY_ANCESTOR_IDS, getAncestorIds(parentId)); 316 docState.put(KEY_NAME, name); 317 docState.put(KEY_POS, pos); 318 docState.put(KEY_PRIMARY_TYPE, typeName); 319 // update read acls for new doc 320 updateReadAcls(id); 321 return docState; 322 } 323 324 /** Gets ancestors including id itself. */ 325 protected Object[] getAncestorIds(String id) { 326 if (id == null) { 327 return null; 328 } 329 State state = getStateForRead(id); 330 if (state == null) { 331 throw new RuntimeException("No such id: " + id); 332 } 333 Object[] ancestors = (Object[]) state.get(KEY_ANCESTOR_IDS); 334 if (ancestors == null) { 335 return new Object[] { id }; 336 } else { 337 Object[] newAncestors = new Object[ancestors.length + 1]; 338 System.arraycopy(ancestors, 0, newAncestors, 0, ancestors.length); 339 newAncestors[ancestors.length] = id; 340 return newAncestors; 341 } 342 } 343 344 /** 345 * Copies the document into a newly-created object. 346 * <p> 347 * The copy is automatically saved. 348 */ 349 public DBSDocumentState copy(String id) { 350 DBSDocumentState copyState = new DBSDocumentState(getStateForRead(id)); 351 String copyId = repository.generateNewId(); 352 copyState.put(KEY_ID, copyId); 353 copyState.put(KEY_PROXY_IDS, null); // no proxies to this new doc 354 // other fields updated by the caller 355 transientStates.put(copyId, copyState); 356 transientCreated.add(copyId); 357 return copyState; 358 } 359 360 /** 361 * Updates ancestors recursively after a move. 362 * <p> 363 * Recursing from given doc, replace the first ndel ancestors with those passed. 364 * <p> 365 * Doesn't check transient (assumes save is done). The modifications are automatically saved. 366 */ 367 public void updateAncestors(String id, int ndel, Object[] ancestorIds) { 368 int nadd = ancestorIds.length; 369 Set<String> ids = getSubTree(id, null, null); 370 ids.add(id); 371 for (String cid : ids) { 372 // XXX TODO oneShot update, don't pollute transient space 373 DBSDocumentState docState = getStateForUpdate(cid); 374 Object[] ancestors = (Object[]) docState.get(KEY_ANCESTOR_IDS); 375 Object[] newAncestors; 376 if (ancestors == null) { 377 newAncestors = ancestorIds.clone(); 378 } else { 379 newAncestors = new Object[ancestors.length - ndel + nadd]; 380 System.arraycopy(ancestorIds, 0, newAncestors, 0, nadd); 381 System.arraycopy(ancestors, ndel, newAncestors, nadd, ancestors.length - ndel); 382 } 383 docState.put(KEY_ANCESTOR_IDS, newAncestors); 384 } 385 } 386 387 /** 388 * Updates the Read ACLs recursively on a document. 389 */ 390 public void updateReadAcls(String id) { 391 // versions too XXX TODO 392 Set<String> ids = getSubTree(id, null, null); 393 ids.add(id); 394 for (String cid : ids) { 395 // XXX TODO oneShot update, don't pollute transient space 396 DBSDocumentState docState = getStateForUpdate(cid); 397 docState.put(KEY_READ_ACL, getReadACL(docState)); 398 } 399 } 400 401 /** 402 * Gets the Read ACL (flat list of users having browse permission, including inheritance) on a document. 403 */ 404 protected String[] getReadACL(DBSDocumentState docState) { 405 Set<String> racls = new HashSet<>(); 406 State state = docState.getState(); 407 LOOP: do { 408 @SuppressWarnings("unchecked") 409 List<Serializable> aclList = (List<Serializable>) state.get(KEY_ACP); 410 if (aclList != null) { 411 for (Serializable aclSer : aclList) { 412 State aclMap = (State) aclSer; 413 @SuppressWarnings("unchecked") 414 List<Serializable> aceList = (List<Serializable>) aclMap.get(KEY_ACL); 415 for (Serializable aceSer : aceList) { 416 State aceMap = (State) aceSer; 417 String username = (String) aceMap.get(KEY_ACE_USER); 418 String permission = (String) aceMap.get(KEY_ACE_PERMISSION); 419 Boolean granted = (Boolean) aceMap.get(KEY_ACE_GRANT); 420 if (TRUE.equals(granted) && browsePermissions.contains(permission)) { 421 racls.add(username); 422 } 423 if (FALSE.equals(granted)) { 424 if (!EVERYONE.equals(username)) { 425 // TODO log 426 racls.add(UNSUPPORTED_ACL); 427 } 428 break LOOP; 429 } 430 } 431 } 432 } 433 // get parent 434 if (TRUE.equals(state.get(KEY_IS_VERSION))) { 435 String versionSeriesId = (String) state.get(KEY_VERSION_SERIES_ID); 436 state = versionSeriesId == null ? null : getStateForRead(versionSeriesId); 437 } else { 438 String parentId = (String) state.get(KEY_PARENT_ID); 439 state = parentId == null ? null : getStateForRead(parentId); 440 } 441 } while (state != null); 442 443 // sort to have canonical order 444 List<String> racl = new ArrayList<>(racls); 445 Collections.sort(racl); 446 return racl.toArray(new String[racl.size()]); 447 } 448 449 /** 450 * Gets all the ids under a given one, recursively. 451 * <p> 452 * Doesn't check transient (assumes save is done). 453 * 454 * @param id the root of the tree (not included in results) 455 * @param proxyTargets returns a map of proxy to target among the documents found 456 * @param targetProxies returns a map of target to proxies among the document found 457 */ 458 protected Set<String> getSubTree(String id, Map<String, String> proxyTargets, Map<String, Object[]> targetProxies) { 459 Set<String> ids = new HashSet<String>(); 460 // check repository 461 repository.queryKeyValueArray(KEY_ANCESTOR_IDS, id, ids, proxyTargets, targetProxies); 462 return ids; 463 } 464 465 public List<DBSDocumentState> getKeyValuedStates(String key, Object value) { 466 List<DBSDocumentState> docStates = new LinkedList<DBSDocumentState>(); 467 Set<String> seen = new HashSet<String>(); 468 // check transient state 469 for (DBSDocumentState docState : transientStates.values()) { 470 seen.add(docState.getId()); 471 if (!value.equals(docState.get(key))) { 472 continue; 473 } 474 docStates.add(docState); 475 } 476 // fetch from repository 477 List<State> states = repository.queryKeyValue(key, value, seen); 478 for (State state : states) { 479 docStates.add(newTransientState(state)); 480 } 481 return docStates; 482 } 483 484 public List<DBSDocumentState> getKeyValuedStates(String key1, Object value1, String key2, Object value2) { 485 List<DBSDocumentState> docStates = new LinkedList<DBSDocumentState>(); 486 Set<String> seen = new HashSet<String>(); 487 // check transient state 488 for (DBSDocumentState docState : transientStates.values()) { 489 seen.add(docState.getId()); 490 if (!(value1.equals(docState.get(key1)) && value2.equals(docState.get(key2)))) { 491 continue; 492 } 493 docStates.add(docState); 494 } 495 // fetch from repository 496 List<State> states = repository.queryKeyValue(key1, value1, key2, value2, seen); 497 for (State state : states) { 498 docStates.add(newTransientState(state)); 499 } 500 return docStates; 501 } 502 503 /** 504 * Removes a list of documents. 505 * <p> 506 * Called after a {@link #save} has been done. 507 */ 508 public void removeStates(Set<String> ids) { 509 if (undoLog != null) { 510 for (String id : ids) { 511 if (undoLog.containsKey(id)) { 512 // there's already a create or an update in the undo log 513 State oldUndo = undoLog.get(id); 514 if (oldUndo == null) { 515 // create + delete -> forget 516 undoLog.remove(id); 517 } else { 518 // update + delete -> original old state to re-create 519 oldUndo.put(KEY_UNDOLOG_CREATE, TRUE); 520 } 521 } else { 522 // just delete -> store old state to re-create 523 State oldState = StateHelper.deepCopy(getStateForRead(id)); 524 oldState.put(KEY_UNDOLOG_CREATE, TRUE); 525 undoLog.put(id, oldState); 526 } 527 } 528 } 529 for (String id : ids) { 530 transientStates.remove(id); 531 } 532 repository.deleteStates(ids); 533 } 534 535 /** 536 * Writes transient state to database. 537 * <p> 538 * An undo log is kept in order to rollback the transaction later if needed. 539 */ 540 public void save() { 541 updateProxies(); 542 List<Work> works; 543 if (!repository.isFulltextDisabled()) { 544 // TODO getting fulltext already does a getStateChange 545 works = getFulltextWorks(); 546 } else { 547 works = Collections.emptyList(); 548 } 549 for (String id : transientCreated) { // ordered 550 DBSDocumentState docState = transientStates.get(id); 551 docState.setNotDirty(); 552 if (undoLog != null) { 553 undoLog.put(id, null); // marker to denote create 554 } 555 repository.createState(docState.getState()); 556 } 557 for (DBSDocumentState docState : transientStates.values()) { 558 String id = docState.getId(); 559 if (transientCreated.contains(id)) { 560 continue; // already done 561 } 562 StateDiff diff = docState.getStateChange(); 563 if (diff != null) { 564 if (undoLog != null) { 565 if (!undoLog.containsKey(id)) { 566 undoLog.put(id, StateHelper.deepCopy(docState.getOriginalState())); 567 } 568 // else there's already a create or an update in the undo log so original info is enough 569 } 570 repository.updateState(id, diff); 571 } 572 docState.setNotDirty(); 573 } 574 transientCreated.clear(); 575 scheduleWork(works); 576 } 577 578 protected void applyUndoLog() { 579 Set<String> deletes = new HashSet<>(); 580 for (Entry<String, State> es : undoLog.entrySet()) { 581 String id = es.getKey(); 582 State state = es.getValue(); 583 if (state == null) { 584 deletes.add(id); 585 } else { 586 boolean recreate = state.remove(KEY_UNDOLOG_CREATE) != null; 587 if (recreate) { 588 repository.createState(state); 589 } else { 590 // undo update 591 State currentState = repository.readState(id); 592 if (currentState != null) { 593 StateDiff diff = StateHelper.diff(currentState, state); 594 if (!diff.isEmpty()) { 595 repository.updateState(id, diff); 596 } 597 } 598 // else we expected to read a current state but it was concurrently deleted... 599 // in that case leave it deleted 600 } 601 } 602 } 603 if (!deletes.isEmpty()) { 604 repository.deleteStates(deletes); 605 } 606 } 607 608 /** 609 * Checks if the changed documents are proxy targets, and updates the proxies if that's the case. 610 */ 611 protected void updateProxies() { 612 for (String id : transientCreated) { // ordered 613 DBSDocumentState docState = transientStates.get(id); 614 updateProxies(docState); 615 } 616 // copy as we may modify proxies 617 for (String id : transientStates.keySet().toArray(new String[0])) { 618 DBSDocumentState docState = transientStates.get(id); 619 if (transientCreated.contains(id)) { 620 continue; // already done 621 } 622 if (docState.isDirty()) { 623 updateProxies(docState); 624 } 625 } 626 } 627 628 protected void updateProxies(DBSDocumentState target) { 629 Object[] proxyIds = (Object[]) target.get(KEY_PROXY_IDS); 630 if (proxyIds != null) { 631 for (Object proxyId : proxyIds) { 632 try { 633 updateProxy(target, (String) proxyId); 634 } catch (ConcurrentUpdateException e) { 635 e.addInfo("On doc " + target.getId()); 636 throw e; 637 } 638 } 639 } 640 } 641 642 /** 643 * Updates the state of a proxy based on its target. 644 */ 645 protected void updateProxy(DBSDocumentState target, String proxyId) { 646 DBSDocumentState proxy = getStateForUpdate(proxyId); 647 if (proxy == null) { 648 throw new ConcurrentUpdateException("Proxy " + proxyId + " concurrently deleted"); 649 } 650 SchemaManager schemaManager = Framework.getService(SchemaManager.class); 651 // clear all proxy data 652 for (String key : proxy.getState().keyArray()) { 653 if (!isProxySpecific(key, schemaManager)) { 654 proxy.put(key, null); 655 } 656 } 657 // copy from target 658 for (Entry<String, Serializable> en : target.getState().entrySet()) { 659 String key = en.getKey(); 660 if (!isProxySpecific(key, schemaManager)) { 661 proxy.put(key, StateHelper.deepCopy(en.getValue())); 662 } 663 } 664 } 665 666 /** 667 * Things that we don't touch on a proxy when updating it. 668 */ 669 protected boolean isProxySpecific(String key, SchemaManager schemaManager) { 670 switch (key) { 671 // these are placeful stuff 672 case KEY_ID: 673 case KEY_PARENT_ID: 674 case KEY_ANCESTOR_IDS: 675 case KEY_NAME: 676 case KEY_POS: 677 case KEY_ACP: 678 case KEY_READ_ACL: 679 // these are proxy-specific 680 case KEY_IS_PROXY: 681 case KEY_PROXY_TARGET_ID: 682 case KEY_PROXY_VERSION_SERIES_ID: 683 case KEY_IS_VERSION: 684 case KEY_PROXY_IDS: 685 return true; 686 } 687 int p = key.indexOf(':'); 688 if (p == -1) { 689 // no prefix, assume not proxy-specific 690 return false; 691 } 692 String prefix = key.substring(0, p); 693 Schema schema = schemaManager.getSchemaFromPrefix(prefix); 694 if (schema == null) { 695 schema = schemaManager.getSchema(prefix); 696 if (schema == null) { 697 // unknown prefix, assume not proxy-specific 698 return false; 699 } 700 } 701 return schemaManager.isProxySchema(schema.getName(), null); // type unused 702 } 703 704 /** 705 * Called when created in a transaction. 706 * 707 * @since 7.4 708 */ 709 public void begin() { 710 undoLog = new HashMap<String, State>(); 711 } 712 713 /** 714 * Saves and flushes to database. 715 */ 716 public void commit() { 717 save(); 718 commitSave(); 719 } 720 721 /** 722 * Commits the saved state to the database. 723 */ 724 protected void commitSave() { 725 // clear transient, this means that after this references to states will be stale 726 // TODO mark states as invalid 727 clearTransient(); 728 // the transaction ended, the proxied DBSSession will disappear and cannot be reused anyway 729 undoLog = null; 730 } 731 732 /** 733 * Rolls back the save state by applying the undo log. 734 */ 735 public void rollback() { 736 clearTransient(); 737 applyUndoLog(); 738 // the transaction ended, the proxied DBSSession will disappear and cannot be reused anyway 739 undoLog = null; 740 } 741 742 protected void clearTransient() { 743 transientStates.clear(); 744 transientCreated.clear(); 745 } 746 747 /** 748 * Gets the fulltext updates to do. Called at save() time. 749 * 750 * @return a list of {@link Work} instances to schedule post-commit. 751 */ 752 protected List<Work> getFulltextWorks() { 753 Set<String> docsWithDirtyStrings = new HashSet<String>(); 754 Set<String> docsWithDirtyBinaries = new HashSet<String>(); 755 findDirtyDocuments(docsWithDirtyStrings, docsWithDirtyBinaries); 756 if (docsWithDirtyStrings.isEmpty() && docsWithDirtyBinaries.isEmpty()) { 757 return Collections.emptyList(); 758 } 759 List<Work> works = new LinkedList<Work>(); 760 getFulltextSimpleWorks(works, docsWithDirtyStrings); 761 getFulltextBinariesWorks(works, docsWithDirtyBinaries); 762 return works; 763 } 764 765 /** 766 * Finds the documents having dirty text or dirty binaries that have to be reindexed as fulltext. 767 * 768 * @param docsWithDirtyStrings set of ids, updated by this method 769 * @param docWithDirtyBinaries set of ids, updated by this method 770 */ 771 protected void findDirtyDocuments(Set<String> docsWithDirtyStrings, Set<String> docWithDirtyBinaries) { 772 for (DBSDocumentState docState : transientStates.values()) { 773 StateDiff diff = docState.getStateChange(); 774 if (diff == null) { 775 continue; 776 } 777 // ignore fulltext-related properties to avoid looping 778 for (String key : diff.keyArray()) { 779 if (key.startsWith(KEY_FULLTEXT_SIMPLE) || key.startsWith(KEY_FULLTEXT_BINARY) 780 || key.equals(KEY_FULLTEXT_JOBID)) { 781 diff.remove(key); 782 } 783 } 784 Set<String> paths = new DirtyPathsFinder().findDirtyPaths(diff); 785 FulltextConfiguration fulltextConfiguration = repository.getFulltextConfiguration(); 786 boolean dirtyStrings = false; 787 boolean dirtyBinaries = false; 788 for (String path : paths) { 789 Set<String> indexesSimple = fulltextConfiguration.indexesByPropPathSimple.get(path); 790 if (indexesSimple != null && !indexesSimple.isEmpty()) { 791 dirtyStrings = true; 792 if (dirtyBinaries) { 793 break; 794 } 795 } 796 Set<String> indexesBinary= fulltextConfiguration.indexesByPropPathBinary.get(path); 797 if (indexesBinary != null && !indexesBinary.isEmpty()) { 798 dirtyBinaries = true; 799 if (dirtyStrings) { 800 break; 801 } 802 } 803 } 804 if (dirtyStrings) { 805 docsWithDirtyStrings.add(docState.getId()); 806 } 807 if (dirtyBinaries) { 808 docWithDirtyBinaries.add(docState.getId()); 809 } 810 } 811 } 812 813 /** 814 * Iterates on a state diff to find the paths corresponding to dirty values. 815 * 816 * @since 7.10-HF04, 8.1 817 */ 818 protected static class DirtyPathsFinder { 819 820 protected Set<String> paths; 821 822 public Set<String> findDirtyPaths(StateDiff value) { 823 paths = new HashSet<>(); 824 findDirtyPaths(value, null); 825 return paths; 826 } 827 828 protected void findDirtyPaths(Object value, String path) { 829 if (value instanceof Object[]) { 830 findDirtyPaths((Object[]) value, path); 831 } else if (value instanceof List) { 832 findDirtyPaths((List<?>) value, path); 833 } else if (value instanceof ListDiff) { 834 findDirtyPaths((ListDiff) value, path); 835 } else if (value instanceof State) { 836 findDirtyPaths((State) value, path); 837 } else { 838 paths.add(path); 839 } 840 } 841 842 protected void findDirtyPaths(Object[] value, String path) { 843 String newPath = path + "/*"; 844 for (Object v : value) { 845 findDirtyPaths(v, newPath); 846 } 847 } 848 849 protected void findDirtyPaths(List<?> value, String path) { 850 String newPath = path + "/*"; 851 for (Object v : value) { 852 findDirtyPaths(v, newPath); 853 } 854 } 855 856 protected void findDirtyPaths(ListDiff value, String path) { 857 String newPath = path + "/*"; 858 if (value.diff != null) { 859 findDirtyPaths(value.diff, newPath); 860 } 861 if (value.rpush != null) { 862 findDirtyPaths(value.rpush, newPath); 863 } 864 } 865 866 protected void findDirtyPaths(State value, String path) { 867 for (Entry<String, Serializable> es : value.entrySet()) { 868 String key = es.getKey(); 869 Serializable v = es.getValue(); 870 String newPath = path == null ? key : path + "/" + key; 871 findDirtyPaths(v, newPath); 872 } 873 } 874 } 875 876 protected void getFulltextSimpleWorks(List<Work> works, Set<String> docsWithDirtyStrings) { 877 // TODO XXX make configurable, see also FulltextExtractorWork 878 FulltextParser fulltextParser = new DefaultFulltextParser(); 879 FulltextConfiguration fulltextConfiguration = repository.getFulltextConfiguration(); 880 // update simpletext on documents with dirty strings 881 for (String id : docsWithDirtyStrings) { 882 if (id == null) { 883 // cannot happen, but has been observed :( 884 log.error("Got null doc id in fulltext update, cannot happen"); 885 continue; 886 } 887 DBSDocumentState docState = getStateForUpdate(id); 888 if (docState == null) { 889 // cannot happen 890 continue; 891 } 892 String documentType = docState.getPrimaryType(); 893 // Object[] mixinTypes = (Object[]) docState.get(KEY_MIXIN_TYPES); 894 895 if (!fulltextConfiguration.isFulltextIndexable(documentType)) { 896 continue; 897 } 898 docState.put(KEY_FULLTEXT_JOBID, docState.getId()); 899 FulltextFinder fulltextFinder = new FulltextFinder(fulltextParser, docState, session); 900 List<IndexAndText> indexesAndText = new LinkedList<IndexAndText>(); 901 for (String indexName : fulltextConfiguration.indexNames) { 902 // TODO paths from config 903 String text = fulltextFinder.findFulltext(indexName); 904 indexesAndText.add(new IndexAndText(indexName, text)); 905 } 906 if (!indexesAndText.isEmpty()) { 907 Work work = new FulltextUpdaterWork(repository.getName(), id, true, false, indexesAndText); 908 works.add(work); 909 } 910 } 911 } 912 913 protected void getFulltextBinariesWorks(List<Work> works, Set<String> docWithDirtyBinaries) { 914 if (docWithDirtyBinaries.isEmpty()) { 915 return; 916 } 917 918 FulltextConfiguration fulltextConfiguration = repository.getFulltextConfiguration(); 919 920 // mark indexing in progress, so that future copies (including versions) 921 // will be indexed as well 922 for (String id : docWithDirtyBinaries) { 923 DBSDocumentState docState = getStateForUpdate(id); 924 if (docState == null) { 925 // cannot happen 926 continue; 927 } 928 if (!fulltextConfiguration.isFulltextIndexable(docState.getPrimaryType())) { 929 continue; 930 } 931 docState.put(KEY_FULLTEXT_JOBID, docState.getId()); 932 } 933 934 // FulltextExtractorWork does fulltext extraction using converters 935 // and then schedules a FulltextUpdaterWork to write the results 936 // single-threaded 937 for (String id : docWithDirtyBinaries) { 938 // don't exclude proxies 939 Work work = new DBSFulltextExtractorWork(repository.getName(), id); 940 works.add(work); 941 } 942 } 943 944 protected static class FulltextFinder { 945 946 protected final FulltextParser fulltextParser; 947 948 protected final DBSDocumentState document; 949 950 protected final DBSSession session; 951 952 protected final String documentType; 953 954 protected final Object[] mixinTypes; 955 956 /** 957 * Prepares parsing for one document. 958 */ 959 public FulltextFinder(FulltextParser fulltextParser, DBSDocumentState document, DBSSession session) { 960 this.fulltextParser = fulltextParser; 961 this.document = document; 962 this.session = session; 963 if (document == null) { 964 documentType = null; 965 mixinTypes = null; 966 } else { // null in tests 967 documentType = document.getPrimaryType(); 968 mixinTypes = (Object[]) document.get(KEY_MIXIN_TYPES); 969 } 970 } 971 972 /** 973 * Parses the document for one index. 974 */ 975 public String findFulltext(String indexName) { 976 // TODO indexName 977 // TODO paths 978 List<String> strings = new ArrayList<String>(); 979 findFulltext(indexName, document.getState(), strings); 980 return StringUtils.join(strings, ' '); 981 } 982 983 protected void findFulltext(String indexName, State state, List<String> strings) { 984 for (Entry<String, Serializable> en : state.entrySet()) { 985 String key = en.getKey(); 986 if (key.startsWith(KEY_PREFIX)) { 987 switch (key) { 988 // allow indexing of this: 989 case DBSDocument.KEY_NAME: 990 break; 991 default: 992 continue; 993 } 994 } 995 Serializable value = en.getValue(); 996 if (value instanceof State) { 997 State s = (State) value; 998 findFulltext(indexName, s, strings); 999 } else if (value instanceof List) { 1000 @SuppressWarnings("unchecked") 1001 List<State> v = (List<State>) value; 1002 for (State s : v) { 1003 findFulltext(indexName, s, strings); 1004 } 1005 } else if (value instanceof Object[]) { 1006 Object[] ar = (Object[]) value; 1007 for (Object v : ar) { 1008 if (v instanceof String) { 1009 fulltextParser.parse((String) v, null, strings); 1010 } else { 1011 // arrays are homogeneous, no need to continue 1012 break; 1013 } 1014 } 1015 } else { 1016 if (value instanceof String) { 1017 fulltextParser.parse((String) value, null, strings); 1018 } 1019 } 1020 } 1021 } 1022 } 1023 1024 protected void scheduleWork(List<Work> works) { 1025 // do async fulltext indexing only if high-level sessions are available 1026 RepositoryManager repositoryManager = Framework.getLocalService(RepositoryManager.class); 1027 if (repositoryManager != null && !works.isEmpty()) { 1028 WorkManager workManager = Framework.getLocalService(WorkManager.class); 1029 for (Work work : works) { 1030 // schedule work post-commit 1031 // in non-tx mode, this may execute it nearly immediately 1032 workManager.schedule(work, Scheduling.IF_NOT_SCHEDULED, true); 1033 } 1034 } 1035 } 1036 1037}