001/* 002 * (C) Copyright 2009-2010 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 * Radu Darlea 018 * Catalin Baican 019 * Florent Guillaume 020 */ 021 022package org.nuxeo.ecm.platform.tag; 023 024import java.io.Serializable; 025import java.util.ArrayList; 026import java.util.Calendar; 027import java.util.Collections; 028import java.util.HashMap; 029import java.util.HashSet; 030import java.util.List; 031import java.util.Map; 032import java.util.Set; 033 034import org.nuxeo.ecm.core.api.CoreSession; 035import org.nuxeo.ecm.core.api.DocumentModel; 036import org.nuxeo.ecm.core.api.DocumentRef; 037import org.nuxeo.ecm.core.api.IdRef; 038import org.nuxeo.ecm.core.api.IterableQueryResult; 039import org.nuxeo.ecm.core.api.NuxeoException; 040import org.nuxeo.ecm.core.api.UnrestrictedSessionRunner; 041import org.nuxeo.ecm.core.api.event.DocumentEventTypes; 042import org.nuxeo.ecm.core.event.Event; 043import org.nuxeo.ecm.core.event.EventService; 044import org.nuxeo.ecm.core.event.impl.DocumentEventContext; 045import org.nuxeo.ecm.core.query.sql.NXQL; 046import org.nuxeo.ecm.platform.query.api.PageProvider; 047import org.nuxeo.ecm.platform.query.api.PageProviderDefinition; 048import org.nuxeo.ecm.platform.query.api.PageProviderService; 049import org.nuxeo.ecm.platform.query.nxql.CoreQueryAndFetchPageProvider; 050import org.nuxeo.runtime.api.Framework; 051import org.nuxeo.runtime.model.DefaultComponent; 052 053/** 054 * The implementation of the tag service. 055 */ 056public class TagServiceImpl extends DefaultComponent implements TagService { 057 058 public static final String NXTAG = TagQueryMaker.NXTAG; 059 060 protected enum PAGE_PROVIDERS { 061 // 062 GET_DOCUMENT_IDS_FOR_TAG, 063 // 064 GET_FIRST_TAGGING_FOR_DOC_AND_TAG_AND_USER, 065 // 066 GET_FIRST_TAGGING_FOR_DOC_AND_TAG, 067 // 068 GET_TAGS_FOR_DOCUMENT, 069 // core version: should keep on querying VCS 070 GET_TAGS_FOR_DOCUMENT_CORE, 071 // 072 GET_DOCUMENTS_FOR_TAG, 073 // 074 GET_TAGS_FOR_DOCUMENT_AND_USER, 075 // core version: should keep on querying VCS 076 GET_TAGS_FOR_DOCUMENT_AND_USER_CORE, 077 // 078 GET_DOCUMENTS_FOR_TAG_AND_USER, 079 // 080 GET_TAGS_TO_COPY_FOR_DOCUMENT, 081 // 082 GET_TAG_SUGGESTIONS, 083 // 084 GET_TAG_SUGGESTIONS_FOR_USER, 085 // 086 GET_TAGGED_DOCUMENTS_UNDER, 087 // 088 GET_ALL_TAGS, 089 // 090 GET_ALL_TAGS_FOR_USER, 091 // 092 GET_TAGS_FOR_DOCUMENTS, 093 // 094 GET_TAGS_FOR_DOCUMENTS_AND_USER, 095 } 096 097 @Override 098 public boolean isEnabled() { 099 return true; 100 } 101 102 protected static String cleanLabel(String label, boolean allowEmpty, boolean allowPercent) { 103 if (label == null) { 104 if (allowEmpty) { 105 return null; 106 } 107 throw new NuxeoException("Invalid empty tag"); 108 } 109 label = label.toLowerCase(); // lowercase 110 label = label.replace(" ", ""); // no spaces 111 label = label.replace("\\", ""); // dubious char 112 label = label.replace("'", ""); // dubious char 113 if (!allowPercent) { 114 label = label.replace("%", ""); // dubious char 115 } 116 if (label.length() == 0) { 117 throw new NuxeoException("Invalid empty tag"); 118 } 119 return label; 120 } 121 122 protected static String cleanUsername(String username) { 123 return username == null ? null : username.replace("'", ""); 124 } 125 126 public void tag(CoreSession session, String docId, String label, String username) { 127 UnrestrictedAddTagging r = new UnrestrictedAddTagging(session, docId, label, username); 128 r.runUnrestricted(); 129 fireUpdateEvent(session, docId); 130 } 131 132 protected void fireUpdateEvent(CoreSession session, String docId) { 133 DocumentRef documentRef = new IdRef(docId); 134 if (session.exists(documentRef)) { 135 DocumentModel documentModel = session.getDocument(documentRef); 136 DocumentEventContext ctx = new DocumentEventContext(session, session.getPrincipal(), documentModel); 137 Event event = ctx.newEvent(DocumentEventTypes.DOCUMENT_TAG_UPDATED); 138 Framework.getLocalService(EventService.class).fireEvent(event); 139 } 140 } 141 142 protected static class UnrestrictedAddTagging extends UnrestrictedSessionRunner { 143 private final String docId; 144 145 private final String label; 146 147 private final String username; 148 149 protected UnrestrictedAddTagging(CoreSession session, String docId, String label, String username) 150 { 151 super(session); 152 this.docId = docId; 153 this.label = cleanLabel(label, false, false); 154 this.username = cleanUsername(username); 155 } 156 157 @Override 158 public void run() { 159 // Find tag 160 List<Map<String, Serializable>> res = getItems(PAGE_PROVIDERS.GET_DOCUMENT_IDS_FOR_TAG.name(), session, 161 label); 162 String tagId = (res != null && !res.isEmpty()) ? (String) res.get(0).get(NXQL.ECM_UUID) : null; 163 Calendar date = Calendar.getInstance(); 164 if (tagId == null) { 165 // no tag found, create it 166 DocumentModel tag = session.createDocumentModel(null, label, TagConstants.TAG_DOCUMENT_TYPE); 167 tag.setPropertyValue("dc:created", date); 168 tag.setPropertyValue(TagConstants.TAG_LABEL_FIELD, label); 169 tag = session.createDocument(tag); 170 tagId = tag.getId(); 171 } 172 // Check if tagging already exists for user. 173 if (username != null) { 174 res = getItems(PAGE_PROVIDERS.GET_FIRST_TAGGING_FOR_DOC_AND_TAG_AND_USER.name(), session, docId, tagId, 175 username); 176 } else { 177 res = getItems(PAGE_PROVIDERS.GET_FIRST_TAGGING_FOR_DOC_AND_TAG.name(), session, docId, tagId); 178 } 179 if (res != null && !res.isEmpty()) { 180 // tagging already exists 181 return; 182 } 183 // Add tagging to the document. 184 DocumentModel tagging = session.createDocumentModel(null, label, TagConstants.TAGGING_DOCUMENT_TYPE); 185 tagging.setPropertyValue("dc:created", date); 186 if (username != null) { 187 tagging.setPropertyValue("dc:creator", username); 188 } 189 tagging.setPropertyValue(TagConstants.TAGGING_SOURCE_FIELD, docId); 190 tagging.setPropertyValue(TagConstants.TAGGING_TARGET_FIELD, tagId); 191 session.createDocument(tagging); 192 session.save(); 193 } 194 } 195 196 public void untag(CoreSession session, String docId, String label, String username) { 197 UnrestrictedRemoveTagging r = new UnrestrictedRemoveTagging(session, docId, label, username); 198 r.runUnrestricted(); 199 if (label != null) { 200 fireUpdateEvent(session, docId); 201 } 202 } 203 204 protected static class UnrestrictedRemoveTagging extends UnrestrictedSessionRunner { 205 206 private final String docId; 207 208 private final String label; 209 210 private final String username; 211 212 protected UnrestrictedRemoveTagging(CoreSession session, String docId, String label, String username) 213 { 214 super(session); 215 this.docId = docId; 216 this.label = cleanLabel(label, true, false); 217 this.username = cleanUsername(username); 218 } 219 220 @Override 221 public void run() { 222 String tagId = null; 223 if (label != null) { 224 // Find tag 225 List<Map<String, Serializable>> res = getItems(PAGE_PROVIDERS.GET_DOCUMENT_IDS_FOR_TAG.name(), session, 226 label); 227 tagId = (res != null && !res.isEmpty()) ? (String) res.get(0).get(NXQL.ECM_UUID) : null; 228 if (tagId == null) { 229 // tag not found 230 return; 231 } 232 } 233 // Find taggings for user. 234 Set<String> taggingIds = new HashSet<String>(); 235 String query = String.format("SELECT ecm:uuid FROM Tagging " + "WHERE relation:source = '%s'", docId); 236 if (tagId != null) { 237 query += String.format(" AND relation:target = '%s'", tagId); 238 } 239 if (username != null) { 240 query += String.format(" AND dc:creator = '%s'", username); 241 } 242 IterableQueryResult res = session.queryAndFetch(query, NXQL.NXQL); 243 try { 244 for (Map<String, Serializable> map : res) { 245 taggingIds.add((String) map.get(NXQL.ECM_UUID)); 246 } 247 } finally { 248 res.close(); 249 } 250 // Remove taggings 251 for (String taggingId : taggingIds) { 252 session.removeDocument(new IdRef(taggingId)); 253 } 254 if (!taggingIds.isEmpty()) { 255 session.save(); 256 } 257 } 258 } 259 260 public List<Tag> getDocumentTags(CoreSession session, String docId, String username) { 261 return getDocumentTags(session, docId, username, true); 262 } 263 264 public List<Tag> getDocumentTags(CoreSession session, String docId, String username, boolean useCore) 265 { 266 UnrestrictedGetDocumentTags r = new UnrestrictedGetDocumentTags(session, docId, username, useCore); 267 r.runUnrestricted(); 268 return r.tags; 269 } 270 271 protected static class UnrestrictedGetDocumentTags extends UnrestrictedSessionRunner { 272 273 protected final String docId; 274 275 protected final String username; 276 277 protected final List<Tag> tags; 278 279 protected final boolean useCore; 280 281 protected UnrestrictedGetDocumentTags(CoreSession session, String docId, String username, boolean useCore) 282 { 283 super(session); 284 this.docId = docId; 285 this.username = cleanUsername(username); 286 this.useCore = useCore; 287 tags = new ArrayList<Tag>(); 288 } 289 290 @Override 291 public void run() { 292 List<Map<String, Serializable>> res; 293 if (username == null) { 294 String ppName = PAGE_PROVIDERS.GET_TAGS_FOR_DOCUMENT.name(); 295 if (useCore) { 296 ppName = PAGE_PROVIDERS.GET_TAGS_FOR_DOCUMENT_CORE.name(); 297 } 298 res = getItems(ppName, session, docId); 299 } else { 300 String ppName = PAGE_PROVIDERS.GET_TAGS_FOR_DOCUMENT_AND_USER.name(); 301 if (useCore) { 302 ppName = PAGE_PROVIDERS.GET_TAGS_FOR_DOCUMENT_AND_USER_CORE.name(); 303 } 304 res = getItems(ppName, session, docId, username); 305 } 306 if (res != null) { 307 for (Map<String, Serializable> map : res) { 308 String label = (String) map.get(TagConstants.TAG_LABEL_FIELD); 309 tags.add(new Tag(label, 0)); 310 } 311 } 312 } 313 } 314 315 @Override 316 public void removeTags(CoreSession session, String docId) { 317 untag(session, docId, null, null); 318 } 319 320 @Override 321 public void copyTags(CoreSession session, String srcDocId, String dstDocId) { 322 copyTags(session, srcDocId, dstDocId, false); 323 } 324 325 protected void copyTags(CoreSession session, String srcDocId, String dstDocId, boolean removeExistingTags) 326 { 327 if (removeExistingTags) { 328 removeTags(session, dstDocId); 329 } 330 331 UnrestrictedCopyTags r = new UnrestrictedCopyTags(session, srcDocId, dstDocId); 332 r.runUnrestricted(); 333 } 334 335 protected static class UnrestrictedCopyTags extends UnrestrictedSessionRunner { 336 337 protected final String srcDocId; 338 339 protected final String dstDocId; 340 341 protected UnrestrictedCopyTags(CoreSession session, String srcDocId, String dstDocId) { 342 super(session); 343 this.srcDocId = srcDocId; 344 this.dstDocId = dstDocId; 345 } 346 347 @Override 348 public void run() { 349 Set<String> existingTags = new HashSet<>(); 350 List<Map<String, Serializable>> dstTagsRes = getItems(PAGE_PROVIDERS.GET_TAGS_TO_COPY_FOR_DOCUMENT.name(), 351 session, dstDocId); 352 if (dstTagsRes != null) { 353 for (Map<String, Serializable> map : dstTagsRes) { 354 existingTags.add(String.format("%s/%s", map.get("tag:label"), map.get("dc:creator"))); 355 } 356 } 357 358 List<Map<String, Serializable>> srcTagsRes = getItems(PAGE_PROVIDERS.GET_TAGS_TO_COPY_FOR_DOCUMENT.name(), 359 session, srcDocId); 360 if (srcTagsRes != null) { 361 boolean docCreated = false; 362 for (Map<String, Serializable> map : srcTagsRes) { 363 String key = String.format("%s/%s", map.get("tag:label"), map.get("dc:creator")); 364 if (!existingTags.contains(key)) { 365 DocumentModel tagging = session.createDocumentModel(null, (String) map.get("tag:label"), 366 TagConstants.TAGGING_DOCUMENT_TYPE); 367 tagging.setPropertyValue("dc:created", map.get("dc:created")); 368 tagging.setPropertyValue("dc:creator", map.get("dc:creator")); 369 tagging.setPropertyValue(TagConstants.TAGGING_SOURCE_FIELD, dstDocId); 370 tagging.setPropertyValue(TagConstants.TAGGING_TARGET_FIELD, map.get("relation:target")); 371 session.createDocument(tagging); 372 docCreated = true; 373 } 374 } 375 if (docCreated) { 376 session.save(); 377 } 378 } 379 } 380 } 381 382 @Override 383 public void replaceTags(CoreSession session, String srcDocId, String dstDocId) { 384 copyTags(session, srcDocId, dstDocId, true); 385 } 386 387 public List<String> getTagDocumentIds(CoreSession session, String label, String username) { 388 UnrestrictedGetTagDocumentIds r = new UnrestrictedGetTagDocumentIds(session, label, username); 389 r.runUnrestricted(); 390 return r.docIds; 391 } 392 393 protected static class UnrestrictedGetTagDocumentIds extends UnrestrictedSessionRunner { 394 395 protected final String label; 396 397 protected final String username; 398 399 protected final List<String> docIds; 400 401 protected UnrestrictedGetTagDocumentIds(CoreSession session, String label, String username) 402 { 403 super(session); 404 this.label = cleanLabel(label, false, false); 405 this.username = cleanUsername(username); 406 docIds = new ArrayList<String>(); 407 } 408 409 @Override 410 public void run() { 411 List<Map<String, Serializable>> res; 412 if (username == null) { 413 res = getItems(PAGE_PROVIDERS.GET_DOCUMENTS_FOR_TAG.name(), session, label); 414 } else { 415 res = getItems(PAGE_PROVIDERS.GET_DOCUMENTS_FOR_TAG_AND_USER.name(), session, label, username); 416 } 417 if (res != null) { 418 for (Map<String, Serializable> map : res) { 419 docIds.add((String) map.get(TagConstants.TAGGING_SOURCE_FIELD)); 420 } 421 } 422 } 423 } 424 425 public List<Tag> getTagCloud(CoreSession session, String docId, String username, Boolean normalize) 426 { 427 UnrestrictedGetDocumentCloud r = new UnrestrictedGetDocumentCloud(session, docId, username, normalize); 428 r.runUnrestricted(); 429 return r.cloud; 430 } 431 432 protected static class UnrestrictedGetDocumentCloud extends UnrestrictedSessionRunner { 433 434 protected final String docId; 435 436 protected final String username; 437 438 protected final List<Tag> cloud; 439 440 protected final Boolean normalize; 441 442 protected UnrestrictedGetDocumentCloud(CoreSession session, String docId, String username, Boolean normalize) 443 { 444 super(session); 445 this.docId = docId; 446 this.username = cleanUsername(username); 447 this.normalize = normalize; 448 cloud = new ArrayList<Tag>(); 449 } 450 451 @Override 452 public void run() { 453 List<Map<String, Serializable>> res; 454 if (docId == null) { 455 if (username == null) { 456 res = getItems(PAGE_PROVIDERS.GET_ALL_TAGS.name(), session); 457 } else { 458 res = getItems(PAGE_PROVIDERS.GET_ALL_TAGS_FOR_USER.name(), session, username); 459 } 460 } else { 461 // find all docs under docid 462 String path = session.getDocument(new IdRef(docId)).getPathAsString(); 463 path = path.replace("'", ""); 464 List<String> docIds = new ArrayList<String>(); 465 docIds.add(docId); 466 List<Map<String, Serializable>> docRes = getItems(PAGE_PROVIDERS.GET_TAGGED_DOCUMENTS_UNDER.name(), 467 session, path); 468 if (docRes != null) { 469 for (Map<String, Serializable> map : docRes) { 470 docIds.add((String) map.get(NXQL.ECM_UUID)); 471 } 472 } 473 474 if (username == null) { 475 res = getItems(PAGE_PROVIDERS.GET_TAGS_FOR_DOCUMENTS.name(), session, docIds); 476 } else { 477 res = getItems(PAGE_PROVIDERS.GET_TAGS_FOR_DOCUMENTS_AND_USER.name(), session, docIds, username); 478 } 479 } 480 481 int min = 999999, max = 0; 482 if (res != null) { 483 for (Map<String, Serializable> map : res) { 484 String label = (String) map.get(TagConstants.TAG_LABEL_FIELD); 485 int weight = ((Long) map.get(TagConstants.TAGGING_SOURCE_FIELD)).intValue(); 486 if (weight == 0) { 487 // shouldn't happen 488 continue; 489 } 490 if (weight > max) { 491 max = weight; 492 } 493 if (weight < min) { 494 min = weight; 495 } 496 Tag weightedTag = new Tag(label, weight); 497 cloud.add(weightedTag); 498 } 499 } 500 if (normalize != null) { 501 normalizeCloud(cloud, min, max, !normalize.booleanValue()); 502 } 503 } 504 } 505 506 public static void normalizeCloud(List<Tag> cloud, int min, int max, boolean linear) { 507 if (min == max) { 508 for (Tag tag : cloud) { 509 tag.setWeight(100); 510 } 511 return; 512 } 513 double nmin; 514 double diff; 515 if (linear) { 516 nmin = min; 517 diff = max - min; 518 } else { 519 nmin = Math.log(min); 520 diff = Math.log(max) - nmin; 521 } 522 for (Tag tag : cloud) { 523 long weight = tag.getWeight(); 524 double norm; 525 if (linear) { 526 norm = (weight - nmin) / diff; 527 } else { 528 norm = (Math.log(weight) - nmin) / diff; 529 } 530 tag.setWeight(Math.round(100 * norm)); 531 } 532 } 533 534 public List<Tag> getSuggestions(CoreSession session, String label, String username) { 535 UnrestrictedGetTagSuggestions r = new UnrestrictedGetTagSuggestions(session, label, username); 536 r.runUnrestricted(); 537 return r.tags; 538 } 539 540 protected static class UnrestrictedGetTagSuggestions extends UnrestrictedSessionRunner { 541 542 protected final String label; 543 544 protected final String username; 545 546 protected final List<Tag> tags; 547 548 protected UnrestrictedGetTagSuggestions(CoreSession session, String label, String username) 549 { 550 super(session); 551 label = cleanLabel(label, false, true); 552 if (!label.contains("%")) { 553 label += "%"; 554 } 555 this.label = label; 556 this.username = cleanUsername(username); 557 tags = new ArrayList<Tag>(); 558 } 559 560 @Override 561 public void run() { 562 List<Map<String, Serializable>> res; 563 if (username == null) { 564 res = getItems(PAGE_PROVIDERS.GET_TAG_SUGGESTIONS.name(), session, label); 565 } else { 566 res = getItems(PAGE_PROVIDERS.GET_TAG_SUGGESTIONS_FOR_USER.name(), session, label, username); 567 } 568 if (res != null) { 569 for (Map<String, Serializable> map : res) { 570 String label = (String) map.get(TagConstants.TAG_LABEL_FIELD); 571 tags.add(new Tag(label, 0)); 572 } 573 } 574 // XXX should sort on tag weight 575 Collections.sort(tags, Tag.LABEL_COMPARATOR); 576 } 577 } 578 579 /** 580 * Returns results from calls to {@link CoreSession#queryAndFetch(String, String, Object...)} using page providers. 581 * 582 * @since 6.0 583 */ 584 @SuppressWarnings("unchecked") 585 protected static List<Map<String, Serializable>> getItems(String pageProviderName, CoreSession session, 586 Object... params) { 587 PageProviderService ppService = Framework.getService(PageProviderService.class); 588 if (ppService == null) { 589 throw new RuntimeException("Missing PageProvider service"); 590 } 591 Map<String, Serializable> props = new HashMap<String, Serializable>(); 592 // first retrieve potential props from definition 593 PageProviderDefinition def = ppService.getPageProviderDefinition(pageProviderName); 594 if (def != null) { 595 Map<String, String> defProps = def.getProperties(); 596 if (defProps != null) { 597 props.putAll(defProps); 598 } 599 } 600 props.put(CoreQueryAndFetchPageProvider.CORE_SESSION_PROPERTY, (Serializable) session); 601 PageProvider<Map<String, Serializable>> pp = (PageProvider<Map<String, Serializable>>) ppService.getPageProvider( 602 pageProviderName, null, null, null, props, params); 603 if (pp == null) { 604 throw new NuxeoException("Page provider not found: " + pageProviderName); 605 } 606 return pp.getCurrentPage(); 607 } 608}