/*
* 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.url;
import ch.entwine.weblounge.common.content.Resource;
import ch.entwine.weblounge.common.language.Language;
import ch.entwine.weblounge.common.request.RequestFlavor;
import ch.entwine.weblounge.common.site.Site;
import ch.entwine.weblounge.common.url.Path;
import ch.entwine.weblounge.common.url.UrlUtils;
import ch.entwine.weblounge.common.url.WebUrl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* A web url represents a url that is used to address locations within the web
* application, such as HTML pages or module actions.
*/
public class WebUrlImpl extends UrlImpl implements WebUrl {
/** Serial version uid */
private static final long serialVersionUID = -5815146954734580746L;
/** The logging facility */
private static Logger logger = LoggerFactory.getLogger(WebUrlImpl.class);
/** Regular expression for /path/to/resource/work_de.html */
private static final Pattern pathInspector = Pattern.compile("^(.*)/(work|index|live|[0-9]*)(_[a-zA-Z]+)?\\.([a-zA-Z0-9]+)$");
/** Regular expression for /path/to/resource/de/html */
private static final Pattern segmentInspector = Pattern.compile("^(/([a-zA-Z0-9\\-\\,\\.\\:\\;\\(\\)/_~!\\$&\\*'\\+=@%^#^\\?])*+)+$");
/** The default request flavor */
private final RequestFlavor defaultFlavor = RequestFlavor.ANY;
/** The associated site */
protected Site site = null;
/** The url version */
protected long version = -1;
/** The language */
protected Language language = null;
/** True if the language is encoded on the path */
protected boolean languageIsPathEncoded = false;
/** The link */
private transient String link = null;
/** The url flavor */
protected RequestFlavor flavor = null;
/**
* Constructor for a url with the given path, a version of <code>LIVE</code>,
* a language matching the site default language and an <code>HTML</code>
* flavor, unless version, flavor and language are encoded in the url using
* either of these two schemes:
* <ul>
* <li>
* <code>path/to/resource/<version>_<language>.<flavor></code>
* </li>
* <li><code>path/to/resource/version/language/flavor</code></li>
* </ul>
*
* @param site
* the associated site
* @param path
* the url path
* @throws IllegalArgumentException
* if either one of <code>site</code> or <code>path</code> are
* <code>null</code> or the path is malformed
*/
public WebUrlImpl(Site site, String path) throws IllegalArgumentException {
super('/');
if (site == null)
throw new IllegalArgumentException("Site must not be null");
if (path == null)
throw new IllegalArgumentException("Path must not be null");
this.site = site;
this.path = analyzePath(path, '/');
version = Math.max(Resource.LIVE, version);
}
/**
* Constructor for a url with the given path, a version of <code>LIVE</code>
* and an <code>HTML</code> flavor, unless version, flavor or language are
* encoded in the url using either of these two schemes:
* <ul>
* <li>
* <code>path/to/resource/<version>_<language>.<flavor></code>
* </li>
* <li><code>path/to/resource/version/language/flavor</code></li>
* </ul>
*
* @param site
* the associated site
* @param url
* the url
* @throws IllegalArgumentException
* if either one of <code>site</code> or <code>path</code> are
* <code>null</code> or the path is malformed
*/
public WebUrlImpl(Site site, Path url) throws IllegalArgumentException {
this(site, url.getPath());
}
/**
* Constructor for a url with the given path added to <code>url</code>, a
* version of <code>LIVE</code> and an <code>HTML</code> flavor, unless
* version and/or flavor are encoded in the url using either of these two
* schemes:
* <ul>
* <li>
* <code>path/to/resource/<version>_<language>.<flavor></code>
* </li>
* <li><code>path/to/resource/version/language/flavor</code></li>
* </ul>
*
* @param site
* the associated site
* @param url
* the url
* @param path
* the path to append
* @throws IllegalArgumentException
* if either one of <code>site</code> or <code>path</code> are
* <code>null</code> or the path is malformed
*/
public WebUrlImpl(Site site, Path url, String path)
throws IllegalArgumentException {
this(site, concat(url.getPath(), path, '/'));
}
/**
* Constructor for a url with the given path and version and an
* <code>HTML</code> flavor, unless the flavor is encoded in the url using
* either of these two schemes:
* <ul>
* <li>
* <code>path/to/resource/<version>_<language>.<flavor></code>
* </li>
* <li><code>path/to/resource/version/language/flavor</code></li>
* </ul>
* <p>
* Note that even if the version is encoded in the url path, the one passed as
* the argument to this constructor will be used.
*
* @param site
* the associated site
* @param path
* the url path
* @param version
* the url version
* @throws IllegalArgumentException
* if either one of <code>site</code> or <code>path</code> are
* <code>null</code> or the path is malformed
*/
public WebUrlImpl(Site site, String path, long version)
throws IllegalArgumentException {
this(site, path);
this.version = version;
}
/**
* Constructor for a url with the given path, version and flavor.
*
* @param site
* the associated site
* @param path
* the url path
* @param version
* the required version
* @param flavor
* the url flavor
* @throws IllegalArgumentException
* if either one of <code>site</code> or <code>path</code> are
* <code>null</code> or the path is malformed
*/
public WebUrlImpl(Site site, String path, long version, RequestFlavor flavor)
throws IllegalArgumentException {
this(site, path, version, flavor, null);
}
/**
* Constructor for a url with the given path, version and flavor.
*
* @param site
* the associated site
* @param path
* the url path
* @param version
* the required version
* @param flavor
* the url flavor
* @param language
* the language
* @throws IllegalArgumentException
* if either one of <code>site</code> or <code>path</code> are
* <code>null</code> or the path is malformed
*/
public WebUrlImpl(Site site, String path, long version, RequestFlavor flavor,
Language language) throws IllegalArgumentException {
super(path, '/');
if (site == null)
throw new IllegalArgumentException("Site must not be null");
this.site = site;
this.version = version;
this.flavor = flavor;
this.language = language;
}
/**
* Creates a new url that has the same properties as <code>url</code> except
* for the <code>path</code>. This constructor is intended to be used when
* redirecting.
*
* @param url
* the original url
* @param path
* the new path
* @throws IllegalArgumentException
* if either one of <code>site</code> or <code>path</code> are
* <code>null</code> or the path is malformed
*/
public WebUrlImpl(WebUrlImpl url, String path) throws IllegalArgumentException {
super(path, url.getPathSeparator());
this.site = url.getSite();
this.version = url.getVersion();
this.flavor = url.getFlavor();
this.language = url.getLanguage();
this.link = url.getLink();
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.url.WebUrl#getSite()
*/
public Site getSite() {
return site;
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.url.WebUrl#getLink()
*/
public String getLink() {
if (link == null) {
try {
link = URLEncoder.encode(getLink(-1, null, null), "utf-8");
} catch (UnsupportedEncodingException e) {
logger.error("Unexpected error while urlencoding link {}", link, e);
}
link = link.replaceAll("%2F", "/");
}
return link;
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.url.WebUrl#getLink(long)
*/
public String getLink(long version) {
return getLink(version, null, null);
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.url.WebUrl#getLink(ch.entwine.weblounge.common.language.Language)
*/
public String getLink(Language language) {
return getLink(-1, language, null);
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.url.WebUrl#getLink(java.lang.String)
*/
public String getLink(String flavor) {
return getLink(-1, null, flavor);
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.url.WebUrl#getLink(long,
* ch.entwine.weblounge.common.language.Language, java.lang.String)
*/
public String getLink(long version, Language language, String flavor) {
StringBuffer selector = new StringBuffer();
boolean hasVersion = false;
if (version >= 0 || this.version > 0 || language != null || this.language != null || flavor != null || this.flavor != null) {
if (version < 0)
version = this.version;
if (version == Resource.LIVE)
selector.append("index");
else if (version == Resource.WORK) {
selector.append("work");
} else if (version >= 0) {
selector.append(Long.toString(version));
} else {
selector.append("index");
}
hasVersion = true;
}
// Language
if (language != null)
selector.append("_").append(language.getIdentifier());
else if (this.language != null) {
selector.append("_").append(this.language.getIdentifier());
}
// Flavor
if (flavor != null)
selector.append(".").append(flavor.toLowerCase());
else if (this.flavor != null) {
selector.append(".").append(this.flavor.toExtension());
} else if (hasVersion) {
selector.append(".").append(RequestFlavor.HTML.toExtension());
}
if (selector.length() > 0)
return UrlUtils.concat(path, selector.toString());
else
return path;
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.url.WebUrl#normalize()
*/
public String normalize() {
return normalize(true, true, true);
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.url.WebUrl#normalize(boolean, boolean,
* boolean)
*/
public String normalize(boolean includeVersion, boolean includeLanguage,
boolean includeFlavor) {
StringBuffer buf = new StringBuffer();
// Path
buf.append(pathElementSeparatorChar).append(path);
// Version
if (includeVersion && version > 0) {
buf.append(pathElementSeparatorChar);
if (version == Resource.WORK)
buf.append("work");
else if (version >= 0)
buf.append(Long.toString(version));
// Language
if (includeLanguage && language != null) {
buf.append("_").append(language.getIdentifier());
}
// Flavor
if (includeFlavor && flavor != null) {
buf.append(".").append(flavor.toExtension());
} else {
buf.append(".");
buf.append(RequestFlavor.HTML.toExtension());
}
} else {
// Language
if (includeLanguage && language != null) {
buf.append(pathElementSeparatorChar).append(language.getIdentifier());
}
// Flavor
if (includeFlavor && flavor != null) {
buf.append(pathElementSeparatorChar).append(flavor.toExtension());
}
}
return trim(buf.toString());
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.url.WebUrl#getFlavor()
*/
public RequestFlavor getFlavor() {
return flavor != null ? flavor : defaultFlavor;
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.url.WebUrl#getVersion()
*/
public long getVersion() {
return version;
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.url.WebUrl#getLanguage()
*/
public Language getLanguage() {
return language;
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.url.WebUrl#hasLanguagePathSegment()
*/
public boolean hasLanguagePathSegment() {
return languageIsPathEncoded;
}
/**
* Returns the hash code for this url. The method includes the super
* implementation and adds sensitivity for the site and the url extension.
*
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
return super.hashCode() | site.hashCode() >> 16;
}
/**
* Returns true if the given object is a url itself and describes the same url
* than this object, including the associated site and possible url
* extensions.
*
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object object) {
if (object instanceof WebUrlImpl) {
WebUrlImpl url = (WebUrlImpl) object;
if (!super.equals(object))
return false;
if (version != url.getVersion())
return false;
if (language == null && url.getLanguage() != null || (language != null && !language.equals(url.getLanguage())))
return false;
if (!getFlavor().equals(url.getFlavor()))
return false;
if (!site.equals(url.getSite()))
return false;
return true;
} else if (object instanceof Path) {
return super.equals(object);
}
return false;
}
/**
* Strips version and flavor from this url. Version and flavor can either be
* encoded as
* <code>path/to/resource/<version>_<language>.<flavor></code>
* or as <code>path/to/resource/version/language/flavor</code>.
*
* @param path
* the full path
* @param separator
* path separator character
* @return the directory path
* @throws IllegalArgumentException
* if an invalid path is given
*/
protected String analyzePath(String path, char separator)
throws IllegalArgumentException {
if (!path.startsWith(Character.toString(separator)) && path.contains("://")) {
try {
URL u = new URL(path);
path = u.getPath();
} catch (MalformedURLException e) {
throw new IllegalArgumentException("Path " + path + " cannot be parsed");
}
}
path = trim(path);
// Make sure the path is absolute
if (!path.startsWith(WebUrlImpl.separator))
throw new IllegalArgumentException("Path must be absolute");
Matcher pathMatcher = pathInspector.matcher(path);
if (pathMatcher.matches()) {
// Version
String v = pathMatcher.group(2);
if ("index".equals(v) || "live".equals(v)) {
this.version = Resource.LIVE;
} else if ("work".equals(v)) {
this.version = Resource.WORK;
} else if (v != null && !"".equals(v)) {
try {
this.version = Long.parseLong(v);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Unable to extract version from url " + path);
}
}
// Language (will be something like "_fr")
String l = pathMatcher.group(3);
if (l != null && !"".equals(l)) {
l = l.substring(1);
Language language = site.getLanguage(l);
if (language == null) {
logger.debug("Switching request language {} for {}", l, site.getDefaultLanguage().getIdentifier());
this.language = site.getDefaultLanguage();
}
this.language = language;
}
// Flavor
String f = pathMatcher.group(4);
if (f != null && !"".equals(f))
try {
this.flavor = RequestFlavor.parseString(f);
} catch (IllegalArgumentException e) {
logger.debug("Found unknwon request flavor {}", f);
}
return trim(pathMatcher.group(1));
}
// Try the segmented approach for /path/to/resource/<language>/<flavor>
Matcher segmentMatcher = segmentInspector.matcher(path);
if (segmentMatcher.matches()) {
// Extract flavor and language
String url = trim(segmentMatcher.group(1));
String[] segments = url.split(Character.toString(separator));
for (int i = segments.length; i > 0; i--) {
String segment = segments[i - 1].replaceAll(Character.toString(separator), "");
boolean foundMetadata = false;
// Test for flavor
try {
this.flavor = RequestFlavor.parseString(segment);
url = url.substring(0, url.length() - segment.length() - 1);
foundMetadata = true;
continue;
} catch (IllegalArgumentException e) {
logger.debug("Found unknown request flavor {}", segment);
}
// Test group for language
Language language = site.getLanguage(segment);
if (language != null) {
this.language = language;
this.languageIsPathEncoded = true;
url = url.substring(0, url.length() - segment.length() - 1);
foundMetadata = true;
continue;
}
if (!foundMetadata)
break;
}
return trim(url);
}
throw new IllegalArgumentException("Invalid path provided");
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.impl.url.UrlImpl#toString()
*/
@Override
public String toString() {
return getLink();
}
}