001/* 002 * (C) Copyright 2014 Nuxeo SA (http://nuxeo.com/) and contributors. 003 * 004 * All rights reserved. This program and the accompanying materials 005 * are made available under the terms of the GNU Lesser General Public License 006 * (LGPL) version 2.1 which accompanies this distribution, and is available at 007 * http://www.gnu.org/licenses/lgpl-2.1.html 008 * 009 * This library is distributed in the hope that it will be useful, 010 * but WITHOUT ANY WARRANTY; without even the implied warranty of 011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 012 * Lesser General Public License for more details. 013 * 014 * Contributors: 015 * Florent Guillaume 016 */ 017package org.nuxeo.ecm.core.storage.dbs; 018 019import static java.lang.Boolean.TRUE; 020import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_ACE_BEGIN; 021import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_ACE_CREATOR; 022import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_ACE_END; 023import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_ACE_GRANT; 024import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_ACE_PERMISSION; 025import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_ACE_STATUS; 026import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_ACE_USER; 027import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_ACL; 028import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_ACL_NAME; 029import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_ACP; 030import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_ANCESTOR_IDS; 031import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_BASE_VERSION_ID; 032import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_FULLTEXT_BINARY; 033import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_FULLTEXT_JOBID; 034import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_FULLTEXT_SCORE; 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_CHECKED_IN; 038import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_IS_LATEST_MAJOR_VERSION; 039import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_IS_LATEST_VERSION; 040import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_IS_PROXY; 041import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_IS_VERSION; 042import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_LIFECYCLE_POLICY; 043import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_LIFECYCLE_STATE; 044import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_LOCK_CREATED; 045import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_LOCK_OWNER; 046import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_MAJOR_VERSION; 047import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_MINOR_VERSION; 048import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_MIXIN_TYPES; 049import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_NAME; 050import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_PARENT_ID; 051import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_PATH_INTERNAL; 052import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_POS; 053import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_PREFIX; 054import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_PRIMARY_TYPE; 055import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_PROXY_IDS; 056import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_PROXY_TARGET_ID; 057import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_PROXY_VERSION_SERIES_ID; 058import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_READ_ACL; 059import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_VERSION_CREATED; 060import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_VERSION_DESCRIPTION; 061import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_VERSION_LABEL; 062import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_VERSION_SERIES_ID; 063 064import java.io.Serializable; 065import java.text.DateFormat; 066import java.text.Normalizer; 067import java.text.ParseException; 068import java.util.ArrayList; 069import java.util.Calendar; 070import java.util.Collections; 071import java.util.Comparator; 072import java.util.GregorianCalendar; 073import java.util.HashMap; 074import java.util.HashSet; 075import java.util.Iterator; 076import java.util.LinkedList; 077import java.util.List; 078import java.util.Map; 079import java.util.Map.Entry; 080import java.util.NoSuchElementException; 081import java.util.Set; 082import java.util.regex.Matcher; 083import java.util.regex.Pattern; 084 085import org.apache.commons.lang.ObjectUtils; 086import org.apache.commons.lang.StringUtils; 087import org.nuxeo.ecm.core.api.CoreSession; 088import org.nuxeo.ecm.core.api.DocumentExistsException; 089import org.nuxeo.ecm.core.api.DocumentNotFoundException; 090import org.nuxeo.ecm.core.api.IterableQueryResult; 091import org.nuxeo.ecm.core.api.NuxeoException; 092import org.nuxeo.ecm.core.api.PartialList; 093import org.nuxeo.ecm.core.api.VersionModel; 094import org.nuxeo.ecm.core.api.security.ACE; 095import org.nuxeo.ecm.core.api.security.ACL; 096import org.nuxeo.ecm.core.api.security.ACP; 097import org.nuxeo.ecm.core.api.security.Access; 098import org.nuxeo.ecm.core.api.security.SecurityConstants; 099import org.nuxeo.ecm.core.api.security.impl.ACLImpl; 100import org.nuxeo.ecm.core.api.security.impl.ACPImpl; 101import org.nuxeo.ecm.core.blob.BlobManager; 102import org.nuxeo.ecm.core.model.Document; 103import org.nuxeo.ecm.core.model.LockManager; 104import org.nuxeo.ecm.core.model.Session; 105import org.nuxeo.ecm.core.query.QueryFilter; 106import org.nuxeo.ecm.core.query.QueryParseException; 107import org.nuxeo.ecm.core.query.sql.NXQL; 108import org.nuxeo.ecm.core.query.sql.SQLQueryParser; 109import org.nuxeo.ecm.core.query.sql.model.MultiExpression; 110import org.nuxeo.ecm.core.query.sql.model.OrderByClause; 111import org.nuxeo.ecm.core.query.sql.model.OrderByExpr; 112import org.nuxeo.ecm.core.query.sql.model.Reference; 113import org.nuxeo.ecm.core.query.sql.model.SQLQuery; 114import org.nuxeo.ecm.core.query.sql.model.SelectClause; 115import org.nuxeo.ecm.core.schema.DocumentType; 116import org.nuxeo.ecm.core.schema.FacetNames; 117import org.nuxeo.ecm.core.schema.SchemaManager; 118import org.nuxeo.ecm.core.storage.ExpressionEvaluator; 119import org.nuxeo.ecm.core.storage.QueryOptimizer; 120import org.nuxeo.ecm.core.storage.State; 121import org.nuxeo.ecm.core.storage.StateHelper; 122import org.nuxeo.ecm.core.storage.dbs.DBSExpressionEvaluator.OrderByComparator; 123import org.nuxeo.runtime.api.Framework; 124import org.nuxeo.runtime.transaction.TransactionHelper; 125 126/** 127 * Implementation of a {@link Session} for Document-Based Storage. 128 * 129 * @since 5.9.4 130 */ 131public class DBSSession implements Session { 132 133 protected final DBSRepository repository; 134 135 protected final DBSTransactionState transaction; 136 137 protected boolean closed; 138 139 public DBSSession(DBSRepository repository) { 140 this.repository = repository; 141 transaction = new DBSTransactionState(repository, this); 142 } 143 144 @Override 145 public String getRepositoryName() { 146 return repository.getName(); 147 } 148 149 @Override 150 public void close() { 151 closed = true; 152 } 153 154 @Override 155 public boolean isLive() { 156 return !closed; 157 } 158 159 @Override 160 public void save() { 161 transaction.save(); 162 if (!TransactionHelper.isTransactionActive()) { 163 transaction.commit(); 164 } 165 } 166 167 public void begin() { 168 transaction.begin(); 169 } 170 171 public void commit() { 172 transaction.commit(); 173 } 174 175 public void rollback() { 176 transaction.rollback(); 177 } 178 179 @Override 180 public boolean isStateSharedByAllThreadSessions() { 181 return false; 182 } 183 184 protected BlobManager getBlobManager() { 185 return repository.getBlobManager(); 186 } 187 188 protected String getRootId() { 189 return repository.getRootId(); 190 } 191 192 /* 193 * Normalize using NFC to avoid decomposed characters (like 'e' + COMBINING ACUTE ACCENT instead of LATIN SMALL 194 * LETTER E WITH ACUTE). NFKC (normalization using compatibility decomposition) is not used, because compatibility 195 * decomposition turns some characters (LATIN SMALL LIGATURE FFI, TRADE MARK SIGN, FULLWIDTH SOLIDUS) into a series 196 * of characters ('f'+'f'+'i', 'T'+'M', '/') that cannot be re-composed into the original, and therefore loses 197 * information. 198 */ 199 protected String normalize(String path) { 200 return Normalizer.normalize(path, Normalizer.Form.NFC); 201 } 202 203 @Override 204 public Document resolvePath(String path) { 205 // TODO move checks and normalize higher in call stack 206 if (path == null) { 207 throw new IllegalArgumentException("Null path"); 208 } 209 int len = path.length(); 210 if (len == 0) { 211 throw new IllegalArgumentException("Empty path"); 212 } 213 if (path.charAt(0) != '/') { 214 throw new IllegalArgumentException("Relative path: " + path); 215 } 216 if (len > 1 && path.charAt(len - 1) == '/') { 217 // remove final slash 218 path = path.substring(0, len - 1); 219 len--; 220 } 221 path = normalize(path); 222 223 if (len == 1) { 224 return getRootDocument(); 225 } 226 DBSDocumentState docState = null; 227 String parentId = getRootId(); 228 String[] names = path.split("/", -1); 229 for (int i = 1; i < names.length; i++) { 230 String name = names[i]; 231 if (name.length() == 0) { 232 throw new IllegalArgumentException("Path with empty component: " + path); 233 } 234 docState = transaction.getChildState(parentId, name); 235 if (docState == null) { 236 throw new DocumentNotFoundException(path); 237 } 238 parentId = docState.getId(); 239 } 240 return getDocument(docState); 241 } 242 243 protected String getDocumentIdByPath(String path) { 244 // TODO move checks and normalize higher in call stack 245 if (path == null) { 246 throw new IllegalArgumentException("Null path"); 247 } 248 int len = path.length(); 249 if (len == 0) { 250 throw new IllegalArgumentException("Empty path"); 251 } 252 if (path.charAt(0) != '/') { 253 throw new IllegalArgumentException("Relative path: " + path); 254 } 255 if (len > 1 && path.charAt(len - 1) == '/') { 256 // remove final slash 257 path = path.substring(0, len - 1); 258 len--; 259 } 260 path = normalize(path); 261 262 if (len == 1) { 263 return getRootId(); 264 } 265 DBSDocumentState docState = null; 266 String parentId = getRootId(); 267 String[] names = path.split("/", -1); 268 for (int i = 1; i < names.length; i++) { 269 String name = names[i]; 270 if (name.length() == 0) { 271 throw new IllegalArgumentException("Path with empty component: " + path); 272 } 273 // TODO XXX add getChildId method 274 docState = transaction.getChildState(parentId, name); 275 if (docState == null) { 276 return null; 277 } 278 parentId = docState.getId(); 279 } 280 return docState.getId(); 281 } 282 283 protected Document getChild(String parentId, String name) { 284 DBSDocumentState docState = transaction.getChildState(parentId, name); 285 return getDocument(docState); 286 } 287 288 protected List<Document> getChildren(String parentId) { 289 List<DBSDocumentState> docStates = transaction.getChildrenStates(parentId); 290 if (isOrderable(parentId)) { 291 // sort children in order 292 Collections.sort(docStates, POS_COMPARATOR); 293 } 294 List<Document> children = new ArrayList<Document>(docStates.size()); 295 for (DBSDocumentState docState : docStates) { 296 try { 297 children.add(getDocument(docState)); 298 } catch (DocumentNotFoundException e) { 299 // ignore error retrieving one of the children 300 // (Unknown document type) 301 continue; 302 } 303 } 304 return children; 305 } 306 307 protected List<String> getChildrenIds(String parentId) { 308 if (isOrderable(parentId)) { 309 // TODO get only id and pos, not full state 310 // TODO state not for update 311 List<DBSDocumentState> docStates = transaction.getChildrenStates(parentId); 312 Collections.sort(docStates, POS_COMPARATOR); 313 List<String> children = new ArrayList<String>(docStates.size()); 314 for (DBSDocumentState docState : docStates) { 315 children.add(docState.getId()); 316 } 317 return children; 318 } else { 319 return transaction.getChildrenIds(parentId); 320 } 321 } 322 323 protected boolean hasChildren(String parentId) { 324 return transaction.hasChildren(parentId); 325 326 } 327 328 @Override 329 public Document getDocumentByUUID(String id) { 330 Document doc = getDocument(id); 331 if (doc != null) { 332 return doc; 333 } 334 // exception required by API 335 throw new DocumentNotFoundException(id); 336 } 337 338 @Override 339 public Document getRootDocument() { 340 return getDocument(getRootId()); 341 } 342 343 @Override 344 public Document getNullDocument() { 345 return new DBSDocument(null, null, this, true); 346 } 347 348 protected Document getDocument(String id) { 349 DBSDocumentState docState = transaction.getStateForUpdate(id); 350 return getDocument(docState); 351 } 352 353 protected List<Document> getDocuments(List<String> ids) { 354 List<DBSDocumentState> docStates = transaction.getStatesForUpdate(ids); 355 List<Document> docs = new ArrayList<Document>(ids.size()); 356 for (DBSDocumentState docState : docStates) { 357 docs.add(getDocument(docState)); 358 } 359 return docs; 360 } 361 362 protected Document getDocument(DBSDocumentState docState) { 363 return getDocument(docState, true); 364 } 365 366 protected Document getDocument(DBSDocumentState docState, boolean readonly) { 367 if (docState == null) { 368 return null; 369 } 370 boolean isVersion = TRUE.equals(docState.get(KEY_IS_VERSION)); 371 372 String typeName = docState.getPrimaryType(); 373 SchemaManager schemaManager = Framework.getLocalService(SchemaManager.class); 374 DocumentType type = schemaManager.getDocumentType(typeName); 375 if (type == null) { 376 throw new DocumentNotFoundException("Unknown document type: " + typeName); 377 } 378 379 if (isVersion) { 380 return new DBSDocument(docState, type, this, readonly); 381 } else { 382 return new DBSDocument(docState, type, this, false); 383 } 384 } 385 386 protected boolean hasChild(String parentId, String name) { 387 return transaction.hasChild(parentId, normalize(name)); 388 } 389 390 public Document createChild(String id, String parentId, String name, Long pos, String typeName) { 391 DBSDocumentState docState = createChildState(id, parentId, name, pos, typeName); 392 return getDocument(docState); 393 } 394 395 protected DBSDocumentState createChildState(String id, String parentId, String name, Long pos, String typeName) { 396 if (pos == null && parentId != null) { 397 pos = getNextPos(parentId); 398 } 399 return transaction.createChild(id, parentId, name, pos, typeName); 400 } 401 402 protected boolean isOrderable(String id) { 403 State state = transaction.getStateForRead(id); 404 String typeName = (String) state.get(KEY_PRIMARY_TYPE); 405 SchemaManager schemaManager = Framework.getLocalService(SchemaManager.class); 406 return schemaManager.getDocumentType(typeName).getFacets().contains(FacetNames.ORDERABLE); 407 } 408 409 protected Long getNextPos(String parentId) { 410 if (!isOrderable(parentId)) { 411 return null; 412 } 413 long max = -1; 414 for (DBSDocumentState docState : transaction.getChildrenStates(parentId)) { 415 Long pos = (Long) docState.get(KEY_POS); 416 if (pos != null && pos.longValue() > max) { 417 max = pos.longValue(); 418 } 419 } 420 return Long.valueOf(max + 1); 421 } 422 423 protected void orderBefore(String parentId, String sourceId, String destId) { 424 if (!isOrderable(parentId)) { 425 // TODO throw exception? 426 return; 427 } 428 if (sourceId.equals(destId)) { 429 return; 430 } 431 // This is optimized by assuming the number of children is small enough 432 // to be manageable in-memory. 433 // fetch children 434 List<DBSDocumentState> docStates = transaction.getChildrenStates(parentId); 435 // sort children in order 436 Collections.sort(docStates, POS_COMPARATOR); 437 // renumber 438 int i = 0; 439 DBSDocumentState source = null; // source if seen 440 Long destPos = null; 441 for (DBSDocumentState docState : docStates) { 442 Serializable id = docState.getId(); 443 if (id.equals(destId)) { 444 destPos = Long.valueOf(i); 445 i++; 446 if (source != null) { 447 source.put(KEY_POS, destPos); 448 } 449 } 450 Long setPos; 451 if (id.equals(sourceId)) { 452 i--; 453 source = docState; 454 setPos = destPos; 455 } else { 456 setPos = Long.valueOf(i); 457 } 458 if (setPos != null) { 459 if (!setPos.equals(docState.get(KEY_POS))) { 460 docState.put(KEY_POS, setPos); 461 } 462 } 463 i++; 464 } 465 if (destId == null) { 466 Long setPos = Long.valueOf(i); 467 if (!setPos.equals(source.get(KEY_POS))) { 468 source.put(KEY_POS, setPos); 469 } 470 } 471 } 472 473 protected void checkOut(String id) { 474 DBSDocumentState docState = transaction.getStateForUpdate(id); 475 if (!TRUE.equals(docState.get(KEY_IS_CHECKED_IN))) { 476 throw new NuxeoException("Already checked out"); 477 } 478 docState.put(KEY_IS_CHECKED_IN, null); 479 } 480 481 protected Document checkIn(String id, String label, String checkinComment) { 482 transaction.save(); 483 DBSDocumentState docState = transaction.getStateForUpdate(id); 484 if (TRUE.equals(docState.get(KEY_IS_CHECKED_IN))) { 485 throw new NuxeoException("Already checked in"); 486 } 487 if (label == null) { 488 // use version major + minor as label 489 Long major = (Long) docState.get(KEY_MAJOR_VERSION); 490 Long minor = (Long) docState.get(KEY_MINOR_VERSION); 491 if (major == null || minor == null) { 492 label = ""; 493 } else { 494 label = major + "." + minor; 495 } 496 } 497 498 // copy into a version 499 DBSDocumentState verState = transaction.copy(id); 500 String verId = verState.getId(); 501 verState.put(KEY_PARENT_ID, null); 502 verState.put(KEY_ANCESTOR_IDS, null); 503 verState.put(KEY_IS_VERSION, TRUE); 504 verState.put(KEY_VERSION_SERIES_ID, id); 505 verState.put(KEY_VERSION_CREATED, new GregorianCalendar()); // now 506 verState.put(KEY_VERSION_LABEL, label); 507 verState.put(KEY_VERSION_DESCRIPTION, checkinComment); 508 verState.put(KEY_IS_LATEST_VERSION, TRUE); 509 verState.put(KEY_IS_CHECKED_IN, null); 510 verState.put(KEY_BASE_VERSION_ID, null); 511 boolean isMajor = Long.valueOf(0).equals(verState.get(KEY_MINOR_VERSION)); 512 verState.put(KEY_IS_LATEST_MAJOR_VERSION, isMajor ? TRUE : null); 513 514 // update the doc to mark it checked in 515 docState.put(KEY_IS_CHECKED_IN, TRUE); 516 docState.put(KEY_BASE_VERSION_ID, verId); 517 518 recomputeVersionSeries(id); 519 transaction.save(); 520 521 return getDocument(verId); 522 } 523 524 /** 525 * Recomputes isLatest / isLatestMajor on all versions. 526 */ 527 protected void recomputeVersionSeries(String versionSeriesId) { 528 List<DBSDocumentState> docStates = transaction.getKeyValuedStates(KEY_VERSION_SERIES_ID, versionSeriesId, 529 KEY_IS_VERSION, TRUE); 530 Collections.sort(docStates, VERSION_CREATED_COMPARATOR); 531 Collections.reverse(docStates); 532 boolean isLatest = true; 533 boolean isLatestMajor = true; 534 for (DBSDocumentState docState : docStates) { 535 // isLatestVersion 536 docState.put(KEY_IS_LATEST_VERSION, isLatest ? TRUE : null); 537 isLatest = false; 538 // isLatestMajorVersion 539 boolean isMajor = Long.valueOf(0).equals(docState.get(KEY_MINOR_VERSION)); 540 docState.put(KEY_IS_LATEST_MAJOR_VERSION, isMajor && isLatestMajor ? TRUE : null); 541 if (isMajor) { 542 isLatestMajor = false; 543 } 544 } 545 } 546 547 protected void restoreVersion(Document doc, Document version) { 548 String docId = doc.getUUID(); 549 String versionId = version.getUUID(); 550 551 DBSDocumentState docState = transaction.getStateForUpdate(docId); 552 State versionState = transaction.getStateForRead(versionId); 553 554 for (Entry<String, Serializable> en : versionState.entrySet()) { 555 String key = en.getKey(); 556 if (!keepWhenRestore(key)) { 557 docState.put(key, StateHelper.deepCopy(en.getValue())); 558 } 559 } 560 docState.put(KEY_IS_VERSION, null); 561 docState.put(KEY_IS_CHECKED_IN, TRUE); 562 docState.put(KEY_BASE_VERSION_ID, versionId); 563 } 564 565 // keys we don't copy from version when restoring 566 protected boolean keepWhenRestore(String key) { 567 switch (key) { 568 // these are placeful stuff 569 case KEY_ID: 570 case KEY_PARENT_ID: 571 case KEY_ANCESTOR_IDS: 572 case KEY_NAME: 573 case KEY_POS: 574 case KEY_PRIMARY_TYPE: 575 case KEY_ACP: 576 case KEY_READ_ACL: 577 // these are version-specific 578 case KEY_VERSION_CREATED: 579 case KEY_VERSION_DESCRIPTION: 580 case KEY_VERSION_LABEL: 581 case KEY_VERSION_SERIES_ID: 582 case KEY_IS_LATEST_VERSION: 583 case KEY_IS_LATEST_MAJOR_VERSION: 584 // these will be updated after restore 585 case KEY_IS_VERSION: 586 case KEY_IS_CHECKED_IN: 587 case KEY_BASE_VERSION_ID: 588 return true; 589 } 590 return false; 591 } 592 593 @Override 594 public Document copy(Document source, Document parent, String name) { 595 transaction.save(); 596 if (name == null) { 597 name = source.getName(); 598 } 599 name = findFreeName(parent, name); 600 String sourceId = source.getUUID(); 601 String parentId = parent.getUUID(); 602 State sourceState = transaction.getStateForRead(sourceId); 603 State parentState = transaction.getStateForRead(parentId); 604 String oldParentId = (String) sourceState.get(KEY_PARENT_ID); 605 Object[] parentAncestorIds = (Object[]) parentState.get(KEY_ANCESTOR_IDS); 606 LinkedList<String> ancestorIds = new LinkedList<String>(); 607 if (parentAncestorIds != null) { 608 for (Object id : parentAncestorIds) { 609 ancestorIds.add((String) id); 610 } 611 } 612 ancestorIds.add(parentId); 613 if (oldParentId != null && !oldParentId.equals(parentId)) { 614 if (ancestorIds.contains(sourceId)) { 615 throw new DocumentExistsException("Cannot copy a node under itself: " + parentId + " is under " + sourceId); 616 617 } 618 // checkNotUnder(parentId, sourceId, "copy"); 619 } 620 // do the copy 621 Long pos = getNextPos(parentId); 622 String copyId = copyRecurse(sourceId, parentId, ancestorIds, name); 623 DBSDocumentState copyState = transaction.getStateForUpdate(copyId); 624 // version copy fixup 625 if (source.isVersion()) { 626 copyState.put(KEY_IS_VERSION, null); 627 } 628 // pos fixup 629 copyState.put(KEY_POS, pos); 630 // update read acls 631 transaction.updateReadAcls(copyId); 632 633 return getDocument(copyState); 634 } 635 636 protected String copyRecurse(String sourceId, String parentId, LinkedList<String> ancestorIds, String name) { 637 String copyId = copy(sourceId, parentId, ancestorIds, name); 638 ancestorIds.addLast(copyId); 639 for (String childId : getChildrenIds(sourceId)) { 640 copyRecurse(childId, copyId, ancestorIds, null); 641 } 642 ancestorIds.removeLast(); 643 return copyId; 644 } 645 646 /** 647 * Copy source under parent, and set its ancestors. 648 */ 649 protected String copy(String sourceId, String parentId, List<String> ancestorIds, String name) { 650 DBSDocumentState copy = transaction.copy(sourceId); 651 copy.put(KEY_PARENT_ID, parentId); 652 copy.put(KEY_ANCESTOR_IDS, ancestorIds.toArray(new Object[ancestorIds.size()])); 653 if (name != null) { 654 copy.put(KEY_NAME, name); 655 } 656 copy.put(KEY_BASE_VERSION_ID, null); 657 copy.put(KEY_IS_CHECKED_IN, null); 658 return copy.getId(); 659 } 660 661 protected static final Pattern dotDigitsPattern = Pattern.compile("(.*)\\.[0-9]+$"); 662 663 protected String findFreeName(Document parent, String name) { 664 if (hasChild(parent.getUUID(), name)) { 665 Matcher m = dotDigitsPattern.matcher(name); 666 if (m.matches()) { 667 // remove trailing dot and digits 668 name = m.group(1); 669 } 670 // add dot + unique digits 671 name += "." + System.currentTimeMillis(); 672 } 673 return name; 674 } 675 676 /** Checks that we don't move/copy under ourselves. */ 677 protected void checkNotUnder(String parentId, String id, String op) { 678 // TODO use ancestors 679 String pid = parentId; 680 do { 681 if (pid.equals(id)) { 682 throw new DocumentExistsException("Cannot " + op + " a node under itself: " + parentId + " is under " + id); 683 } 684 State state = transaction.getStateForRead(pid); 685 if (state == null) { 686 // cannot happen 687 throw new NuxeoException("No parent: " + pid); 688 } 689 pid = (String) state.get(KEY_PARENT_ID); 690 } while (pid != null); 691 } 692 693 @Override 694 public Document move(Document source, Document parent, String name) { 695 String oldName = (String) source.getName(); 696 if (name == null) { 697 name = oldName; 698 } 699 String sourceId = source.getUUID(); 700 String parentId = parent.getUUID(); 701 DBSDocumentState sourceState = transaction.getStateForUpdate(sourceId); 702 String oldParentId = (String) sourceState.get(KEY_PARENT_ID); 703 704 // simple case of a rename 705 if (ObjectUtils.equals(oldParentId, parentId)) { 706 if (!oldName.equals(name)) { 707 if (hasChild(parentId, name)) { 708 throw new DocumentExistsException("Destination name already exists: " + name); 709 } 710 // do the move 711 sourceState.put(KEY_NAME, name); 712 // no ancestors to change 713 } 714 return source; 715 } else { 716 // if not just a simple rename, flush 717 transaction.save(); 718 if (hasChild(parentId, name)) { 719 throw new DocumentExistsException("Destination name already exists: " + name); 720 } 721 } 722 723 // prepare new ancestor ids 724 State parentState = transaction.getStateForRead(parentId); 725 Object[] parentAncestorIds = (Object[]) parentState.get(KEY_ANCESTOR_IDS); 726 List<String> ancestorIdsList = new ArrayList<String>(); 727 if (parentAncestorIds != null) { 728 for (Object id : parentAncestorIds) { 729 ancestorIdsList.add((String) id); 730 } 731 } 732 ancestorIdsList.add(parentId); 733 Object[] ancestorIds = ancestorIdsList.toArray(new Object[ancestorIdsList.size()]); 734 735 if (ancestorIdsList.contains(sourceId)) { 736 throw new DocumentExistsException("Cannot move a node under itself: " + parentId + " is under " + sourceId); 737 } 738 739 // do the move 740 sourceState.put(KEY_NAME, name); 741 sourceState.put(KEY_PARENT_ID, parentId); 742 743 // update ancestors on all sub-children 744 Object[] oldAncestorIds = (Object[]) sourceState.get(KEY_ANCESTOR_IDS); 745 int ndel = oldAncestorIds == null ? 0 : oldAncestorIds.length; 746 transaction.updateAncestors(sourceId, ndel, ancestorIds); 747 748 // update read acls 749 transaction.updateReadAcls(sourceId); 750 751 return source; 752 } 753 754 /** 755 * Removes a document. 756 * <p> 757 * We also have to update everything impacted by "relations": 758 * <ul> 759 * <li>parent-child relations: delete all subchildren recursively, 760 * <li>proxy-target relations: if a proxy is removed, update the target's PROXY_IDS; and if a target is removed, 761 * raise an error if a proxy still exists for that target. 762 * </ul> 763 */ 764 protected void remove(String id) { 765 transaction.save(); 766 767 State state = transaction.getStateForRead(id); 768 String versionSeriesId; 769 if (TRUE.equals(state.get(KEY_IS_VERSION))) { 770 versionSeriesId = (String) state.get(KEY_VERSION_SERIES_ID); 771 } else { 772 versionSeriesId = null; 773 } 774 // find all sub-docs and whether they're proxies 775 Map<String, String> proxyTargets = new HashMap<>(); 776 Map<String, Object[]> targetProxies = new HashMap<>(); 777 Set<String> removedIds = transaction.getSubTree(id, proxyTargets, targetProxies); 778 779 // add this node 780 removedIds.add(id); 781 if (TRUE.equals(state.get(KEY_IS_PROXY))) { 782 String targetId = (String) state.get(KEY_PROXY_TARGET_ID); 783 proxyTargets.put(id, targetId); 784 } 785 Object[] proxyIds = (Object[]) state.get(KEY_PROXY_IDS); 786 if (proxyIds != null) { 787 targetProxies.put(id, proxyIds); 788 } 789 790 // if a proxy target is removed, check that all proxies to it 791 // are removed 792 for (Entry<String, Object[]> en : targetProxies.entrySet()) { 793 String targetId = en.getKey(); 794 if (!removedIds.contains(targetId)) { 795 continue; 796 } 797 for (Object proxyId : en.getValue()) { 798 if (!removedIds.contains(proxyId)) { 799 throw new DocumentExistsException("Cannot remove " + id + ", subdocument " + targetId 800 + " is the target of proxy " + proxyId); 801 } 802 } 803 } 804 805 // remove all docs 806 transaction.removeStates(removedIds); 807 808 // fix proxies back-pointers on proxy targets 809 Set<String> targetIds = new HashSet<>(proxyTargets.values()); 810 for (String targetId : targetIds) { 811 if (removedIds.contains(targetId)) { 812 // the target was also removed, skip 813 continue; 814 } 815 DBSDocumentState target = transaction.getStateForUpdate(targetId); 816 removeBackProxyIds(target, removedIds); 817 } 818 819 // recompute version series if needed 820 // only done for root of deletion as versions are not fileable 821 if (versionSeriesId != null) { 822 recomputeVersionSeries(versionSeriesId); 823 } 824 } 825 826 @Override 827 public Document createProxy(Document doc, Document folder) { 828 if (doc == null) { 829 throw new NullPointerException(); 830 } 831 String id = doc.getUUID(); 832 String targetId; 833 String versionSeriesId; 834 if (doc.isVersion()) { 835 targetId = id; 836 versionSeriesId = doc.getVersionSeriesId(); 837 } else if (doc.isProxy()) { 838 // copy the proxy 839 State state = transaction.getStateForRead(id); 840 targetId = (String) state.get(KEY_PROXY_TARGET_ID); 841 versionSeriesId = (String) state.get(KEY_PROXY_VERSION_SERIES_ID); 842 } else { 843 // working copy (live document) 844 targetId = id; 845 versionSeriesId = targetId; 846 } 847 848 String parentId = folder.getUUID(); 849 String name = findFreeName(folder, doc.getName()); 850 Long pos = parentId == null ? null : getNextPos(parentId); 851 852 DBSDocumentState docState = addProxyState(null, parentId, name, pos, targetId, versionSeriesId); 853 return getDocument(docState); 854 } 855 856 protected DBSDocumentState addProxyState(String id, String parentId, String name, Long pos, String targetId, 857 String versionSeriesId) { 858 DBSDocumentState target = transaction.getStateForUpdate(targetId); 859 String typeName = (String) target.get(KEY_PRIMARY_TYPE); 860 861 DBSDocumentState proxy = transaction.createChild(id, parentId, name, pos, typeName); 862 String proxyId = proxy.getId(); 863 proxy.put(KEY_IS_PROXY, TRUE); 864 proxy.put(KEY_PROXY_TARGET_ID, targetId); 865 proxy.put(KEY_PROXY_VERSION_SERIES_ID, versionSeriesId); 866 867 // copy target state to proxy 868 transaction.updateProxy(target, proxyId); 869 870 // add back-reference to proxy on target 871 addBackProxyId(target, proxyId); 872 873 return transaction.getStateForUpdate(proxyId); 874 } 875 876 protected void addBackProxyId(DBSDocumentState docState, String id) { 877 Object[] proxyIds = (Object[]) docState.get(KEY_PROXY_IDS); 878 Object[] newProxyIds; 879 if (proxyIds == null) { 880 newProxyIds = new Object[] { id }; 881 } else { 882 newProxyIds = new Object[proxyIds.length + 1]; 883 System.arraycopy(proxyIds, 0, newProxyIds, 0, proxyIds.length); 884 newProxyIds[proxyIds.length] = id; 885 } 886 docState.put(KEY_PROXY_IDS, newProxyIds); 887 } 888 889 protected void removeBackProxyId(DBSDocumentState docState, String id) { 890 removeBackProxyIds(docState, Collections.singleton(id)); 891 } 892 893 protected void removeBackProxyIds(DBSDocumentState docState, Set<String> ids) { 894 Object[] proxyIds = (Object[]) docState.get(KEY_PROXY_IDS); 895 if (proxyIds == null) { 896 return; 897 } 898 List<Object> keepIds = new ArrayList<>(proxyIds.length); 899 for (Object pid : proxyIds) { 900 if (!ids.contains(pid)) { 901 keepIds.add(pid); 902 } 903 } 904 Object[] newProxyIds = keepIds.isEmpty() ? null : keepIds.toArray(new Object[keepIds.size()]); 905 docState.put(KEY_PROXY_IDS, newProxyIds); 906 } 907 908 @Override 909 public List<Document> getProxies(Document doc, Document folder) { 910 List<DBSDocumentState> docStates; 911 String docId = doc.getUUID(); 912 if (doc.isVersion()) { 913 docStates = transaction.getKeyValuedStates(KEY_PROXY_TARGET_ID, docId); 914 } else { 915 String versionSeriesId; 916 if (doc.isProxy()) { 917 State state = transaction.getStateForRead(docId); 918 versionSeriesId = (String) state.get(KEY_PROXY_VERSION_SERIES_ID); 919 } else { 920 versionSeriesId = docId; 921 } 922 docStates = transaction.getKeyValuedStates(KEY_PROXY_VERSION_SERIES_ID, versionSeriesId); 923 } 924 925 String parentId = folder == null ? null : folder.getUUID(); 926 List<Document> documents = new ArrayList<Document>(docStates.size()); 927 for (DBSDocumentState docState : docStates) { 928 // filter by parent 929 if (parentId != null && !parentId.equals(docState.getParentId())) { 930 continue; 931 } 932 documents.add(getDocument(docState)); 933 } 934 return documents; 935 } 936 937 @Override 938 public void setProxyTarget(Document proxy, Document target) { 939 String proxyId = proxy.getUUID(); 940 String targetId = target.getUUID(); 941 DBSDocumentState proxyState = transaction.getStateForUpdate(proxyId); 942 String oldTargetId = (String) proxyState.get(KEY_PROXY_TARGET_ID); 943 944 // update old target's back-pointers: remove proxy id 945 DBSDocumentState oldTargetState = transaction.getStateForUpdate(oldTargetId); 946 removeBackProxyId(oldTargetState, proxyId); 947 // update new target's back-pointers: add proxy id 948 DBSDocumentState targetState = transaction.getStateForUpdate(targetId); 949 addBackProxyId(targetState, proxyId); 950 // set new target 951 proxyState.put(KEY_PROXY_TARGET_ID, targetId); 952 } 953 954 @Override 955 public Document importDocument(String id, Document parent, String name, String typeName, 956 Map<String, Serializable> properties) { 957 String parentId = parent == null ? null : parent.getUUID(); 958 boolean isProxy = typeName.equals(CoreSession.IMPORT_PROXY_TYPE); 959 Map<String, Serializable> props = new HashMap<String, Serializable>(); 960 Long pos = null; // TODO pos 961 DBSDocumentState docState; 962 if (isProxy) { 963 // check that target exists and find its typeName 964 String targetId = (String) properties.get(CoreSession.IMPORT_PROXY_TARGET_ID); 965 if (targetId == null) { 966 throw new NuxeoException("Cannot import proxy " + id + " with null target"); 967 } 968 State targetState = transaction.getStateForRead(targetId); 969 if (targetState == null) { 970 throw new DocumentNotFoundException("Cannot import proxy " + id + " with missing target " + targetId); 971 } 972 String versionSeriesId = (String) properties.get(CoreSession.IMPORT_PROXY_VERSIONABLE_ID); 973 docState = addProxyState(id, parentId, name, pos, targetId, versionSeriesId); 974 } else { 975 // version & live document 976 props.put(KEY_LIFECYCLE_POLICY, properties.get(CoreSession.IMPORT_LIFECYCLE_POLICY)); 977 props.put(KEY_LIFECYCLE_STATE, properties.get(CoreSession.IMPORT_LIFECYCLE_STATE)); 978 // compat with old lock import 979 @SuppressWarnings("deprecation") 980 String key = (String) properties.get(CoreSession.IMPORT_LOCK); 981 if (key != null) { 982 String[] values = key.split(":"); 983 if (values.length == 2) { 984 String owner = values[0]; 985 Calendar created = new GregorianCalendar(); 986 try { 987 created.setTimeInMillis(DateFormat.getDateInstance(DateFormat.MEDIUM).parse(values[1]).getTime()); 988 } catch (ParseException e) { 989 // use current date 990 } 991 props.put(KEY_LOCK_OWNER, owner); 992 props.put(KEY_LOCK_CREATED, created); 993 } 994 } 995 996 Serializable importLockOwnerProp = properties.get(CoreSession.IMPORT_LOCK_OWNER); 997 if (importLockOwnerProp != null) { 998 props.put(KEY_LOCK_OWNER, importLockOwnerProp); 999 } 1000 Serializable importLockCreatedProp = properties.get(CoreSession.IMPORT_LOCK_CREATED); 1001 if (importLockCreatedProp != null) { 1002 props.put(KEY_LOCK_CREATED, importLockCreatedProp); 1003 } 1004 1005 props.put(KEY_MAJOR_VERSION, properties.get(CoreSession.IMPORT_VERSION_MAJOR)); 1006 props.put(KEY_MINOR_VERSION, properties.get(CoreSession.IMPORT_VERSION_MINOR)); 1007 Boolean isVersion = trueOrNull(properties.get(CoreSession.IMPORT_IS_VERSION)); 1008 props.put(KEY_IS_VERSION, isVersion); 1009 if (TRUE.equals(isVersion)) { 1010 // version 1011 props.put(KEY_VERSION_SERIES_ID, properties.get(CoreSession.IMPORT_VERSION_VERSIONABLE_ID)); 1012 props.put(KEY_VERSION_CREATED, properties.get(CoreSession.IMPORT_VERSION_CREATED)); 1013 props.put(KEY_VERSION_LABEL, properties.get(CoreSession.IMPORT_VERSION_LABEL)); 1014 props.put(KEY_VERSION_DESCRIPTION, properties.get(CoreSession.IMPORT_VERSION_DESCRIPTION)); 1015 // TODO maybe these should be recomputed at end of import: 1016 props.put(KEY_IS_LATEST_VERSION, trueOrNull(properties.get(CoreSession.IMPORT_VERSION_IS_LATEST))); 1017 props.put(KEY_IS_LATEST_MAJOR_VERSION, 1018 trueOrNull(properties.get(CoreSession.IMPORT_VERSION_IS_LATEST_MAJOR))); 1019 } else { 1020 // live document 1021 props.put(KEY_BASE_VERSION_ID, properties.get(CoreSession.IMPORT_BASE_VERSION_ID)); 1022 props.put(KEY_IS_CHECKED_IN, trueOrNull(properties.get(CoreSession.IMPORT_CHECKED_IN))); 1023 } 1024 docState = createChildState(id, parentId, name, pos, typeName); 1025 } 1026 for (Entry<String, Serializable> entry : props.entrySet()) { 1027 docState.put(entry.getKey(), entry.getValue()); 1028 } 1029 return getDocument(docState, false); // not readonly 1030 } 1031 1032 protected static Boolean trueOrNull(Object value) { 1033 return TRUE.equals(value) ? TRUE : null; 1034 } 1035 1036 @Override 1037 public Document getVersion(String versionSeriesId, VersionModel versionModel) { 1038 DBSDocumentState docState = getVersionByLabel(versionSeriesId, versionModel.getLabel()); 1039 if (docState == null) { 1040 return null; 1041 } 1042 versionModel.setDescription((String) docState.get(KEY_VERSION_DESCRIPTION)); 1043 versionModel.setCreated((Calendar) docState.get(KEY_VERSION_CREATED)); 1044 return getDocument(docState); 1045 } 1046 1047 protected DBSDocumentState getVersionByLabel(String versionSeriesId, String label) { 1048 List<DBSDocumentState> docStates = transaction.getKeyValuedStates(KEY_VERSION_SERIES_ID, versionSeriesId, 1049 KEY_IS_VERSION, TRUE); 1050 for (DBSDocumentState docState : docStates) { 1051 if (label.equals(docState.get(KEY_VERSION_LABEL))) { 1052 return docState; 1053 } 1054 } 1055 return null; 1056 } 1057 1058 protected List<String> getVersionsIds(String versionSeriesId) { 1059 // order by creation date 1060 List<DBSDocumentState> docStates = transaction.getKeyValuedStates(KEY_VERSION_SERIES_ID, versionSeriesId, 1061 KEY_IS_VERSION, TRUE); 1062 Collections.sort(docStates, VERSION_CREATED_COMPARATOR); 1063 List<String> ids = new ArrayList<String>(docStates.size()); 1064 for (DBSDocumentState docState : docStates) { 1065 ids.add(docState.getId()); 1066 } 1067 return ids; 1068 } 1069 1070 protected Document getLastVersion(String versionSeriesId) { 1071 List<DBSDocumentState> docStates = transaction.getKeyValuedStates(KEY_VERSION_SERIES_ID, versionSeriesId, 1072 KEY_IS_VERSION, TRUE); 1073 // find latest one 1074 Calendar latest = null; 1075 DBSDocumentState latestState = null; 1076 for (DBSDocumentState docState : docStates) { 1077 Calendar created = (Calendar) docState.get(KEY_VERSION_CREATED); 1078 if (latest == null || created.compareTo(latest) > 0) { 1079 latest = created; 1080 latestState = docState; 1081 } 1082 } 1083 return latestState == null ? null : getDocument(latestState); 1084 } 1085 1086 private static final Comparator<DBSDocumentState> VERSION_CREATED_COMPARATOR = new Comparator<DBSDocumentState>() { 1087 @Override 1088 public int compare(DBSDocumentState s1, DBSDocumentState s2) { 1089 Calendar c1 = (Calendar) s1.get(KEY_VERSION_CREATED); 1090 Calendar c2 = (Calendar) s2.get(KEY_VERSION_CREATED); 1091 if (c1 == null && c2 == null) { 1092 // coherent sort 1093 return s1.hashCode() - s2.hashCode(); 1094 } 1095 if (c1 == null) { 1096 return 1; 1097 } 1098 if (c2 == null) { 1099 return -1; 1100 } 1101 return c1.compareTo(c2); 1102 } 1103 }; 1104 1105 private static final Comparator<DBSDocumentState> POS_COMPARATOR = new Comparator<DBSDocumentState>() { 1106 @Override 1107 public int compare(DBSDocumentState s1, DBSDocumentState s2) { 1108 Long p1 = (Long) s1.get(KEY_POS); 1109 Long p2 = (Long) s2.get(KEY_POS); 1110 if (p1 == null && p2 == null) { 1111 // coherent sort 1112 return s1.hashCode() - s2.hashCode(); 1113 } 1114 if (p1 == null) { 1115 return 1; 1116 } 1117 if (p2 == null) { 1118 return -1; 1119 } 1120 return p1.compareTo(p2); 1121 } 1122 }; 1123 1124 @Override 1125 public boolean isNegativeAclAllowed() { 1126 return false; 1127 } 1128 1129 // TODO move logic higher 1130 @Override 1131 public ACP getMergedACP(Document doc) { 1132 Document base = doc.isVersion() ? doc.getSourceDocument() : doc; 1133 if (base == null) { 1134 return null; 1135 } 1136 ACP acp = getACP(base); 1137 if (doc.getParent() == null) { 1138 return acp; 1139 } 1140 // get inherited ACLs only if no blocking inheritance ACE exists 1141 // in the top level ACP. 1142 ACL acl = null; 1143 if (acp == null || acp.getAccess(SecurityConstants.EVERYONE, SecurityConstants.EVERYTHING) != Access.DENY) { 1144 acl = getInheritedACLs(doc); 1145 } 1146 if (acp == null) { 1147 if (acl == null) { 1148 return null; 1149 } 1150 acp = new ACPImpl(); 1151 } 1152 if (acl != null) { 1153 acp.addACL(acl); 1154 } 1155 return acp; 1156 } 1157 1158 protected ACL getInheritedACLs(Document doc) { 1159 doc = doc.getParent(); 1160 ACL merged = null; 1161 while (doc != null) { 1162 ACP acp = getACP(doc); 1163 if (acp != null) { 1164 ACL acl = acp.getMergedACLs(ACL.INHERITED_ACL); 1165 if (merged == null) { 1166 merged = acl; 1167 } else { 1168 merged.addAll(acl); 1169 } 1170 if (acp.getAccess(SecurityConstants.EVERYONE, SecurityConstants.EVERYTHING) == Access.DENY) { 1171 break; 1172 } 1173 } 1174 doc = doc.getParent(); 1175 } 1176 return merged; 1177 } 1178 1179 protected ACP getACP(Document doc) { 1180 State state = transaction.getStateForRead(doc.getUUID()); 1181 return memToAcp(state.get(KEY_ACP)); 1182 } 1183 1184 @Override 1185 public void setACP(Document doc, ACP acp, boolean overwrite) { 1186 checkNegativeAcl(acp); 1187 if (!overwrite) { 1188 if (acp == null) { 1189 return; 1190 } 1191 // merge with existing 1192 acp = updateACP(getACP(doc), acp); 1193 } 1194 String id = doc.getUUID(); 1195 DBSDocumentState docState = transaction.getStateForUpdate(id); 1196 docState.put(KEY_ACP, acpToMem(acp)); 1197 transaction.save(); // read acls update needs full tree 1198 transaction.updateReadAcls(id); 1199 } 1200 1201 protected void checkNegativeAcl(ACP acp) { 1202 if (acp == null) { 1203 return; 1204 } 1205 for (ACL acl : acp.getACLs()) { 1206 if (acl.getName().equals(ACL.INHERITED_ACL)) { 1207 continue; 1208 } 1209 for (ACE ace : acl.getACEs()) { 1210 if (ace.isGranted()) { 1211 continue; 1212 } 1213 String permission = ace.getPermission(); 1214 if (permission.equals(SecurityConstants.EVERYTHING) 1215 && ace.getUsername().equals(SecurityConstants.EVERYONE)) { 1216 continue; 1217 } 1218 // allow Write, as we're sure it doesn't include Read/Browse 1219 if (permission.equals(SecurityConstants.WRITE)) { 1220 continue; 1221 } 1222 throw new IllegalArgumentException("Negative ACL not allowed: " + ace); 1223 } 1224 } 1225 } 1226 1227 /** 1228 * Returns the merge of two ACPs. 1229 */ 1230 // TODO move to ACPImpl 1231 protected static ACP updateACP(ACP curAcp, ACP addAcp) { 1232 if (curAcp == null) { 1233 return addAcp; 1234 } 1235 ACP newAcp = curAcp.clone(); 1236 Map<String, ACL> acls = new HashMap<String, ACL>(); 1237 for (ACL acl : newAcp.getACLs()) { 1238 String name = acl.getName(); 1239 if (ACL.INHERITED_ACL.equals(name)) { 1240 throw new IllegalStateException(curAcp.toString()); 1241 } 1242 acls.put(name, acl); 1243 } 1244 for (ACL acl : addAcp.getACLs()) { 1245 String name = acl.getName(); 1246 if (ACL.INHERITED_ACL.equals(name)) { 1247 continue; 1248 } 1249 ACL curAcl = acls.get(name); 1250 if (curAcl != null) { 1251 // TODO avoid duplicates 1252 curAcl.addAll(acl); 1253 } else { 1254 newAcp.addACL(acl); 1255 } 1256 } 1257 return newAcp; 1258 } 1259 1260 protected static Serializable acpToMem(ACP acp) { 1261 if (acp == null) { 1262 return null; 1263 } 1264 ACL[] acls = acp.getACLs(); 1265 if (acls.length == 0) { 1266 return null; 1267 } 1268 List<Serializable> aclList = new ArrayList<Serializable>(acls.length); 1269 for (ACL acl : acls) { 1270 String name = acl.getName(); 1271 if (name.equals(ACL.INHERITED_ACL)) { 1272 continue; 1273 } 1274 ACE[] aces = acl.getACEs(); 1275 List<Serializable> aceList = new ArrayList<Serializable>(aces.length); 1276 for (ACE ace : aces) { 1277 State aceMap = new State(6); 1278 aceMap.put(KEY_ACE_USER, ace.getUsername()); 1279 aceMap.put(KEY_ACE_PERMISSION, ace.getPermission()); 1280 aceMap.put(KEY_ACE_GRANT, Boolean.valueOf(ace.isGranted())); 1281 String creator = ace.getCreator(); 1282 if (creator != null) { 1283 aceMap.put(KEY_ACE_CREATOR, creator); 1284 } 1285 Calendar begin = ace.getBegin(); 1286 if (begin != null) { 1287 aceMap.put(KEY_ACE_BEGIN, begin); 1288 } 1289 Calendar end = ace.getEnd(); 1290 if (end != null) { 1291 aceMap.put(KEY_ACE_END, end); 1292 } 1293 Long status = ace.getLongStatus(); 1294 if (status != null) { 1295 aceMap.put(KEY_ACE_STATUS, status); 1296 } 1297 aceList.add(aceMap); 1298 } 1299 if (aceList.isEmpty()) { 1300 continue; 1301 } 1302 State aclMap = new State(2); 1303 aclMap.put(KEY_ACL_NAME, name); 1304 aclMap.put(KEY_ACL, (Serializable) aceList); 1305 aclList.add(aclMap); 1306 } 1307 return (Serializable) aclList; 1308 } 1309 1310 protected static ACP memToAcp(Serializable acpSer) { 1311 if (acpSer == null) { 1312 return null; 1313 } 1314 @SuppressWarnings("unchecked") 1315 List<Serializable> aclList = (List<Serializable>) acpSer; 1316 ACP acp = new ACPImpl(); 1317 for (Serializable aclSer : aclList) { 1318 State aclMap = (State) aclSer; 1319 String name = (String) aclMap.get(KEY_ACL_NAME); 1320 @SuppressWarnings("unchecked") 1321 List<Serializable> aceList = (List<Serializable>) aclMap.get(KEY_ACL); 1322 if (aceList == null) { 1323 continue; 1324 } 1325 ACL acl = new ACLImpl(name); 1326 for (Serializable aceSer : aceList) { 1327 State aceMap = (State) aceSer; 1328 String username = (String) aceMap.get(KEY_ACE_USER); 1329 String permission = (String) aceMap.get(KEY_ACE_PERMISSION); 1330 Boolean granted = (Boolean) aceMap.get(KEY_ACE_GRANT); 1331 String creator = (String) aceMap.get(KEY_ACE_CREATOR); 1332 Calendar begin = (Calendar) aceMap.get(KEY_ACE_BEGIN); 1333 Calendar end = (Calendar) aceMap.get(KEY_ACE_END); 1334 // status not read, ACE always computes it on read 1335 ACE ace = ACE.builder(username, permission).isGranted(granted.booleanValue()).creator(creator).begin( 1336 begin).end(end).build(); 1337 acl.add(ace); 1338 } 1339 acp.addACL(acl); 1340 } 1341 return acp; 1342 } 1343 1344 @Override 1345 public Map<String, String> getBinaryFulltext(String id) { 1346 State state = transaction.getStateForRead(id); 1347 String fulltext = (String) state.get(KEY_FULLTEXT_BINARY); 1348 return Collections.singletonMap("binarytext", fulltext); 1349 } 1350 1351 @Override 1352 public PartialList<Document> query(String query, String queryType, QueryFilter queryFilter, long countUpTo) { 1353 // query 1354 PartialList<String> pl = doQuery(query, queryType, queryFilter, (int) countUpTo); 1355 1356 // get Documents in bulk 1357 List<Document> docs = getDocuments(pl.list); 1358 1359 return new PartialList<>(docs, pl.totalSize); 1360 } 1361 1362 protected PartialList<String> doQuery(String query, String queryType, QueryFilter queryFilter, int countUpTo) { 1363 PartialList<Map<String, Serializable>> pl = doQueryAndFetch(query, queryType, queryFilter, countUpTo, true); 1364 List<String> ids = new ArrayList<String>(pl.list.size()); 1365 for (Map<String, Serializable> map : pl.list) { 1366 String id = (String) map.get(NXQL.ECM_UUID); 1367 ids.add(id); 1368 } 1369 return new PartialList<String>(ids, pl.totalSize); 1370 } 1371 1372 protected PartialList<Map<String, Serializable>> doQueryAndFetch(String query, String queryType, 1373 QueryFilter queryFilter, int countUpTo) { 1374 return doQueryAndFetch(query, queryType, queryFilter, countUpTo, false); 1375 } 1376 1377 protected PartialList<Map<String, Serializable>> doQueryAndFetch(String query, String queryType, 1378 QueryFilter queryFilter, int countUpTo, boolean onlyId) { 1379 if ("NXTAG".equals(queryType)) { 1380 // for now don't try to implement tags 1381 // and return an empty list 1382 return new PartialList<Map<String, Serializable>>(Collections.<Map<String, Serializable>> emptyList(), 0); 1383 } 1384 if (!NXQL.NXQL.equals(queryType)) { 1385 throw new NuxeoException("No QueryMaker accepts query type: " + queryType); 1386 } 1387 // transform the query according to the transformers defined by the 1388 // security policies 1389 SQLQuery sqlQuery = SQLQueryParser.parse(query); 1390 SelectClause selectClause = sqlQuery.select; 1391 if (selectClause.isDistinct()) { 1392 if (selectClause.isEmpty()) { 1393 // ok, turned into SELECT ecm:uuid 1394 } else if (selectClause.getSelectList().size() == 1 1395 && (selectClause.get(0).equals(new Reference(NXQL.ECM_UUID)))) { 1396 // ok, SELECT ecm:uuid 1397 } else { 1398 throw new QueryParseException("SELECT DISTINCT not supported on DBS"); 1399 } 1400 } 1401 for (SQLQuery.Transformer transformer : queryFilter.getQueryTransformers()) { 1402 sqlQuery = transformer.transform(queryFilter.getPrincipal(), sqlQuery); 1403 } 1404 OrderByClause orderByClause = sqlQuery.orderBy; 1405 1406 QueryOptimizer optimizer = new QueryOptimizer(); 1407 MultiExpression expression = optimizer.getOptimizedQuery(sqlQuery, queryFilter.getFacetFilter()); 1408 DBSExpressionEvaluator evaluator = new DBSExpressionEvaluator(this, selectClause, expression, 1409 queryFilter.getPrincipals()); 1410 1411 int limit = (int) queryFilter.getLimit(); 1412 int offset = (int) queryFilter.getOffset(); 1413 if (offset < 0) { 1414 offset = 0; 1415 } 1416 if (limit < 0) { 1417 limit = 0; 1418 } 1419 1420 int repoLimit; 1421 int repoOffset; 1422 OrderByClause repoOrderByClause; 1423 boolean postFilter = isOrderByPath(orderByClause); 1424 if (postFilter) { 1425 // we have to merge ordering and batching between memory and 1426 // repository 1427 repoLimit = 0; 1428 repoOffset = 0; 1429 repoOrderByClause = null; 1430 } else { 1431 // fast case, we can use the repository query directly 1432 repoLimit = limit; 1433 repoOffset = offset; 1434 repoOrderByClause = orderByClause; 1435 } 1436 1437 // query the repository 1438 boolean deepCopy = !onlyId; 1439 PartialList<State> pl = repository.queryAndFetch(expression, selectClause, repoOrderByClause, repoLimit, 1440 repoOffset, countUpTo, evaluator, deepCopy); 1441 1442 List<State> states = pl.list; 1443 long totalSize = pl.totalSize; 1444 if (totalSize >= 0) { 1445 if (countUpTo == -1) { 1446 // count full size 1447 } else if (countUpTo == 0) { 1448 // no count 1449 totalSize = -1; // not counted 1450 } else { 1451 // count only if less than countUpTo 1452 if (totalSize > countUpTo) { 1453 totalSize = -2; // truncated 1454 } 1455 } 1456 } 1457 1458 if (postFilter) { 1459 // ORDER BY 1460 if (orderByClause != null) { 1461 doOrderBy(states, orderByClause, evaluator); 1462 } 1463 // LIMIT / OFFSET 1464 if (limit != 0) { 1465 int size = states.size(); 1466 states.subList(0, offset > size ? size : offset).clear(); 1467 size = states.size(); 1468 if (limit < size) { 1469 states.subList(limit, size).clear(); 1470 } 1471 } 1472 } 1473 1474 List<Map<String, Serializable>> flatList; 1475 if (onlyId) { 1476 // optimize because we just need the id 1477 flatList = new ArrayList<>(states.size()); 1478 for (State state : states) { 1479 flatList.add(Collections.singletonMap(NXQL.ECM_UUID, state.get(KEY_ID))); 1480 } 1481 } else { 1482 flatList = flatten(states); 1483 } 1484 1485 return new PartialList<Map<String, Serializable>>(flatList, totalSize); 1486 } 1487 1488 /** Does an ORDER BY clause include ecm:path */ 1489 protected boolean isOrderByPath(OrderByClause orderByClause) { 1490 if (orderByClause == null) { 1491 return false; 1492 } 1493 for (OrderByExpr ob : orderByClause.elements) { 1494 if (ob.reference.name.equals(NXQL.ECM_PATH)) { 1495 return true; 1496 } 1497 } 1498 return false; 1499 } 1500 1501 protected String getPath(State state) { 1502 LinkedList<String> list = new LinkedList<String>(); 1503 for (boolean first = true;; first = false) { 1504 String name = (String) state.get(KEY_NAME); 1505 String parentId = (String) state.get(KEY_PARENT_ID); 1506 list.addFirst(name); 1507 if (parentId == null || (state = transaction.getStateForRead(parentId)) == null) { 1508 if (first) { 1509 if ("".equals(name)) { 1510 return "/"; // root 1511 } else { 1512 return name; // placeless, no slash 1513 } 1514 } else { 1515 return StringUtils.join(list, '/'); 1516 } 1517 } 1518 } 1519 } 1520 1521 protected void doOrderBy(List<State> states, OrderByClause orderByClause, DBSExpressionEvaluator evaluator) { 1522 if (isOrderByPath(orderByClause)) { 1523 // add path info to do the sort 1524 for (State state : states) { 1525 state.put(KEY_PATH_INTERNAL, getPath(state)); 1526 } 1527 } 1528 Collections.sort(states, new OrderByComparator(orderByClause, evaluator)); 1529 } 1530 1531 /** 1532 * Flatten and convert from internal names to NXQL. 1533 */ 1534 protected List<Map<String, Serializable>> flatten(List<State> states) { 1535 List<Map<String, Serializable>> flatList = new ArrayList<>(states.size()); 1536 for (State state : states) { 1537 Map<String, Serializable> map = new HashMap<>(); 1538 flatten(map, state, null); 1539 flatList.add(map); 1540 } 1541 return flatList; 1542 } 1543 1544 protected void flatten(Map<String, Serializable> map, State state, String prefix) { 1545 for (Entry<String, Serializable> en : state.entrySet()) { 1546 String key = en.getKey(); 1547 Serializable value = en.getValue(); 1548 String name; 1549 if (key.startsWith(KEY_PREFIX)) { 1550 name = convToNXQL(key); 1551 if (name == null) { 1552 // present in state but not returned to caller 1553 continue; 1554 } 1555 } else { 1556 name = key; 1557 } 1558 name = prefix == null ? name : prefix + name; 1559 if (value instanceof State) { 1560 flatten(map, (State) value, name + '/'); 1561 } else if (value instanceof List) { 1562 String nameSlash = name + '/'; 1563 int i = 0; 1564 for (Object v : (List<?>) value) { 1565 if (v instanceof State) { 1566 flatten(map, (State) v, nameSlash + i + '/'); 1567 } else { 1568 map.put(nameSlash + i, (Serializable) v); 1569 } 1570 i++; 1571 } 1572 } else if (value instanceof Object[]) { 1573 String nameSlash = name + '/'; 1574 int i = 0; 1575 for (Object v : (Object[]) value) { 1576 map.put(nameSlash + i, (Serializable) v); 1577 i++; 1578 } 1579 } else { 1580 map.put(name, value); 1581 } 1582 } 1583 } 1584 1585 @Override 1586 public IterableQueryResult queryAndFetch(String query, String queryType, QueryFilter queryFilter, Object[] params) { 1587 int countUpTo = -1; 1588 PartialList<Map<String, Serializable>> pl = doQueryAndFetch(query, queryType, queryFilter, countUpTo); 1589 return new DBSQueryResult(pl); 1590 } 1591 1592 protected static class DBSQueryResult implements IterableQueryResult, Iterator<Map<String, Serializable>> { 1593 1594 boolean closed; 1595 1596 protected List<Map<String, Serializable>> maps; 1597 1598 protected long totalSize; 1599 1600 protected long pos; 1601 1602 protected DBSQueryResult(PartialList<Map<String, Serializable>> pl) { 1603 this.maps = pl.list; 1604 this.totalSize = pl.totalSize; 1605 } 1606 1607 @Override 1608 public Iterator<Map<String, Serializable>> iterator() { 1609 return this; 1610 } 1611 1612 @Override 1613 public void close() { 1614 closed = true; 1615 pos = -1; 1616 } 1617 1618 @Override 1619 public boolean isLife() { 1620 return !closed; 1621 } 1622 1623 @Override 1624 public long size() { 1625 return totalSize; 1626 } 1627 1628 @Override 1629 public long pos() { 1630 return pos; 1631 } 1632 1633 @Override 1634 public void skipTo(long pos) { 1635 if (pos < 0) { 1636 pos = 0; 1637 } else if (pos > totalSize) { 1638 pos = totalSize; 1639 } 1640 this.pos = pos; 1641 } 1642 1643 @Override 1644 public boolean hasNext() { 1645 return pos < totalSize; 1646 } 1647 1648 @Override 1649 public Map<String, Serializable> next() { 1650 if (closed || pos == totalSize) { 1651 throw new NoSuchElementException(); 1652 } 1653 Map<String, Serializable> map = maps.get((int) pos); 1654 pos++; 1655 return map; 1656 } 1657 1658 @Override 1659 public void remove() { 1660 throw new UnsupportedOperationException(); 1661 } 1662 } 1663 1664 public static String convToInternal(String name) { 1665 switch (name) { 1666 case NXQL.ECM_UUID: 1667 return KEY_ID; 1668 case NXQL.ECM_NAME: 1669 return KEY_NAME; 1670 case NXQL.ECM_POS: 1671 return KEY_POS; 1672 case NXQL.ECM_PARENTID: 1673 return KEY_PARENT_ID; 1674 case NXQL.ECM_MIXINTYPE: 1675 return KEY_MIXIN_TYPES; 1676 case NXQL.ECM_PRIMARYTYPE: 1677 return KEY_PRIMARY_TYPE; 1678 case NXQL.ECM_ISPROXY: 1679 return KEY_IS_PROXY; 1680 case NXQL.ECM_ISVERSION: 1681 case NXQL.ECM_ISVERSION_OLD: 1682 return KEY_IS_VERSION; 1683 case NXQL.ECM_LIFECYCLESTATE: 1684 return KEY_LIFECYCLE_STATE; 1685 case NXQL.ECM_LOCK_OWNER: 1686 return KEY_LOCK_OWNER; 1687 case NXQL.ECM_LOCK_CREATED: 1688 return KEY_LOCK_CREATED; 1689 case NXQL.ECM_PROXY_TARGETID: 1690 return KEY_PROXY_TARGET_ID; 1691 case NXQL.ECM_PROXY_VERSIONABLEID: 1692 return KEY_PROXY_VERSION_SERIES_ID; 1693 case NXQL.ECM_ISCHECKEDIN: 1694 return KEY_IS_CHECKED_IN; 1695 case NXQL.ECM_ISLATESTVERSION: 1696 return KEY_IS_LATEST_VERSION; 1697 case NXQL.ECM_ISLATESTMAJORVERSION: 1698 return KEY_IS_LATEST_MAJOR_VERSION; 1699 case NXQL.ECM_VERSIONLABEL: 1700 return KEY_VERSION_LABEL; 1701 case NXQL.ECM_VERSIONCREATED: 1702 return KEY_VERSION_CREATED; 1703 case NXQL.ECM_VERSIONDESCRIPTION: 1704 return KEY_VERSION_DESCRIPTION; 1705 case NXQL.ECM_VERSION_VERSIONABLEID: 1706 return KEY_VERSION_SERIES_ID; 1707 case ExpressionEvaluator.NXQL_ECM_ANCESTOR_IDS: 1708 return KEY_ANCESTOR_IDS; 1709 case ExpressionEvaluator.NXQL_ECM_PATH: 1710 return KEY_PATH_INTERNAL; 1711 case ExpressionEvaluator.NXQL_ECM_READ_ACL: 1712 return KEY_READ_ACL; 1713 case NXQL.ECM_FULLTEXT_JOBID: 1714 return KEY_FULLTEXT_JOBID; 1715 case NXQL.ECM_FULLTEXT_SCORE: 1716 return KEY_FULLTEXT_SCORE; 1717 case NXQL.ECM_FULLTEXT: 1718 case NXQL.ECM_TAG: 1719 throw new UnsupportedOperationException(name); 1720 } 1721 throw new QueryParseException("No such property: " + name); 1722 } 1723 1724 public static String convToInternalAce(String name) { 1725 switch (name) { 1726 case NXQL.ECM_ACL_PRINCIPAL: 1727 return KEY_ACE_USER; 1728 case NXQL.ECM_ACL_PERMISSION: 1729 return KEY_ACE_PERMISSION; 1730 case NXQL.ECM_ACL_GRANT: 1731 return KEY_ACE_GRANT; 1732 case NXQL.ECM_ACL_CREATOR: 1733 return KEY_ACE_CREATOR; 1734 case NXQL.ECM_ACL_BEGIN: 1735 return KEY_ACE_BEGIN; 1736 case NXQL.ECM_ACL_END: 1737 return KEY_ACE_END; 1738 case NXQL.ECM_ACL_STATUS: 1739 return KEY_ACE_STATUS; 1740 } 1741 return null; 1742 } 1743 1744 public static String convToNXQL(String name) { 1745 switch (name) { 1746 case KEY_ID: 1747 return NXQL.ECM_UUID; 1748 case KEY_NAME: 1749 return NXQL.ECM_NAME; 1750 case KEY_POS: 1751 return NXQL.ECM_POS; 1752 case KEY_PARENT_ID: 1753 return NXQL.ECM_PARENTID; 1754 case KEY_MIXIN_TYPES: 1755 return NXQL.ECM_MIXINTYPE; 1756 case KEY_PRIMARY_TYPE: 1757 return NXQL.ECM_PRIMARYTYPE; 1758 case KEY_IS_PROXY: 1759 return NXQL.ECM_ISPROXY; 1760 case KEY_IS_VERSION: 1761 return NXQL.ECM_ISVERSION; 1762 case KEY_LIFECYCLE_STATE: 1763 return NXQL.ECM_LIFECYCLESTATE; 1764 case KEY_LOCK_OWNER: 1765 return NXQL.ECM_LOCK_OWNER; 1766 case KEY_LOCK_CREATED: 1767 return NXQL.ECM_LOCK_CREATED; 1768 case KEY_PROXY_TARGET_ID: 1769 return NXQL.ECM_PROXY_TARGETID; 1770 case KEY_PROXY_VERSION_SERIES_ID: 1771 return NXQL.ECM_PROXY_VERSIONABLEID; 1772 case KEY_IS_CHECKED_IN: 1773 return NXQL.ECM_ISCHECKEDIN; 1774 case KEY_IS_LATEST_VERSION: 1775 return NXQL.ECM_ISLATESTVERSION; 1776 case KEY_IS_LATEST_MAJOR_VERSION: 1777 return NXQL.ECM_ISLATESTMAJORVERSION; 1778 case KEY_VERSION_LABEL: 1779 return NXQL.ECM_VERSIONLABEL; 1780 case KEY_VERSION_CREATED: 1781 return NXQL.ECM_VERSIONCREATED; 1782 case KEY_VERSION_DESCRIPTION: 1783 return NXQL.ECM_VERSIONDESCRIPTION; 1784 case KEY_VERSION_SERIES_ID: 1785 return NXQL.ECM_VERSION_VERSIONABLEID; 1786 case KEY_MAJOR_VERSION: 1787 return "major_version"; // TODO XXX constant 1788 case KEY_MINOR_VERSION: 1789 return "minor_version"; 1790 case KEY_FULLTEXT_SCORE: 1791 return NXQL.ECM_FULLTEXT_SCORE; 1792 case KEY_LIFECYCLE_POLICY: 1793 case KEY_ACP: 1794 case KEY_ANCESTOR_IDS: 1795 case KEY_BASE_VERSION_ID: 1796 case KEY_READ_ACL: 1797 case KEY_FULLTEXT_SIMPLE: 1798 case KEY_FULLTEXT_BINARY: 1799 case KEY_FULLTEXT_JOBID: 1800 return null; 1801 } 1802 throw new QueryParseException("No such property: " + name); 1803 } 1804 1805 public static boolean isArray(String name) { 1806 switch (name) { 1807 case KEY_MIXIN_TYPES: 1808 case KEY_ANCESTOR_IDS: 1809 case KEY_PROXY_IDS: 1810 return true; 1811 } 1812 return false; 1813 } 1814 1815 public static boolean isBoolean(String name) { 1816 switch (name) { 1817 case KEY_IS_VERSION: 1818 case KEY_IS_CHECKED_IN: 1819 case KEY_IS_LATEST_VERSION: 1820 case KEY_IS_LATEST_MAJOR_VERSION: 1821 case KEY_IS_PROXY: 1822 return true; 1823 } 1824 return false; 1825 } 1826 1827 @Override 1828 public LockManager getLockManager() { 1829 return repository.getLockManager(); 1830 } 1831 1832}