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