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