/* * (C) Copyright 2009-2016 Nuxeo SA (http://nuxeo.com/) and others. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * Contributors: * Radu Darlea * Catalin Baican * Florent Guillaume */ package org.nuxeo.ecm.platform.tag; import java.io.Serializable; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.nuxeo.ecm.core.api.CoreSession; import org.nuxeo.ecm.core.api.DocumentModel; import org.nuxeo.ecm.core.api.DocumentRef; import org.nuxeo.ecm.core.api.DocumentSecurityException; import org.nuxeo.ecm.core.api.IdRef; import org.nuxeo.ecm.core.api.IterableQueryResult; import org.nuxeo.ecm.core.api.NuxeoException; import org.nuxeo.ecm.core.api.UnrestrictedSessionRunner; import org.nuxeo.ecm.core.api.event.DocumentEventTypes; import org.nuxeo.ecm.core.api.security.SecurityConstants; import org.nuxeo.ecm.core.event.Event; import org.nuxeo.ecm.core.event.EventService; import org.nuxeo.ecm.core.event.impl.DocumentEventContext; import org.nuxeo.ecm.core.query.sql.NXQL; import org.nuxeo.ecm.platform.query.api.PageProvider; import org.nuxeo.ecm.platform.query.api.PageProviderDefinition; import org.nuxeo.ecm.platform.query.api.PageProviderService; import org.nuxeo.ecm.platform.query.nxql.CoreQueryAndFetchPageProvider; import org.nuxeo.runtime.api.Framework; import org.nuxeo.runtime.model.DefaultComponent; /** * The implementation of the tag service. */ public class TagServiceImpl extends DefaultComponent implements TagService { public static final String NXTAG = TagQueryMaker.NXTAG; protected enum PAGE_PROVIDERS { // GET_DOCUMENT_IDS_FOR_TAG, // GET_FIRST_TAGGING_FOR_DOC_AND_TAG_AND_USER, // GET_FIRST_TAGGING_FOR_DOC_AND_TAG, // GET_TAGS_FOR_DOCUMENT, // core version: should keep on querying VCS GET_TAGS_FOR_DOCUMENT_CORE, // GET_DOCUMENTS_FOR_TAG, // GET_TAGS_FOR_DOCUMENT_AND_USER, // core version: should keep on querying VCS GET_TAGS_FOR_DOCUMENT_AND_USER_CORE, // GET_DOCUMENTS_FOR_TAG_AND_USER, // GET_TAGS_TO_COPY_FOR_DOCUMENT, // GET_TAG_SUGGESTIONS, // GET_TAG_SUGGESTIONS_FOR_USER, // GET_TAGGED_DOCUMENTS_UNDER, // GET_ALL_TAGS, // GET_ALL_TAGS_FOR_USER, // GET_TAGS_FOR_DOCUMENTS, // GET_TAGS_FOR_DOCUMENTS_AND_USER, } @Override public boolean isEnabled() { return true; } protected static String cleanLabel(String label, boolean allowEmpty, boolean allowPercent) { if (label == null) { if (allowEmpty) { return null; } throw new NuxeoException("Invalid empty tag"); } label = label.toLowerCase(); // lowercase label = label.replace(" ", ""); // no spaces label = label.replace("\\", ""); // dubious char label = label.replace("'", ""); // dubious char if (!allowPercent) { label = label.replace("%", ""); // dubious char } if (label.length() == 0) { throw new NuxeoException("Invalid empty tag"); } return label; } protected static String cleanUsername(String username) { return username == null ? null : username.replace("'", ""); } @Override public void tag(CoreSession session, String docId, String label, String username) { UnrestrictedAddTagging r = new UnrestrictedAddTagging(session, docId, label, username); r.runUnrestricted(); fireUpdateEvent(session, docId); } protected void fireUpdateEvent(CoreSession session, String docId) { DocumentRef documentRef = new IdRef(docId); if (session.exists(documentRef)) { DocumentModel documentModel = session.getDocument(documentRef); DocumentEventContext ctx = new DocumentEventContext(session, session.getPrincipal(), documentModel); Event event = ctx.newEvent(DocumentEventTypes.DOCUMENT_TAG_UPDATED); Framework.getLocalService(EventService.class).fireEvent(event); } } protected static class UnrestrictedAddTagging extends UnrestrictedSessionRunner { private final String docId; private final String label; private final String username; protected UnrestrictedAddTagging(CoreSession session, String docId, String label, String username) { super(session); this.docId = docId; this.label = cleanLabel(label, false, false); this.username = cleanUsername(username); } @Override public void run() { // Find tag List<Map<String, Serializable>> res = getItems(PAGE_PROVIDERS.GET_DOCUMENT_IDS_FOR_TAG.name(), session, label); String tagId = (res != null && !res.isEmpty()) ? (String) res.get(0).get(NXQL.ECM_UUID) : null; Calendar date = Calendar.getInstance(); if (tagId == null) { // no tag found, create it DocumentModel tag = session.createDocumentModel(null, label, TagConstants.TAG_DOCUMENT_TYPE); tag.setPropertyValue("dc:created", date); tag.setPropertyValue(TagConstants.TAG_LABEL_FIELD, label); tag = session.createDocument(tag); tagId = tag.getId(); } // Check if tagging already exists for user. if (username != null) { res = getItems(PAGE_PROVIDERS.GET_FIRST_TAGGING_FOR_DOC_AND_TAG_AND_USER.name(), session, docId, tagId, username); } else { res = getItems(PAGE_PROVIDERS.GET_FIRST_TAGGING_FOR_DOC_AND_TAG.name(), session, docId, tagId); } if (res != null && !res.isEmpty()) { // tagging already exists return; } // Add tagging to the document. DocumentModel tagging = session.createDocumentModel(null, label, TagConstants.TAGGING_DOCUMENT_TYPE); tagging.setPropertyValue("dc:created", date); if (username != null) { tagging.setPropertyValue("dc:creator", username); } tagging.setPropertyValue(TagConstants.TAGGING_SOURCE_FIELD, docId); tagging.setPropertyValue(TagConstants.TAGGING_TARGET_FIELD, tagId); session.createDocument(tagging); session.save(); } } @Override public void untag(CoreSession session, String docId, String label, String username) throws DocumentSecurityException { // There's two allowed cases here: // - document doesn't exist, we're here after documentRemoved event // - regular case: check if user can remove this tag on document if (!session.exists(new IdRef(docId)) || canUntag(session, docId, label)) { UnrestrictedRemoveTagging r = new UnrestrictedRemoveTagging(session, docId, label, username); r.runUnrestricted(); if (label != null) { fireUpdateEvent(session, docId); } } else { String principalName = session.getPrincipal().getName(); throw new DocumentSecurityException("User '" + principalName + "' is not allowed to remove tag '" + label + "' on document '" + docId + "'"); } } protected static class UnrestrictedRemoveTagging extends UnrestrictedSessionRunner { private final String docId; private final String label; private final String username; protected UnrestrictedRemoveTagging(CoreSession session, String docId, String label, String username) { super(session); this.docId = docId; this.label = cleanLabel(label, true, false); this.username = cleanUsername(username); } @Override public void run() { String tagId = null; if (label != null) { // Find tag List<Map<String, Serializable>> res = getItems(PAGE_PROVIDERS.GET_DOCUMENT_IDS_FOR_TAG.name(), session, label); tagId = (res != null && !res.isEmpty()) ? (String) res.get(0).get(NXQL.ECM_UUID) : null; if (tagId == null) { // tag not found return; } } // Find taggings for user. Set<String> taggingIds = new HashSet<>(); String query = String.format("SELECT ecm:uuid FROM Tagging WHERE relation:source = '%s'", docId); if (tagId != null) { query += String.format(" AND relation:target = '%s'", tagId); } if (username != null) { query += String.format(" AND dc:creator = '%s'", username); } try (IterableQueryResult res = session.queryAndFetch(query, NXQL.NXQL)) { for (Map<String, Serializable> map : res) { taggingIds.add((String) map.get(NXQL.ECM_UUID)); } } // Remove taggings for (String taggingId : taggingIds) { session.removeDocument(new IdRef(taggingId)); } if (!taggingIds.isEmpty()) { session.save(); } } } /** * @since 8.4 */ @Override public boolean canUntag(CoreSession session, String docId, String label) { if (session.hasPermission(new IdRef(docId), SecurityConstants.WRITE)) { // If user has WRITE permission, user can remove any tags return true; } // Else check if desired tag was created by current user UnrestrictedCanRemoveTagging r = new UnrestrictedCanRemoveTagging(session, docId, label); r.runUnrestricted(); return r.canUntag; } protected static class UnrestrictedCanRemoveTagging extends UnrestrictedSessionRunner { private final String docId; private final String label; private boolean canUntag; protected UnrestrictedCanRemoveTagging(CoreSession session, String docId, String label) { super(session); this.docId = docId; this.label = cleanLabel(label, true, false); this.canUntag = false; } @Override public void run() { String tagId = null; if (label != null) { // Find tag List<Map<String, Serializable>> res = getItems(PAGE_PROVIDERS.GET_DOCUMENT_IDS_FOR_TAG.name(), session, label); tagId = (res != null && !res.isEmpty()) ? (String) res.get(0).get(NXQL.ECM_UUID) : null; if (tagId == null) { // tag not found - so user can untag canUntag = true; return; } } // Find creators of tag(s). Set<String> creators = new HashSet<>(); String query = String.format("SELECT DISTINCT dc:creator FROM Tagging WHERE relation:source = '%s'", docId); if (tagId != null) { query += String.format(" AND relation:target = '%s'", tagId); } try (IterableQueryResult res = session.queryAndFetch(query, NXQL.NXQL)) { for (Map<String, Serializable> map : res) { creators.add((String) map.get("dc:creator")); } } // Check if user can untag // - in case of one tag, check if creators contains user // - in case of all tags, check if user is the only creator canUntag = creators.size() == 1 && creators.contains(originatingUsername); } } @Override public List<Tag> getDocumentTags(CoreSession session, String docId, String username) { return getDocumentTags(session, docId, username, true); } @Override public List<Tag> getDocumentTags(CoreSession session, String docId, String username, boolean useCore) { UnrestrictedGetDocumentTags r = new UnrestrictedGetDocumentTags(session, docId, username, useCore); r.runUnrestricted(); return r.tags; } protected static class UnrestrictedGetDocumentTags extends UnrestrictedSessionRunner { protected final String docId; protected final String username; protected final List<Tag> tags; protected final boolean useCore; protected UnrestrictedGetDocumentTags(CoreSession session, String docId, String username, boolean useCore) { super(session); this.docId = docId; this.username = cleanUsername(username); this.useCore = useCore; this.tags = new ArrayList<>(); } @Override public void run() { List<Map<String, Serializable>> res; if (username == null) { String ppName = PAGE_PROVIDERS.GET_TAGS_FOR_DOCUMENT.name(); if (useCore) { ppName = PAGE_PROVIDERS.GET_TAGS_FOR_DOCUMENT_CORE.name(); } res = getItems(ppName, session, docId); } else { String ppName = PAGE_PROVIDERS.GET_TAGS_FOR_DOCUMENT_AND_USER.name(); if (useCore) { ppName = PAGE_PROVIDERS.GET_TAGS_FOR_DOCUMENT_AND_USER_CORE.name(); } res = getItems(ppName, session, docId, username); } if (res != null) { for (Map<String, Serializable> map : res) { String label = (String) map.get(TagConstants.TAG_LABEL_FIELD); tags.add(new Tag(label, 0)); } } } } @Override public void removeTags(CoreSession session, String docId) { untag(session, docId, null, null); } @Override public void copyTags(CoreSession session, String srcDocId, String dstDocId) { copyTags(session, srcDocId, dstDocId, false); } protected void copyTags(CoreSession session, String srcDocId, String dstDocId, boolean removeExistingTags) { if (removeExistingTags) { removeTags(session, dstDocId); } UnrestrictedCopyTags r = new UnrestrictedCopyTags(session, srcDocId, dstDocId); r.runUnrestricted(); } protected static class UnrestrictedCopyTags extends UnrestrictedSessionRunner { protected final String srcDocId; protected final String dstDocId; protected UnrestrictedCopyTags(CoreSession session, String srcDocId, String dstDocId) { super(session); this.srcDocId = srcDocId; this.dstDocId = dstDocId; } @Override public void run() { Set<String> existingTags = new HashSet<>(); List<Map<String, Serializable>> dstTagsRes = getItems(PAGE_PROVIDERS.GET_TAGS_TO_COPY_FOR_DOCUMENT.name(), session, dstDocId); if (dstTagsRes != null) { for (Map<String, Serializable> map : dstTagsRes) { existingTags.add(String.format("%s/%s", map.get("tag:label"), map.get("dc:creator"))); } } List<Map<String, Serializable>> srcTagsRes = getItems(PAGE_PROVIDERS.GET_TAGS_TO_COPY_FOR_DOCUMENT.name(), session, srcDocId); if (srcTagsRes != null) { boolean docCreated = false; for (Map<String, Serializable> map : srcTagsRes) { String key = String.format("%s/%s", map.get("tag:label"), map.get("dc:creator")); if (!existingTags.contains(key)) { DocumentModel tagging = session.createDocumentModel(null, (String) map.get("tag:label"), TagConstants.TAGGING_DOCUMENT_TYPE); tagging.setPropertyValue("dc:created", map.get("dc:created")); tagging.setPropertyValue("dc:creator", map.get("dc:creator")); tagging.setPropertyValue(TagConstants.TAGGING_SOURCE_FIELD, dstDocId); tagging.setPropertyValue(TagConstants.TAGGING_TARGET_FIELD, map.get("relation:target")); session.createDocument(tagging); docCreated = true; } } if (docCreated) { session.save(); } } } } @Override public void replaceTags(CoreSession session, String srcDocId, String dstDocId) { copyTags(session, srcDocId, dstDocId, true); } @Override public List<String> getTagDocumentIds(CoreSession session, String label, String username) { UnrestrictedGetTagDocumentIds r = new UnrestrictedGetTagDocumentIds(session, label, username); r.runUnrestricted(); return r.docIds; } protected static class UnrestrictedGetTagDocumentIds extends UnrestrictedSessionRunner { protected final String label; protected final String username; protected final List<String> docIds; protected UnrestrictedGetTagDocumentIds(CoreSession session, String label, String username) { super(session); this.label = cleanLabel(label, false, false); this.username = cleanUsername(username); this.docIds = new ArrayList<>(); } @Override public void run() { List<Map<String, Serializable>> res; if (username == null) { res = getItems(PAGE_PROVIDERS.GET_DOCUMENTS_FOR_TAG.name(), session, label); } else { res = getItems(PAGE_PROVIDERS.GET_DOCUMENTS_FOR_TAG_AND_USER.name(), session, label, username); } if (res != null) { for (Map<String, Serializable> map : res) { docIds.add((String) map.get(TagConstants.TAGGING_SOURCE_FIELD)); } } } } @Override public List<Tag> getTagCloud(CoreSession session, String docId, String username, Boolean normalize) { UnrestrictedGetDocumentCloud r = new UnrestrictedGetDocumentCloud(session, docId, username, normalize); r.runUnrestricted(); return r.cloud; } protected static class UnrestrictedGetDocumentCloud extends UnrestrictedSessionRunner { protected final String docId; protected final String username; protected final List<Tag> cloud; protected final Boolean normalize; protected UnrestrictedGetDocumentCloud(CoreSession session, String docId, String username, Boolean normalize) { super(session); this.docId = docId; this.username = cleanUsername(username); this.normalize = normalize; this.cloud = new ArrayList<>(); } @Override public void run() { List<Map<String, Serializable>> res; if (docId == null) { if (username == null) { res = getItems(PAGE_PROVIDERS.GET_ALL_TAGS.name(), session); } else { res = getItems(PAGE_PROVIDERS.GET_ALL_TAGS_FOR_USER.name(), session, username); } } else { // find all docs under docid String path = session.getDocument(new IdRef(docId)).getPathAsString(); path = path.replace("'", ""); List<String> docIds = new ArrayList<>(); docIds.add(docId); List<Map<String, Serializable>> docRes = getItems(PAGE_PROVIDERS.GET_TAGGED_DOCUMENTS_UNDER.name(), session, path); if (docRes != null) { for (Map<String, Serializable> map : docRes) { docIds.add((String) map.get(NXQL.ECM_UUID)); } } if (username == null) { res = getItems(PAGE_PROVIDERS.GET_TAGS_FOR_DOCUMENTS.name(), session, docIds); } else { res = getItems(PAGE_PROVIDERS.GET_TAGS_FOR_DOCUMENTS_AND_USER.name(), session, docIds, username); } } int min = 999999, max = 0; if (res != null) { for (Map<String, Serializable> map : res) { String label = (String) map.get(TagConstants.TAG_LABEL_FIELD); int weight = ((Long) map.get(TagConstants.TAGGING_SOURCE_FIELD)).intValue(); if (weight == 0) { // shouldn't happen continue; } if (weight > max) { max = weight; } if (weight < min) { min = weight; } Tag weightedTag = new Tag(label, weight); cloud.add(weightedTag); } } if (normalize != null) { normalizeCloud(cloud, min, max, !normalize.booleanValue()); } } } public static void normalizeCloud(List<Tag> cloud, int min, int max, boolean linear) { if (min == max) { for (Tag tag : cloud) { tag.setWeight(100); } return; } double nmin; double diff; if (linear) { nmin = min; diff = max - min; } else { nmin = Math.log(min); diff = Math.log(max) - nmin; } for (Tag tag : cloud) { long weight = tag.getWeight(); double norm; if (linear) { norm = (weight - nmin) / diff; } else { norm = (Math.log(weight) - nmin) / diff; } tag.setWeight(Math.round(100 * norm)); } } @Override public List<Tag> getSuggestions(CoreSession session, String label, String username) { UnrestrictedGetTagSuggestions r = new UnrestrictedGetTagSuggestions(session, label, username); r.runUnrestricted(); return r.tags; } protected static class UnrestrictedGetTagSuggestions extends UnrestrictedSessionRunner { protected final String label; protected final String username; protected final List<Tag> tags; protected UnrestrictedGetTagSuggestions(CoreSession session, String label, String username) { super(session); label = cleanLabel(label, false, true); if (!label.contains("%")) { label += "%"; } this.label = label; this.username = cleanUsername(username); this.tags = new ArrayList<>(); } @Override public void run() { List<Map<String, Serializable>> res; if (username == null) { res = getItems(PAGE_PROVIDERS.GET_TAG_SUGGESTIONS.name(), session, label); } else { res = getItems(PAGE_PROVIDERS.GET_TAG_SUGGESTIONS_FOR_USER.name(), session, label, username); } if (res != null) { for (Map<String, Serializable> map : res) { String label = (String) map.get(TagConstants.TAG_LABEL_FIELD); tags.add(new Tag(label, 0)); } } // XXX should sort on tag weight Collections.sort(tags, Tag.LABEL_COMPARATOR); } } /** * Returns results from calls to {@link CoreSession#queryAndFetch(String, String, Object...)} using page providers. * * @since 6.0 */ @SuppressWarnings("unchecked") protected static List<Map<String, Serializable>> getItems(String pageProviderName, CoreSession session, Object... params) { PageProviderService ppService = Framework.getService(PageProviderService.class); if (ppService == null) { throw new RuntimeException("Missing PageProvider service"); } Map<String, Serializable> props = new HashMap<>(); // first retrieve potential props from definition PageProviderDefinition def = ppService.getPageProviderDefinition(pageProviderName); if (def != null) { Map<String, String> defProps = def.getProperties(); if (defProps != null) { props.putAll(defProps); } } props.put(CoreQueryAndFetchPageProvider.CORE_SESSION_PROPERTY, (Serializable) session); PageProvider<Map<String, Serializable>> pp = (PageProvider<Map<String, Serializable>>) ppService.getPageProvider( pageProviderName, null, null, null, props, params); if (pp == null) { throw new NuxeoException("Page provider not found: " + pageProviderName); } return pp.getCurrentPage(); } }