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