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