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