/* Copyright (2005-2012) Schibsted ASA * This file is part of Possom. * * Possom 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 3 of the License, or * (at your option) any later version. * * Possom 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 Possom. If not, see <http://www.gnu.org/licenses/>. * * Site.java * * Created on 22 January 2006, 13:48 * */ package no.sesat.search.site; import java.io.IOException; import java.io.InputStream; import java.io.Serializable; import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.Properties; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.locks.ReentrantReadWriteLock; import no.sesat.commons.ioc.BaseContext; import org.apache.log4j.Level; import no.sesat.Interpreter; import no.sesat.Interpreter.Function; import org.apache.log4j.Logger; /** A Site object identifies a Skin + Locale pairing. * * This bean holds nothing more than the name of the virtual host (siteName) and locale used to access this Skin. * * It is used as a key to obtain the correct factory instances in the application. * There is usually only one Skin per siteName and it is left up to the skin to handle different locales. * . * <b>Immutable</b>. * * Does a little bit of niggling wiggling to load the DEFAULT site. See static constructor. * * @version $Id$ * */ public final class Site implements Serializable { /** During construction of any site we must know who the parent site is going to be. * It will likely be also constructed since leaf sites are those first requested. * * The base level sites, locale variants of the DEFAULT site, are expected to have a null parent. * These DEFAULT sites can just pass in a null context instead of constructing a context that returns a null parent. * * Not to be confused with the SiteContext. * This is a Context required for constructing a Site. * While a SiteContext is a context required to use a Site. **/ public interface Context extends BaseContext{ /** Get the name of the parent site. * @param siteContext that can provide the current site the code is within. * @return the name of the parent site. */ String getParentSiteName(SiteContext siteContext); } private static final Logger LOG = Logger.getLogger(Site.class); private static final String FATAL_CANT_FIND_DEFAULT_SITE = "Could not load the property \"site.default\" from configuration.properties" + "to define what the default site is."; /** Found from the configuration.properties resource found in this class's ClassLoader. **/ public static final String DEFAULT_SITE_KEY = "site.default"; public static final String DEFAULT_SITE_LOCALE_KEY = "site.default.locale.default"; public static final String DEFAULT_SERVER_PORT_KEY = "server.port"; /** Property key for site parent's name. **/ public static final String PARENT_SITE_KEY = "site.parent"; /** Property key for a site object. **/ public static final String NAME_KEY = "site"; /** Name of the resource to find the PARENT_SITE_KEY property. **/ public static final String CONFIGURATION_FILE = "configuration.properties"; private static final String CORE_CONF_FILE = "core.properties"; private static final Map<String,Site> INSTANCES = new HashMap<String,Site>(); private static final ReentrantReadWriteLock INSTANCES_LOCK = new ReentrantReadWriteLock(); private static final Map<Locale,String> LOCALE_DISPLAY_NAMES = new ConcurrentHashMap<Locale, String>(); private static volatile boolean constructingDefault = false; /** * Holds value of property siteName. */ private final String siteName; /** * Holds value of property cxtName. */ private final String cxtName; /** * Holds value of property locale. */ private Locale locale; /** * Holds value of property uniqueName. */ private final String uniqueName; /** * Holds value of property parent. */ private final Site parent; private transient SiteContext siteContext; private Site() { siteName = null; cxtName = null; locale = Locale.getDefault(); uniqueName = null; parent = null; init(); } /** Creates a new instance of Site. * A null Context will result in a parentSiteName == siteName * @throws IllegalArgumentException when there exists no skin matching the theSiteName argument. */ private Site(final Context cxt, final String theSiteName, final Locale theLocale) { try{ INSTANCES_LOCK.writeLock().lock(); LOG.info("Site(cxt, " + theSiteName + ", " + theLocale + ')'); assert null != theSiteName; assert null != theLocale; // siteName must finish with a '\' siteName = ensureTrailingSlash(theSiteName); cxtName = ensureTrailingSlash(siteName.replaceAll(":.*$", "")); // don't include the port in the cxtName. locale = theLocale; uniqueName = getUniqueName(siteName, locale); init(); final String parentSiteName; if(null != cxt){ // cxt.getParentSiteName(siteContext) is an expensive call due to resource load every call. final String psn = cxt.getParentSiteName(siteContext); parentSiteName = null != psn ? ensureTrailingSlash(psn) : null; }else{ parentSiteName = siteName; } final String tsParentNameNoPort = null != parentSiteName ? ensureTrailingSlash(parentSiteName.replaceAll(":.*$", "")) : null; LOG.debug(siteName + " parent is " + parentSiteName); if(constructingDefault || DEFAULT.getName().equals(cxtName)){ // dont let the original DEFAULT have a parent // OR let a port-explicit generic.sesam have a parent. parent = null; }else{ LOG.debug("Default-check-> " + DEFAULT.getName() + " ?= " + parentSiteName); final boolean invalidParent = null == parentSiteName // also avoid any incest || siteName.equals(tsParentNameNoPort) // and detect ahead the link to the grandfather of all "generic.sesam" || DEFAULT.getName().equals(parentSiteName); parent = invalidParent ? DEFAULT : Site.valueOf(cxt, parentSiteName, theLocale); } assert null != parent || constructingDefault || DEFAULT.getName().equals(cxtName) : "Parent must exist for all Sites except the DEFAULT"; // register in global pool. LOG.debug("INSTANCES.put(" + uniqueName + ", this)"); INSTANCES.put(uniqueName, this); }finally{ INSTANCES_LOCK.writeLock().unlock(); } } private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); init(); } private void init() { final Site thisSite = this; siteContext = new SiteContext() { public Site getSite() { return thisSite; } }; } /** * Get a SiteContext for this site. * * @return SiteContext for this site. */ public SiteContext getSiteContext() { return siteContext; } /** the parent to this site. * @return site null if we are the DEFAULT site. */ public Site getParent(){ return parent; } /** * Getter for property siteName. * Guaranteed to have "www." prefix stripped. * Guaranteed to finish with '/'. * @return Value of property siteName. */ public String getName() { return siteName; } /** * Getter for property cxtName. * Same as name but without port specification. * Guaranteed to finish with '/'. * @return Value of property cxtName. */ public String getConfigContext() { return cxtName; } /** * Getter for property (velocity) template directory. * Absolute URL to directory (velocity) template is found for this site. * <b>Does not</b> finish with '/'. Reads nicer in template statements. * @return Value of property (velocity) template directory. */ public String getTemplateDir() { return "http://" + siteName + cxtName + "templates"; } /** * Getter for property locale. * @return Value of property locale. */ public Locale getLocale() { return locale; } @Override public String toString(){ return uniqueName; } @Override public boolean equals(final Object obj) { return obj instanceof Site ? uniqueName.equals(((Site)obj).uniqueName) : super.equals(obj); } @Override public int hashCode() { return uniqueName.hashCode(); } /** Get the instance for the given siteName. * The port number will be changed if the server has explicitly assigned one port number to use. * A "www." prefix will be automatically ignored. * * TODO refactor to instanceOf(..). it is an instance returned not a primitive. * * @param cxt the cxt to use during creation. null will prevent constructing a new site. * @param siteName the virtual host name. * @param locale the locale desired * @throws IllegalArgumentException when there exists no skin matching the siteName argument. * @return the site instance. never null. */ public static Site valueOf(final Context cxt, final String siteName, final Locale locale) { Site site = null; // Strip www. from siteName final String realSiteName = ensureTrailingSlash(siteName.replaceAll("www.", "")); // Look for existing instances final String uniqueName = getUniqueName(realSiteName,locale); try{ INSTANCES_LOCK.readLock().lock(); LOG.trace("INSTANCES.get(" + uniqueName + ")"); site = INSTANCES.get(uniqueName); }finally{ INSTANCES_LOCK.readLock().unlock(); } // construct a new instance if (null == site && null != cxt) { site = new Site(cxt, realSiteName, locale); } return site; } static { final Properties props = new Properties(); final InputStream is = Site.class.getResourceAsStream('/' + CORE_CONF_FILE); try { if(null != is){ props.load(is); is.close(); } } catch (IOException ex) { LOG.fatal(FATAL_CANT_FIND_DEFAULT_SITE, ex); } final Level oLevel = LOG.getLevel(); LOG.setLevel(Level.ALL); final String defaultSiteName = props.getProperty(DEFAULT_SITE_KEY, System.getProperty(DEFAULT_SITE_KEY)); LOG.info("defaultSiteName: " + defaultSiteName); final String defaultSiteLocaleName = props.getProperty(DEFAULT_SITE_LOCALE_KEY, System.getProperty(DEFAULT_SITE_LOCALE_KEY)); LOG.info("defaultSiteLocaleName: " + defaultSiteLocaleName); final String defaultSitePort = props.getProperty(DEFAULT_SERVER_PORT_KEY, System.getProperty(DEFAULT_SERVER_PORT_KEY)); LOG.info("defaultSitePort: " + defaultSitePort); LOG.setLevel(oLevel); SERVER_PORT = Integer.parseInt(defaultSitePort); constructingDefault = true; final Locale defaultLocale = new Locale(defaultSiteLocaleName); DEFAULT = new Site(null, defaultSiteName, defaultLocale); // All locales along-side DEFAULT for(Locale l : Locale.getAvailableLocales()){ if(defaultLocale != l){ new Site(null, defaultSiteName, l); } } constructingDefault = false; } /** the default SiteSearch. For example: "generic.sesam" or "generic.localhost:8080". */ public static final Site DEFAULT; /** the server's actual port. **/ public static final int SERVER_PORT; /** Get a uniqueName given a pair of siteName and locale. * Used internally and before construction of any new Site. * * @param siteName * @param locale * @return */ public static String getUniqueName(final String siteName, final Locale locale) { String localDisplayName = LOCALE_DISPLAY_NAMES.get(locale); if(null == localDisplayName){ localDisplayName = locale.getDisplayName(); LOCALE_DISPLAY_NAMES.put(locale, localDisplayName); } return siteName + '[' + localDisplayName + ']'; } private static String ensureTrailingSlash(final String theSiteName) { return theSiteName.endsWith("/") ? theSiteName : theSiteName + '/'; } static { Interpreter.addFunction("sites", new Function() { public String execute(no.sesat.Interpreter.Context ctx) { String res = "Sites instances:\n"; try{ INSTANCES_LOCK.readLock().lock(); for(String s : INSTANCES.keySet()) { Site site = INSTANCES.get(s); res += "Site: " + s + "\n"; res += " Name: " + site.getName() + "\n"; res += " Local: " + site.getLocale() + "\n"; res += " cxtName: " + site.cxtName + "\n"; res += " UniqueName: " + site.uniqueName + "\n\n"; } }finally{ INSTANCES_LOCK.readLock().unlock(); } return res; } @Override public String describe() { return "List content of INSTANCES in Site."; } }); } }