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