/*
* 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 org.xwiki.watchlist.internal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Provider;
import javax.inject.Singleton;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.xwiki.component.annotation.Component;
import org.xwiki.localization.ContextualLocalizationManager;
import org.xwiki.watchlist.internal.api.AutomaticWatchMode;
import org.xwiki.watchlist.internal.api.WatchListStore;
import org.xwiki.watchlist.internal.api.WatchedElementType;
import org.xwiki.watchlist.internal.documents.WatchListClassDocumentInitializer;
import com.xpn.xwiki.XWikiContext;
import com.xpn.xwiki.XWikiException;
import com.xpn.xwiki.doc.XWikiDocument;
import com.xpn.xwiki.objects.BaseObject;
/**
* WatchList store class. Handles user subscription storage.
*
* @version $Id: 01e7f81e4f408002aa63cde25e44b3a0b08a2ac3 $
*/
@Component
@Singleton
public class DefaultWatchListStore implements WatchListStore
{
/**
* Character used to separated wiki and space in XWiki model.
*/
public static final String WIKI_SPACE_SEP = ":";
/**
* Character used to separated space and page in XWiki model.
*/
public static final String SPACE_PAGE_SEP = ".";
/**
* Character used to separated values in XProperties lists.
*/
public static final String PIPE_SEP = "|";
/**
* XWiki Class used to store user.
*/
public static final String USERS_CLASS = "XWiki.XWikiUsers";
/**
* Logging helper object.
*/
@Inject
private Logger logger;
/**
* Context provider.
*/
@Inject
private Provider<XWikiContext> contextProvider;
/**
* Used to read cached notification related information.
*/
@Inject
private Provider<WatchListNotificationCache> notificationCache;
/**
* Used to resolve translations.
*/
@Inject
private ContextualLocalizationManager localization;
@Override
public Collection<String> getWatchedElements(String user, WatchedElementType type) throws XWikiException
{
BaseObject watchListObject = getWatchListObject(user);
List<String> watchedItems = watchListObject.getListValue(getWatchListClassPropertyForType(type));
return watchedItems;
}
/**
* Gets the WatchList XWiki Object from a user's profile's page.
*
* @param user the user to check
* @return the WatchList XWiki BaseObject
* @throws XWikiException if BaseObject creation fails or if user does not exists
*/
public BaseObject getWatchListObject(String user) throws XWikiException
{
XWikiContext context = contextProvider.get();
XWikiDocument userDocument = context.getWiki().getDocument(user, context);
if (userDocument.isNew() || userDocument.getObject(USERS_CLASS) == null) {
throw new XWikiException(XWikiException.MODULE_XWIKI_PLUGINS, XWikiException.ERROR_XWIKI_UNKNOWN, "User ["
+ user + "] does not exists");
}
BaseObject obj = userDocument.getObject(WatchListClassDocumentInitializer.DOCUMENT_FULL_NAME);
if (obj == null) {
obj = createWatchListObject(user, context);
}
return obj;
}
/**
* Creates a WatchList XWiki Object in the user's profile's page.
*
* @param user XWiki User
* @param context Context of the request
* @return the watchlist object that has been created
* @throws XWikiException if the document cannot be saved
*/
public BaseObject createWatchListObject(String user, XWikiContext context) throws XWikiException
{
XWikiDocument userDocument = context.getWiki().getDocument(user, context);
BaseObject object = userDocument.newObject(WatchListClassDocumentInitializer.DOCUMENT_FULL_NAME, context);
context.getWiki().saveDocument(userDocument, this.localization.getTranslationPlain("watchlist.create.object"),
true, context);
return object;
}
/**
* Get the name of the XClass property the given type is stored in.
*
* @param type type to retrieve
* @return the name of the XClass property
*/
private String getWatchListClassPropertyForType(WatchedElementType type)
{
String result = StringUtils.EMPTY;
switch (type) {
case WIKI:
result = WatchListClassDocumentInitializer.WIKIS_PROPERTY;
break;
case SPACE:
result = WatchListClassDocumentInitializer.SPACES_PROPERTY;
break;
case DOCUMENT:
result = WatchListClassDocumentInitializer.DOCUMENTS_PROPERTY;
break;
case USER:
result = WatchListClassDocumentInitializer.USERS_PROPERTY;
break;
default:
break;
}
return result;
}
@Override
public boolean isWatched(String element, String user, WatchedElementType type) throws XWikiException
{
// TODO: Can this be optimized by a direct "exists" query on the list item? Would it e better than what we
// currently have with the document cache? If we try a query, it would also need to be performed on the user's
// wiki/database, not the current one.
Collection<String> watchedElements = getWatchedElements(user, type);
if (WatchedElementType.SPACE.equals(type)) {
// Special handling for Nested Spaces
for (String watchedSpace : watchedElements) {
// Check if there is an exact match on the watched space or if the current space is nested inside a
// watched space.
String watchedSpacePrefix = String.format("%s.", watchedSpace);
if (element.equals(watchedSpace) || element.startsWith(watchedSpacePrefix)) {
return true;
}
}
return false;
} else {
return watchedElements.contains(element);
}
}
@Override
public boolean addWatchedElement(String user, String newWatchedElement, WatchedElementType type)
throws XWikiException
{
XWikiContext context = contextProvider.get();
String elementToWatch = newWatchedElement;
if (!WatchedElementType.WIKI.equals(type) && !newWatchedElement.contains(WIKI_SPACE_SEP)) {
elementToWatch = context.getWikiId() + WIKI_SPACE_SEP + newWatchedElement;
}
if (isWatched(elementToWatch, user, type)) {
return false;
}
// Copy the list of watched elements because it could be unmodifiable.
List<String> watchedElements = new ArrayList<String>(getWatchedElements(user, type));
watchedElements.add(elementToWatch);
setWatchListElementsProperty(user, type, watchedElements);
return true;
}
/**
* Sets a DBList property in the user's WatchList Object, then saves the user's profile.
*
* @param user the user whose watchlist to set
* @param type the element type as defined by {@link WatchedElementType}
* @param elements the elements to store
* @throws XWikiException if the user's profile cannot be saved
*/
private void setWatchListElementsProperty(String user, WatchedElementType type, Collection<String> elements)
throws XWikiException
{
XWikiContext context = contextProvider.get();
XWikiDocument userDocument = context.getWiki().getDocument(user, context);
List<String> elementsList = new ArrayList<String>(elements);
userDocument.setDBStringListValue(WatchListClassDocumentInitializer.DOCUMENT_FULL_NAME,
getWatchListClassPropertyForType(type), elementsList);
context.getWiki().saveDocument(userDocument, localization.getTranslationPlain("watchlist.save.object"), true,
context);
}
@Override
public boolean removeWatchedElement(String user, String watchedElement, WatchedElementType type)
throws XWikiException
{
XWikiContext context = contextProvider.get();
String elementToRemove = watchedElement;
if (!WatchedElementType.WIKI.equals(type) && !watchedElement.contains(WIKI_SPACE_SEP)) {
elementToRemove = context.getWikiId() + WIKI_SPACE_SEP + watchedElement;
}
if (!this.isWatched(elementToRemove, user, type)) {
return false;
}
Collection<String> watchedElements = getWatchedElements(user, type);
watchedElements.remove(elementToRemove);
this.setWatchListElementsProperty(user, type, watchedElements);
return true;
}
@Override
public AutomaticWatchMode getAutomaticWatchMode(String user)
{
XWikiContext context = contextProvider.get();
AutomaticWatchMode mode = null;
try {
BaseObject watchObject = getWatchListObject(user);
String value = watchObject.getStringValue(WatchListClassDocumentInitializer.AUTOMATICWATCH_PROPERTY);
if (StringUtils.isNotBlank(value)
&& !WatchListClassDocumentInitializer.AUTOMATICWATCH_DEFAULT_VALUE.equals(value)) {
mode = AutomaticWatchMode.valueOf(value);
}
} catch (Exception e) {
// Failed for some reason, now try getting it from xwiki.cfg
logger.error("Failed to get automatic watch mode for user [{}], using fallbacks", user, e);
}
if (mode == null) {
String value = context.getWiki().Param("xwiki.plugin.watchlist.automaticwatch");
if (value != null) {
try {
mode = AutomaticWatchMode.valueOf(value.toUpperCase());
} catch (Exception e) {
logger.warn("Invalid configuration in xwiki.plugin.watchlist.automaticwatch", e);
}
}
}
return mode != null ? mode : AutomaticWatchMode.MAJOR;
}
@Override
public List<String> getIntervals()
{
return notificationCache.get().getIntervals();
}
@Override
public String getInterval(String user)
{
String result = "";
try {
BaseObject watchObject = getWatchListObject(user);
result = watchObject.getStringValue(WatchListClassDocumentInitializer.INTERVAL_PROPERTY);
} catch (Exception e) {
// Failed for some reason, now try getting it from xwiki.cfg
logger.error("Failed to get notification interval for user [{}], using fallbacks", user, e);
}
return result;
}
@Override
public Collection<String> getSubscribers(String intervalId)
{
return notificationCache.get().getSubscribers(intervalId);
}
}