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