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