/*
* Weblounge: Web Content Management System
* Copyright (c) 2003 - 2011 The Weblounge Team
* http://entwinemedia.com/weblounge
*
* This program 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
* of the License, or (at your option) any later version.
*
* This program 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 program; if not, write to the Free Software Foundation
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package ch.entwine.weblounge.common.impl.content;
import ch.entwine.weblounge.common.impl.language.LanguageUtils;
import ch.entwine.weblounge.common.impl.language.LocalizableObject;
import ch.entwine.weblounge.common.impl.security.UserImpl;
import ch.entwine.weblounge.common.impl.util.WebloungeDateFormat;
import ch.entwine.weblounge.common.impl.util.config.ConfigurationUtils;
import ch.entwine.weblounge.common.impl.util.xml.XPathHelper;
import ch.entwine.weblounge.common.language.Language;
import ch.entwine.weblounge.common.language.Localizable;
import ch.entwine.weblounge.common.security.User;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.text.ParseException;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathFactory;
/**
* The localized modification context contains information about when an object
* was modified and who the modifier was in a given language. It can be used by
* <code>LocalizedModifiable</code> objects as the backing implementation.
* <p>
* The context adds additional means of specifying and querying modifier and
* modification date. It also allows for easy serialization and deserialization
* of <code>LocalizedModifiable</code> data.
* <p>
* Following is an example of the data structure that the modification context
* is able to handle:
*
* <pre>
* <content>
* <locale language="de" original="true">
* <modified>
* <user id="hans" realm="testland">Hans Muster</user>
* <date>2009/01/07 19:05:41 GMT</date>
* </modified>
* </locale>
* <locale language="fr">
* <modified>
* <user id="amelie" realm="testland">Amélie Poulard</user>
* <date>2009/02/18 21:06:40 GMT</date>
* </modified>
* </locale>
* </content>
* </pre>
*
* @see ch.entwine.weblounge.common.content.LocalizedModifiable
*/
public class LocalizedModificationContext extends LocalizableObject implements Cloneable {
/** The last modifier */
protected transient User lastModifier = null;
/** The last modification date */
protected transient Date lastModification = null;
/** The earliest modification date */
protected transient Date earliestModification = null;
/** Localized publishing information */
protected Map<Language, Modification> modifications = null;
/**
* Creates a new and empty localized modification context.
*/
public LocalizedModificationContext() {
this.modifications = new HashMap<Language, Modification>();
}
/**
* Returns the modification date in the current language. If the current
* language is undefined, then the last modification date is returned.
* <p>
* If no modification information is present for any language, this method
* returns <code>null</code>.
*
* @return the modification date in the current language
* @see #switchTo(Language)
* @see #switchTo(Language, boolean)
*/
public Date getModificationDate() {
Language currentLanguage = getLanguage();
Date date = null;
if (currentLanguage != null) {
Modification c = modifications.get(currentLanguage);
if (c != null)
date = c.getDate();
}
return date != null ? date : getLastModificationDate();
}
/**
* Returns the modification date in the specified language or
* <code>null</code> if no modification information is present in that
* language.
*
* @param language
* the language
* @return the modification time
*/
public Date getModificationDate(Language language) {
Modification modification = modifications.get(language);
return (modification != null) ? modification.getDate() : null;
}
/**
* Returns the user that modified the object in the current language. If the
* current language is undefined, then the user is returned that last modified
* the object in any language.
* <p>
* If no modification information is present for any language, this method
* returns <code>null</code>.
*
* @return the modifier in the current language
* @see #switchTo(Language)
* @see #switchTo(Language, boolean)
*/
public User getModifier() {
Language currentLanguage = getLanguage();
User modifier = null;
if (currentLanguage != null) {
Modification c = modifications.get(currentLanguage);
if (c != null)
modifier = c.getUser();
}
return modifier != null ? modifier : getLastModifier();
}
/**
* Returns the user that modified the object in the specified language or
* <code>null</code> if no modification in that language has occurred.
*
* @param language
* the language
* @return the modifier
*/
public User getModifier(Language language) {
Modification modification = modifications.get(language);
return (modification != null) ? modification.getUser() : null;
}
/**
* Returns <code>true</code> if the object is modified in the current language
* prior to <code>now</code> (or <code>new Date()</code>, for that matter). If
* the current language is undefined, then <code>true</code> is returned if
* there was any modification prior to <code>now</code>.
* <p>
* If no modification information is present for any language, this method
* returns <code>false</code>.
*
* @return <code>true</code> if the object was modified
* @see #switchTo(Language)
* @see #switchTo(Language, boolean)
*/
public boolean isModified() {
Language currentLanguage = getLanguage();
Date d = null;
if (currentLanguage != null) {
Modification c = modifications.get(currentLanguage);
if (c != null)
d = c.getDate();
}
if (d == null)
d = getLastModificationDate();
return d != null && new Date().after(d);
}
/**
* Returns <code>true</code> if the object is modified in the current language
* after <code>date</code>. If the current language is undefined, then
* <code>true</code> is returned if there was any modification after
* <code>date</code>.
* <p>
* If no modification information is present for any language, this method
* returns <code>false</code>.
*
* @return <code>true</code> if the object was modified after
* <code>date</code>
* @see #switchTo(Language)
* @see #switchTo(Language, boolean)
*/
public boolean isModifiedAfter(Date date) {
Language currentLanguage = getLanguage();
Date d = null;
if (currentLanguage != null) {
Modification c = modifications.get(currentLanguage);
if (c != null)
d = c.getDate();
}
if (d == null)
d = getLastModificationDate();
return d != null && d.after(date);
}
/**
* Returns <code>true</code> if the object is modified in the specified
* language after <code>date</code>.
* <p>
* If no modification information is present for that language, this method
* returns <code>false</code>.
*
* @return <code>true</code> if the object was modified after
* <code>date</code>
*/
public boolean isModifiedAfter(Date date, Language language) {
Modification modification = modifications.get(language);
return modification != null && modification.getDate().after(date);
}
/**
* Returns <code>true</code> if the object is modified in the current language
* before <code>date</code>. If the current language is undefined, then
* <code>true</code> is returned if there was any modification before
* <code>date</code>.
* <p>
* If no modification information is present for any language, this method
* returns <code>false</code>.
*
* @return <code>true</code> if the object was modified before
* <code>date</code>
* @see #switchTo(Language)
* @see #switchTo(Language, boolean)
*/
public boolean isModifiedBefore(Date date) {
Language currentLanguage = getLanguage();
Date d = null;
if (currentLanguage != null) {
Modification c = modifications.get(currentLanguage);
if (c != null)
d = c.getDate();
}
if (d == null)
d = getLastModificationDate();
return d != null && d.before(date);
}
/**
* Returns <code>true</code> if the object is modified in the specified
* language before <code>date</code>.
* <p>
* If no modification information is present for that language, this method
* returns <code>false</code>.
*
* @return <code>true</code> if the object was modified before
* <code>date</code>
*/
public boolean isModifiedBefore(Date date, Language language) {
Modification modification = modifications.get(language);
return modification != null && modification.getDate().before(date);
}
/**
* Sets the user that last modified the object in the given language as well
* as the modification date.
*
* @param user
* the user that modified the object
* @param date
* the date of modification
* @param language
* the language version that was modified
*/
public void setModified(User user, Date date, Language language) {
Modification modification = modifications.get(language);
if (modification == null) {
modification = new Modification();
modifications.put(language, modification);
}
modification.setUser(user);
modification.setDate(date);
// These might no longer be valid
lastModifier = null;
lastModification = null;
}
/**
* Returns the date when this object was last modified in any language.
*
* @return the last modification date, regardless of the language
*/
public Date getLastModificationDate() {
if (lastModification != null)
return lastModification;
// First time calculation
Date date = null;
for (Modification m : modifications.values()) {
if (date == null || m.getDate().after(date))
date = m.getDate();
}
// Store for later reference
lastModification = date;
return date;
}
/**
* Returns the date when this object was first modified in any language.
*
* @return the first modification date, regardless of the language
*/
public Date getEarliestModificationDate() {
if (earliestModification != null)
return earliestModification;
// First time calculation
Date date = null;
for (Modification m : modifications.values()) {
if (date == null || m.getDate().before(date))
date = m.getDate();
}
// Store for later reference
earliestModification = date;
return date;
}
/**
* Returns the user that last modified the object, regardless of the language.
*
* @return the last modifier
*/
public User getLastModifier() {
if (lastModifier != null)
return lastModifier;
// First time calculation
Date date = null;
User user = null;
for (Modification m : modifications.values()) {
if (date == null || m.getDate().after(date)) {
date = m.getDate();
user = m.getUser();
}
}
// Store for later reference
lastModification = date;
lastModifier = user;
return user;
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.impl.language.LocalizableObject#compareTo(ch.entwine.weblounge.common.language.Localizable,
* ch.entwine.weblounge.common.language.Language)
*/
public int compareTo(Localizable o, Language l) {
if (o instanceof LocalizedModificationContext) {
return getLastModificationDate().compareTo(getLastModificationDate());
} else if (o instanceof ModificationContext) {
return getModificationDate().compareTo(((ModificationContext) o).getModificationDate());
}
return 0;
}
/**
* Returns a copy of this context.
*
* @see java.lang.Object#clone()
*/
public Object clone() throws CloneNotSupportedException {
LocalizedModificationContext ctxt = (LocalizedModificationContext) super.clone();
ctxt.behavior = behavior;
ctxt.currentLanguage = currentLanguage;
ctxt.defaultLanguage = defaultLanguage;
ctxt.originalLanguage = originalLanguage;
ctxt.languages = new HashSet<Language>();
ctxt.languages.addAll(languages);
ctxt.modifications = new HashMap<Language, Modification>();
for (Map.Entry<Language, Modification> entry : modifications.entrySet()) {
ctxt.modifications.put(entry.getKey(), entry.getValue());
}
return ctxt;
}
/**
* Initializes this context from an XML node that was generated using
* {@link #toXml()}.
* <p>
* To speed things up, you might consider using the second signature that uses
* an existing <code>XPath</code> instance instead of creating a new one.
*
* @param context
* the localized modification context node
* @throws IllegalStateException
* if the context cannot be parsed
* @see #fromXml(Node, XPath)
* @see #toXml()
*/
public static LocalizedModificationContext fromXml(Node context)
throws IllegalArgumentException {
XPath xpath = XPathFactory.newInstance().newXPath();
return fromXml(context, xpath);
}
/**
* Initializes this context from an XML node that was generated using
* {@link #toXml()}.
*
* @param context
* the publish context node
* @param xpath
* the xpath processor
* @throws IllegalStateException
* if the context cannot be parsed
* @see #toXml()
*/
public static LocalizedModificationContext fromXml(Node context, XPath xpath)
throws IllegalStateException {
NodeList locales = XPathHelper.selectList(context, "locale", xpath);
if (locales == null)
return null;
LocalizedModificationContext ctx = new LocalizedModificationContext();
for (int i = 0; i < locales.getLength(); i++) {
xpath.reset();
Node locale = locales.item(i);
Node languageNode = locale.getAttributes().getNamedItem("language");
Node originalNode = locale.getAttributes().getNamedItem("original");
boolean original = originalNode != null && ConfigurationUtils.isTrue(originalNode.getNodeValue());
if (languageNode == null)
throw new IllegalStateException("Found locale without language");
Language language = LanguageUtils.getLanguage(languageNode.getNodeValue());
// Modifying user
Node modifierNode = XPathHelper.select(locale, "modified/user", xpath);
if (modifierNode == null)
throw new IllegalStateException("Modifier cannot be null");
User modifier = UserImpl.fromXml(modifierNode, xpath);
// Modification date
String date = XPathHelper.valueOf(locale, "modified/date", xpath);
if (date == null)
throw new IllegalStateException("Date cannot be null");
Date modificationDate = null;
try {
modificationDate = WebloungeDateFormat.parseStatic(date);
} catch (ParseException e) {
throw new IllegalStateException("The modification date '" + date + "' cannot be parsed", e);
}
ctx.setModified(modifier, modificationDate, language);
if (original)
ctx.setOriginalLanguage(language);
}
return ctx;
}
/**
* Returns an <code>XML</code> representation of the context, that will look
* similar to the following example:
*
* <pre>
* <content>
* <locale language="de" original="true">
* <modified>
* <user id="hans" realm="testland">Hans Muster</user>
* <date>2009/01/07 19:05:41 GMT</date>
* </modified>
* </locale>
* <locale language="fr">
* <modified>
* <user id="amelie" realm="testland">Amélie Poulard</user>
* <date>2009/02/18 21:06:40 GMT</date>
* </modified>
* </locale>
* </content>
* </pre>
*
* Use {@link #fromXml(Node))} or {@link #fromXml(Node, XPath)} to create a
* <code>LocalizedModificationContext</code> from the serialized output of
* this method.
*
* @return the <code>XML</code> representation of the context
* @see #fromXml(Node)
* @see #fromXml(Node, XPath)
*/
public String toXml(Language language) {
Modification modification = modifications.get(language);
StringBuffer b = new StringBuffer();
if (modification != null) {
b.append("<modified>");
b.append((new UserImpl(modification.getUser())).toXml());
b.append("<date>");
b.append(WebloungeDateFormat.formatStatic(modification.getDate()));
b.append("</date>");
b.append("</modified>");
}
return b.toString();
}
/**
* Helper class used to hold modification information.
*/
private static class Modification {
/** Modifier */
private User modifier = null;
/** Date of modification */
private Date modificationDate = null;
/**
* Creates a new modification.
*/
Modification() {
}
/**
* Sets the modifier.
*
* @param modifier
* the modifying user
*/
void setUser(User modifier) {
this.modifier = modifier;
}
/**
* Returns the user that modified the item.
*
* @return the modifier
*/
User getUser() {
return modifier;
}
/**
* Sets the modification date.
*
* @param date
* the modification date
*/
void setDate(Date date) {
modificationDate = date;
}
/**
* Returns the modification date.
*
* @return the date
*/
Date getDate() {
return modificationDate;
}
}
}