/* * See the NOTICE file distributed with this work for additional * information regarding copyright ownership. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package com.xpn.xwiki.plugin.tag; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.regex.Pattern; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.xpn.xwiki.XWikiContext; import com.xpn.xwiki.XWikiException; import com.xpn.xwiki.api.Api; import com.xpn.xwiki.doc.XWikiDocument; import com.xpn.xwiki.objects.BaseObject; import com.xpn.xwiki.objects.BaseProperty; import com.xpn.xwiki.objects.DBStringListProperty; import com.xpn.xwiki.objects.classes.BaseClass; import com.xpn.xwiki.objects.classes.PropertyClass; import com.xpn.xwiki.plugin.XWikiDefaultPlugin; import com.xpn.xwiki.plugin.XWikiPluginInterface; /** * TagPlugin is a plugin that allows to manipulate tags easily. It allows to get, rename and delete tags. * * @version $Id: 736a8e95b881b2d3334812e1552943aad80a6fca $ */ public class TagPlugin extends XWikiDefaultPlugin implements XWikiPluginInterface { /** Logging helper object. */ public static final Logger LOGGER = LoggerFactory.getLogger(TagPlugin.class); /** * The identifier for this plugin; used for accessing the plugin from velocity, and as the action returning the * extension content. */ public static final String PLUGIN_NAME = "tag"; /** * XWiki class defining tags. */ public static final String TAG_CLASS = "XWiki.TagClass"; /** * XWiki property of XWiki.TagClass storing tags. */ public static final String TAG_PROPERTY = "tags"; /** * L10N key for the "tag added" document edit comment. */ public static final String DOC_COMMENT_TAG_ADDED = "plugin.tag.editcomment.added"; private static final Pattern LIKE_ESCAPE = Pattern.compile("[_%\\\\]"); private static final String LIKE_REPLACEMENT = "\\\\$0"; private static final String LIKE_APPEND = ".%"; /** * Tag plugin constructor. * * @param name The name of the plugin, which can be used for retrieving the plugin API from velocity. Unused. * @param className The canonical classname of the plugin. Unused. * @param context The current request context. * @see com.xpn.xwiki.plugin.XWikiDefaultPlugin#XWikiDefaultPlugin(String,String,com.xpn.xwiki.XWikiContext) */ public TagPlugin(String name, String className, XWikiContext context) { super(PLUGIN_NAME, className, context); } @Override public Api getPluginApi(XWikiPluginInterface plugin, XWikiContext context) { return new TagPluginApi((TagPlugin) plugin, context); } /** * Get tags of the given document. * * @param document document to search in. * @return list of tags. The list is a snapshot of the current tags. Changes to this list won't affect the document, * and changes to the document's tags won't be visible in the returned list. */ @SuppressWarnings("unchecked") private List<String> getTagsFromDocument(XWikiDocument document) { try { BaseProperty prop = (BaseProperty) document.getObject(TAG_CLASS).safeget(TAG_PROPERTY); return new ArrayList<String>((List<String>) prop.getValue()); } catch (NullPointerException ex) { return new ArrayList<String>(); } } /** * Set tags of the given document. * * @param document document to put the tags to. * @param tags list of tags. * @param context XWiki context. */ private void setDocumentTags(XWikiDocument document, List<String> tags, XWikiContext context) { BaseProperty prop = (BaseProperty) document.getObject(TAG_CLASS, true, context).safeget(TAG_PROPERTY); // Properties aren't added to an object unless a value is specified either from the Web or from an XML. if (prop == null) { prop = createTagProperty(document.getObject(TAG_CLASS, true, context), context); } prop.setValue(tags); } /** * Create and add the main tag property to the provided tag object. The new property corresponds to the definition * in the tag class, but in case of an error, the default type is a relational-stored list. * * @param tagObject the target tag object * @param context the current request context * @return the created property * @see #TAG_PROPERTY */ private BaseProperty createTagProperty(BaseObject tagObject, XWikiContext context) { BaseProperty tagProperty; try { BaseClass tagClass = context.getWiki().getClass(TAG_CLASS, context); PropertyClass tagPropertyDefinition = (PropertyClass) tagClass.getField(TAG_PROPERTY); tagProperty = tagPropertyDefinition.newProperty(); } catch (XWikiException ex) { LOGGER.warn("Failed to properly create tag property for the tag object, creating a default one"); tagProperty = new DBStringListProperty(); } tagProperty.setName(TAG_PROPERTY); tagProperty.setObject(tagObject); tagObject.safeput(TAG_PROPERTY, tagProperty); return tagProperty; } /** * Get all tags within the wiki. * * @param context XWiki context. * @return list of tags (alphabetical order). * @throws XWikiException if search query fails (possible failures: DB access problems, etc). */ public List<String> getAllTags(XWikiContext context) throws XWikiException { return TagQueryUtils.getAllTags(context); } /** * Get cardinality map of tags within the wiki. * * @param context XWiki context. * @return map of tags (alphabetical order) with their occurences counts. * @throws XWikiException if search query fails (possible failures: DB access problems, etc). */ public Map<String, Integer> getTagCount(XWikiContext context) throws XWikiException { return this.getTagCountForQuery(null, null, context); } /** * Get cardinality map of tags for a specific wiki space (including sub spaces). * * @param spaceReference the local reference of the space to get tags from. If blank, return tags for the whole * wiki. * @param context XWiki context. * @return map of tags (alphabetical order) with their occurrences counts. * @throws XWikiException if search query fails (possible failures: DB access problems, etc). * @since 1.2 */ public Map<String, Integer> getTagCount(String spaceReference, XWikiContext context) throws XWikiException { if (!StringUtils.isBlank(spaceReference)) { StringBuilder where = new StringBuilder(); where.append('('); where.append("doc.space = ?"); where.append(" OR "); where.append("doc.space LIKE ?"); where.append(')'); // Make sure to escape the LIKE syntax String escapedSpaceReference = LIKE_ESCAPE.matcher(spaceReference).replaceAll(LIKE_REPLACEMENT); return getTagCountForQuery("", where.toString(), Arrays.asList(spaceReference, escapedSpaceReference + LIKE_APPEND), context); } return getTagCount(context); } /** * Get cardinality map of tags for a list of wiki spaces (including sub spaces). * For example "'Main','Sandbox'" for all tags in the "Main" and "Sandbox" spaces, * or "'Apo''stroph'" for all tags in the space "Apo'stroph". * * @param spaces the list of space to get tags in, as a comma separated, quoted space references strings. * @param context XWiki context. * @return map of tags with their occurences counts * @throws XWikiException if search query fails (possible failures: space list parse error, DB problems, etc). * @since 8.2M1 */ public Map<String, Integer> getTagCountForSpaces(String spaces, XWikiContext context) throws XWikiException { List<String> spaceRefList = TagParamUtils.spacesParameterToList(spaces); List<Object> queryParameter = new ArrayList<>(); StringBuilder where = new StringBuilder(); boolean first = true; for (String spaceReference : spaceRefList) { if (first) { where.append("(doc.space = ? "); first = false; } else { where.append(" OR doc.space = ? "); } queryParameter.add(spaceReference); where.append("OR doc.space LIKE ?"); String escapedSpaceReference = LIKE_ESCAPE.matcher(spaceReference).replaceAll(LIKE_REPLACEMENT); queryParameter.add(escapedSpaceReference + LIKE_APPEND); } // if first is true the "for" loop never ran, and spaces is empty // so only close brace if first is false if (!first) { where.append(')'); } return getTagCountForQuery("", where.toString(), queryParameter, context); } /** * Get cardinality map of tags matching a hql query. * * @param fromHql the <code>from</code> fragment of the hql query * @param whereHql the <code>where</code> fragment of the hql query * @param context XWiki context. * @return map of tags (alphabetical order) with their occurrences counts. * @throws XWikiException if search query fails (possible failures: DB access problems, etc). * @since 1.2 * @see TagPluginApi#getTagCountForQuery(String, String) */ public Map<String, Integer> getTagCountForQuery(String fromHql, String whereHql, XWikiContext context) throws XWikiException { return getTagCountForQuery(fromHql, whereHql, null, context); } /** * Get cardinality map of tags matching a parameterized hql query. * * @param fromHql the <code>from</code> fragment of the hql query * @param whereHql the <code>where</code> fragment of the hql query * @param parameterValues list of parameter values for the query * @param context XWiki context. * @return map of tags (alphabetical order) with their occurrences counts. * @throws XWikiException if search query fails (possible failures: DB access problems, etc). * @since 1.18 * @see TagPluginApi#getTagCountForQuery(String, String, java.util.List) */ public Map<String, Integer> getTagCountForQuery(String fromHql, String whereHql, List<?> parameterValues, XWikiContext context) throws XWikiException { return TagQueryUtils.getTagCountForQuery(fromHql, whereHql, parameterValues, context); } /** * Get non-hidden documents with the given tags. * * @param tag a list of tags to match. * @param context XWiki context. * @return list of docNames. * @throws XWikiException if search query fails (possible failures: DB access problems, etc). */ public List<String> getDocumentsWithTag(String tag, XWikiContext context) throws XWikiException { return TagQueryUtils.getDocumentsWithTag(tag, context); } /** * Get documents with the given tags. * * @param tag a list of tags to match. * @param includeHiddenDocuments if true then also include hidden documents * @param context XWiki context. * @return list of docNames. * @throws XWikiException if search query fails (possible failures: DB access problems, etc). * @since 6.2M1 */ public List<String> getDocumentsWithTag(String tag, boolean includeHiddenDocuments, XWikiContext context) throws XWikiException { return TagQueryUtils.getDocumentsWithTag(tag, includeHiddenDocuments, context); } /** * Get tags from a document. * * @param documentName name of the document. * @param context XWiki context. * @return list of tags. * @throws XWikiException if document read fails (possible failures: insufficient rights, DB access problems, etc). */ public List<String> getTagsFromDocument(String documentName, XWikiContext context) throws XWikiException { return getTagsFromDocument(context.getWiki().getDocument(documentName, context)); } /** * Get tags from a document. * * @param document the document. * @param context XWiki context. * @return list of tags. * @throws XWikiException if document read fails (possible failures: insufficient rights, DB access problems, etc). */ public List<String> getTagsFromDocument(XWikiDocument document, XWikiContext context) throws XWikiException { return getTagsFromDocument(document); } /** * Add a tag to a document. The document is saved (minor edit) after this operation. * * @param tag tag to set. * @param documentName name of the document. * @param context XWiki context. * @return the {@link TagOperationResult result} of the operation * @throws XWikiException if document save fails (possible failures: insufficient rights, DB access problems, etc). */ public TagOperationResult addTagToDocument(String tag, String documentName, XWikiContext context) throws XWikiException { return addTagToDocument(tag, context.getWiki().getDocument(documentName, context), context); } /** * Add a tag to a document. The document is saved (minor edit) after this operation. * * @param tag tag to set. * @param document the document. * @param context XWiki context. * @return the {@link TagOperationResult result} of the operation * @throws XWikiException if document save fails (possible failures: insufficient rights, DB access problems, etc). */ public TagOperationResult addTagToDocument(String tag, XWikiDocument document, XWikiContext context) throws XWikiException { List<String> tags = getTagsFromDocument(document); if (!StringUtils.isBlank(tag) && !tags.contains(tag)) { tags.add(tag); setDocumentTags(document, tags, context); String comment = localizePlainOrKey(DOC_COMMENT_TAG_ADDED, tag); // Since we're changing the document we need to set the new author document.setAuthorReference(context.getUserReference()); context.getWiki().saveDocument(document, comment, true, context); return TagOperationResult.OK; } return TagOperationResult.NO_EFFECT; } /** * Add a list of tags to a document. The document is saved (minor edit) after this operation. * * @param tags the comma separated list of tags to set; whitespace around the tags is stripped * @param documentName the name of the target document * @param context the current request context. * @return the {@link TagOperationResult result} of the operation. {@link TagOperationResult#NO_EFFECT} is returned * only if all the tags were already set on the document, {@link TagOperationResult#OK} is returned even if * only some of the tags are new. * @throws XWikiException if document save fails (possible failures: insufficient rights, DB access problems, etc). */ public TagOperationResult addTagsToDocument(String tags, String documentName, XWikiContext context) throws XWikiException { return addTagsToDocument(tags, context.getWiki().getDocument(documentName, context), context); } /** * Add a list of tags to a document. The document is saved (minor edit) after this operation. * * @param tags the comma separated list of tags to set; whitespace around the tags is stripped * @param document the target document * @param context the current request context * @return the {@link TagOperationResult result} of the operation. {@link TagOperationResult#NO_EFFECT} is returned * only if all the tags were already set on the document, {@link TagOperationResult#OK} is returned even if * only some of the tags are new. * @throws XWikiException if document save fails (possible failures: insufficient rights, DB access problems, etc). */ public TagOperationResult addTagsToDocument(String tags, XWikiDocument document, XWikiContext context) throws XWikiException { List<String> documentTags = getTagsFromDocument(document); String[] newTags = tags.trim().split("\\s*+,\\s*+"); boolean added = false; for (String tag : newTags) { if (!StringUtils.isBlank(tag) && !containsIgnoreCase(documentTags, tag)) { documentTags.add(tag); added = true; } } if (added) { setDocumentTags(document, documentTags, context); String comment = localizePlainOrKey(DOC_COMMENT_TAG_ADDED, tags); // Since we're changing the document we need to set the new author document.setAuthorReference(context.getUserReference()); context.getWiki().saveDocument(document, comment, true, context); return TagOperationResult.OK; } return TagOperationResult.NO_EFFECT; } /** * @param collection a collection of strings * @param item a string * @return {@code true} if there is an item in the given collection that equals ignoring case the given string */ private boolean containsIgnoreCase(Collection<String> collection, String item) { for (String existingItem : collection) { if (existingItem.equalsIgnoreCase(item)) { return true; } } return false; } /** * Remove a tag from a document. The document is saved (minor edit) after this operation. * * @param tag tag to remove. * @param documentName name of the document. * @param context XWiki context. * @return the {@link TagOperationResult result} of the operation * @throws XWikiException if document save fails for some reason (Insufficient rights, DB access, etc). */ public TagOperationResult removeTagFromDocument(String tag, String documentName, XWikiContext context) throws XWikiException { return removeTagFromDocument(tag, context.getWiki().getDocument(documentName, context), context); } /** * Remove a tag from a document. The document is saved (minor edit) after this operation. * * @param tag tag to remove. * @param document the document. * @param context XWiki context. * @return the {@link TagOperationResult result} of the operation * @throws XWikiException if document save fails for some reason (Insufficient rights, DB access, etc). */ public TagOperationResult removeTagFromDocument(String tag, XWikiDocument document, XWikiContext context) throws XWikiException { List<String> tags = getTagsFromDocument(document); boolean needsUpdate = false; ListIterator<String> it = tags.listIterator(); while (it.hasNext()) { if (tag.equalsIgnoreCase(it.next())) { needsUpdate = true; it.remove(); } } if (needsUpdate) { setDocumentTags(document, tags, context); String comment = localizePlainOrKey("plugin.tag.editcomment.removed", tag); // Since we're changing the document we need to set the new author document.setAuthorReference(context.getUserReference()); context.getWiki().saveDocument(document, comment, true, context); return TagOperationResult.OK; } else { // Document doesn't contain this tag. return TagOperationResult.NO_EFFECT; } } /** * Rename a tag. * * @param tag tag to rename. * @param newTag new tag. * @param context XWiki context. * @return the {@link TagOperationResult result} of the operation * @throws XWikiException if document save fails for some reason (Insufficient rights, DB access, etc). */ protected TagOperationResult renameTag(String tag, String newTag, XWikiContext context) throws XWikiException { // Since we're renaming a tag, we want to rename it even if the document is hidden. A hidden document is still // accessible to users, it's just not visible for simple users; it doesn't change permissions. List<String> docNamesToProcess = getDocumentsWithTag(tag, true, context); if (StringUtils.equals(tag, newTag) || docNamesToProcess.size() == 0 || StringUtils.isBlank(newTag)) { return TagOperationResult.NO_EFFECT; } String comment = localizePlainOrKey("plugin.tag.editcomment.renamed", tag, newTag); for (String docName : docNamesToProcess) { XWikiDocument doc = context.getWiki().getDocument(docName, context); List<String> tags = getTagsFromDocument(doc); if (tags.contains(newTag)) { // The new tag might already be present in the document, in this case we just need to remove the old one removeTagFromDocument(tag, doc.getFullName(), context); } else { for (int i = 0; i < tags.size(); i++) { if (tags.get(i).equalsIgnoreCase(tag)) { tags.set(i, newTag); } } setDocumentTags(doc, tags, context); // Since we're changing the document we need to set the new author doc.setAuthorReference(context.getUserReference()); context.getWiki().saveDocument(doc, comment, true, context); } } return TagOperationResult.OK; } /** * Delete a tag. * * @param tag tag to delete. * @param context XWiki context. * @return the {@link TagOperationResult result} of the operation * @throws XWikiException if document save fails for some reason (Insufficient rights, DB access, etc). */ protected TagOperationResult deleteTag(String tag, XWikiContext context) throws XWikiException { // Since we're deleting a tag, we want to delete it even if the document is hidden. A hidden document is still // accessible to users, it's just not visible for simple users; it doesn't change permissions. List<String> docsToProcess = getDocumentsWithTag(tag, true, context); if (docsToProcess.size() == 0) { return TagOperationResult.NO_EFFECT; } for (String docName : docsToProcess) { removeTagFromDocument(tag, docName, context); } return TagOperationResult.OK; } }