/* Copyright (2006-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/>. * * ViewFactory.java * * Created on 19. april 2006, 20:48 */ package no.sesat.search.view; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.concurrent.locks.ReentrantReadWriteLock; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import no.sesat.commons.ioc.ContextWrapper; import no.sesat.Interpreter; import no.sesat.search.site.config.AbstractConfigFactory; import no.sesat.search.view.navigation.NavigationConfig; import no.sesat.search.site.config.DocumentLoader; import no.sesat.search.site.config.ResourceContext; import no.sesat.search.site.Site; import no.sesat.search.site.SiteContext; import no.sesat.search.site.SiteKeyedFactory; import no.sesat.search.site.config.AbstractDocumentFactory; import no.sesat.search.site.config.Spi; import no.sesat.search.view.config.SearchTab.Layout; import no.sesat.search.view.config.SearchTab; import org.apache.log4j.Logger; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; /** Deserialises views.xml into SearchTab (& its inner classes). * * * @version $Id$ */ public final class SearchTabFactory extends AbstractDocumentFactory implements SiteKeyedFactory{ /** * The context any SearchTabFactory must work against. */ public interface Context extends ResourceContext, AbstractConfigFactory.Context {} // Constants ----------------------------------------------------- private static final Map<Site, SearchTabFactory> INSTANCES = new HashMap<Site,SearchTabFactory>(); private static final ReentrantReadWriteLock INSTANCES_LOCK = new ReentrantReadWriteLock(); private static final TabFactory TAB_FACTORY = new TabFactory(); /** * Name of the configuration file. */ public static final String VIEWS_XMLFILE = "views.xml"; private static final Logger LOG = Logger.getLogger(SearchTabFactory.class); private static final String ERR_DOC_BUILDER_CREATION = "Failed to DocumentBuilderFactory.newInstance().newDocumentBuilder()"; private static final String INFO_PARSING_TAB = "Parsing tab "; private static final String INFO_PARSING_ENRICHMENT = " Parsing enrichment "; private static final String RESET_NAV_ELEMENT = "reset"; private static final String NAV_CONFIG_ELEMENT = "config"; // Attributes ---------------------------------------------------- private final Map<String,SearchTab> tabsByName = new HashMap<String,SearchTab>(); private final Map<String,SearchTab> tabsByKey = new HashMap<String,SearchTab>(); private final DocumentLoader loader; private final Context context; // Static -------------------------------------------------------- /** Return the factory in use for the skin defined within the context. * * @param cxt * @return */ public static SearchTabFactory instanceOf(final Context cxt) { final Site site = cxt.getSite(); assert null != site; SearchTabFactory instance; try{ INSTANCES_LOCK.readLock().lock(); instance = INSTANCES.get(site); }finally{ INSTANCES_LOCK.readLock().unlock(); } if (instance == null) { try { instance = new SearchTabFactory(cxt); } catch (ParserConfigurationException ex) { LOG.error(ERR_DOC_BUILDER_CREATION,ex); } } return instance; } /** Remove the factory in use for the skin defined within the context. **/ public boolean remove(final Site site){ try{ INSTANCES_LOCK.writeLock().lock(); return null != INSTANCES.remove(site); }finally{ INSTANCES_LOCK.writeLock().unlock(); } } // Constructors -------------------------------------------------- /** Creates a new instance of ViewFactory */ private SearchTabFactory(final Context cxt) throws ParserConfigurationException { LOG.trace("SearchTabFactory(cxt)"); try{ INSTANCES_LOCK.writeLock().lock(); context = cxt; // configuration files final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setValidating(false); final DocumentBuilder builder = factory.newDocumentBuilder(); loader = context.newDocumentLoader(cxt, VIEWS_XMLFILE, builder); // start initialisation init(); // update the store of factories INSTANCES.put(context.getSite(), this); LOG.debug("site: "+ context.getSite() + "; tabsByName:" + tabsByName); }finally{ INSTANCES_LOCK.writeLock().unlock(); } } // Public -------------------------------------------------------- /** Find the tab with the given id. * Search recursively up through the skin's parents. * <b>Allow to return null.</b> * @param id * @return */ public SearchTab getTabByName(final String id){ LOG.trace("getTabByName(" + id + ')'); LOG.trace(tabsByName); SearchTab tab = getTabImpl(id); Site site = context.getSite().getParent(); while(null == tab && null != site){ // not found in this site's views.xml. look in parent's site. final SearchTabFactory factory = instanceOf(ContextWrapper.wrap( Context.class, site.getSiteContext(), context )); tab = factory.getTabByName(id); site = site.getParent(); } if(null != tab){ LOG.trace("found tab for " + id + " against SearchTabFactory for " + context.getSite()); } return tab; } /** Find the tab with the given key. * Search recursively up through the skin's parents. * <b>Allow to return null.</b> * @param key * @return */ public SearchTab getTabByKey(final String key){ LOG.trace("getTabByKey(" + key + ')'); SearchTab tab = getTabByKeyImpl(key); Site site = context.getSite().getParent(); while(null == tab && null != site){ // not found in this site's views.xml. look in parent's site. final SearchTabFactory factory = instanceOf(ContextWrapper.wrap( Context.class, site.getSiteContext(), context )); tab = factory.getTabByKeyImpl(key); site = site.getParent(); } if(null != tab){ LOG.trace("found tab for " + key + " against SearchTabFactory for " + context.getSite()); } return tab; } public Map<String,SearchTab> getTabsByName(){ LOG.trace("getTabsByName()"); return Collections.unmodifiableMap(tabsByName); } // Package protected --------------------------------------------- // Protected ----------------------------------------------------- // Private ------------------------------------------------------- /** Initialises instance. * Crucial that this method is called from the constructor, * otherwise read/write synchronisation must be re-added to the tabsByName & tabsByKey fields. * * @throws javax.xml.parsers.ParserConfigurationException */ private void init() throws ParserConfigurationException { loader.abut(); LOG.info("Parsing " + VIEWS_XMLFILE + " started. " + "Site: " + context.getSite()); final Document doc = loader.getDocument(); final Element root = doc.getDocumentElement(); if( null != root ){ final NodeList tabList = root.getChildNodes(); for(int i = 0 ; i < tabList.getLength(); ++i){ if(tabList.item(i) instanceof Element){ final Element tabE = (Element) tabList.item(i); if("tab".equals(tabE.getTagName())){ final String key = parseString(tabE.getAttribute("key"), ""); final SearchTab inherit = getTabByName(tabE.getAttribute("inherit")); final SearchTab tab = TAB_FACTORY.parseTab(tabE, context, inherit); tabsByName.put(tab.getId(), tab); if(key.length() > 0){ tabsByKey.put(key, tab); } } } } } // finished LOG.info("Parsing " + VIEWS_XMLFILE + " finished"); } private SearchTab getTabImpl(final String id){ LOG.trace("getTabImpl(" + id + ')'); return tabsByName.get(id); } private SearchTab getTabByKeyImpl(final String key){ LOG.trace("getTabByKeyImpl(" + key + ')'); return tabsByKey.get(key); } // Inner classes ------------------------------------------------- private static final class TabFactory extends AbstractConfigFactory<SearchTab> { private static final NavFactory NAV_FACTORY = new NavFactory(); SearchTab parseTab( final Element tabE, final Context context, final SearchTab inherit) throws ParserConfigurationException { final String id = tabE.getAttribute("id"); LOG.info(INFO_PARSING_TAB + id); final String mode = parseString(tabE.getAttribute("mode"), inherit != null ? inherit.getMode() : ""); final String key = parseString(tabE.getAttribute("key"), ""); final String parentKey = parseString(tabE.getAttribute("parent-key"), inherit != null ? inherit.getParentKey() : ""); final String adCommand = parseString(tabE.getAttribute("ad-command"), inherit != null ? inherit.getAdCommand() : ""); final String allCss = parseString(tabE.getAttribute("css"), null); final String[] css = allCss != null ? allCss.split(",") : new String[]{}; final String allJavascript = parseString(tabE.getAttribute("javascript"), null); final String[] javascript = allJavascript != null ? allJavascript.split(",") : new String[]{}; // enrichment placement hints final NodeList placementsNodeList = tabE.getElementsByTagName("enrichment-placement"); final Collection<SearchTab.EnrichmentPlacementHint> placements = new ArrayList<SearchTab.EnrichmentPlacementHint>(); for(int j = 0 ; j < placementsNodeList.getLength(); ++j){ final Element e = (Element) placementsNodeList.item(j); final String placementId = e.getAttribute("id"); final int threshold = parseInt(e.getAttribute("threshold"), 0); final int max = parseInt(e.getAttribute("max"), 0); final Map<String,String> properties = new HashMap<String,String>(); final NodeList nodeList = e.getChildNodes(); for (int l = 0; l < nodeList.getLength(); l++) { final Node propNode = nodeList.item(l); if (propNode instanceof Element){ final Element propE = (Element)propNode; properties.put(propE.getNodeName(), propE.getFirstChild().getNodeValue()); } } placements.add(new SearchTab.EnrichmentPlacementHint(placementId, threshold, max, properties)); } // enrichment hints final NodeList enrichmentNodeList = tabE.getElementsByTagName("enrichment"); final Collection<SearchTab.EnrichmentHint> enrichments = new ArrayList<SearchTab.EnrichmentHint>(); for(int j = 0 ; j < enrichmentNodeList.getLength(); ++j){ final Element e = (Element) enrichmentNodeList.item(j); final String rule = e.getAttribute("rule"); LOG.info(INFO_PARSING_ENRICHMENT + rule); final int baseScore = parseInt(e.getAttribute("base-score"), 0); final int threshold = parseInt(e.getAttribute("threshold"), 0); final float weight = parseFloat(e.getAttribute("weight"), 0); final String command = e.getAttribute("command"); final Map<String,String> properties = new HashMap<String,String>(); final NodeList nodeList = e.getChildNodes(); for (int l = 0; l < nodeList.getLength(); l++) { final Node propNode = nodeList.item(l); if (propNode instanceof Element){ final Element propE = (Element)propNode; properties.put(propE.getNodeName(), propE.getFirstChild().getNodeValue()); } } final SearchTab.EnrichmentHint enrichment = new SearchTab.EnrichmentHint( rule, baseScore, threshold, weight, command, properties); enrichments.add(enrichment); } // navigation hints final NodeList navigationNodeList = tabE.getElementsByTagName("navigation"); Element navE = null; for(int j = 0 ; null == navE && j < navigationNodeList.getLength(); ++j){ final Element n = (Element) navigationNodeList.item(j); // only interested in the direct children if(tabE == n.getParentNode()){ navE = n; } } final NavigationConfig navConf = parseNavigation( mode, null != navE ? navE.getElementsByTagName("navigation") : new NodeList() { public Node item(final int arg0) { throw new IllegalArgumentException("empty nodelist"); } public int getLength() { return 0; } }, context, null != inherit ? inherit.getNavigationConfiguration() : null); // the tab's layout final NodeList layoutsNodeList = tabE.getElementsByTagName("layout"); Layout defaultLayout = null; final Layout defaultInheritedLayout = null != inherit ? inherit.getDefaultLayout() : null; final Map<String,Layout> layouts = new HashMap<String,Layout>(); for(int j = 0 ;j < layoutsNodeList.getLength(); ++j){ final Element layoutE = (Element) layoutsNodeList.item(j); final String layoutId = null != layoutE.getAttribute("id") ? layoutE.getAttribute("id") : ""; final Layout inheritedLayout = null != inherit && null != inherit.getLayouts().get(layoutId) ? inherit.getLayouts().get(layoutId) : defaultInheritedLayout; final Layout layout = new Layout(inheritedLayout).readLayout(layoutE); layouts.put(layoutId, layout); if(0 == layoutId.length()){ defaultLayout = layout; } } final String scopeStr = tabE.getAttribute("scope"); final SearchTab.Scope scope = 0 < scopeStr.length() ? SearchTab.Scope.valueOf(scopeStr.toUpperCase()) : null != inherit ? inherit.getScope() : SearchTab.Scope.REQUEST; return new SearchTab( inherit, id, mode, key, parentKey, tabE.getAttribute("rss-result-name"), parseBoolean(tabE.getAttribute("rss-hidden"), false), navConf, placements, enrichments, adCommand, parseInt(tabE.getAttribute("ad-limit"), inherit != null ? inherit.getAdLimit() : -1), parseInt(tabE.getAttribute("ad-on-top"), inherit != null ? inherit.getAdOnTop() : -1), Arrays.asList(css), Arrays.asList(javascript), parseBoolean(tabE.getAttribute("display-css"), true), parseBoolean(tabE.getAttribute("execute-on-blank"), inherit != null ? inherit.isExecuteOnBlank() : false), null != defaultLayout ? defaultLayout : defaultInheritedLayout, layouts, scope); } private NavigationConfig parseNavigation( final String modeId, final NodeList navigationElements, final Context context, final NavigationConfig inherit) throws ParserConfigurationException { final NavigationConfig cfg = new NavigationConfig(inherit); for (int i = 0; i < navigationElements.getLength(); i++) { final Element navigationElement = (Element) navigationElements.item(i); final NavigationConfig.Navigation navigation = new NavigationConfig.Navigation(navigationElement); final NodeList navs = navigationElement.getChildNodes(); for (int l = 0; l < navs.getLength(); l++) { final Node navNode = navs.item(l); if (navNode instanceof Element && ! (RESET_NAV_ELEMENT.equals(navNode.getNodeName()) || NAV_CONFIG_ELEMENT.equals(navNode.getNodeName()))) { navigation.addNav( NAV_FACTORY.parseNav((Element) navNode, navigation, context, null), cfg); } } for (int j = 0; j < navs.getLength(); j++) { final Node navElement = navs.item(j); if (RESET_NAV_ELEMENT.equals(navElement.getNodeName())) { final String resetNavId = ((Element)navElement).getAttribute("id"); if (resetNavId != null) { final NavigationConfig.Nav nav = cfg.getNavMap().get(resetNavId); if (nav != null) { navigation.addReset(nav); } else { LOG.error("Error in config, <reset id=\"" + resetNavId + "\" />, in tab " + modeId + " not found"); } } } } cfg.addNavigation(navigation); } return cfg; } /** XXX Implement me. Everything is hardcoded to SearchTab. Not even used i believe. */ protected Class<SearchTab> findClass(final String xmlName, final Context context) throws ClassNotFoundException { final String bName = xmlToBeanName(xmlName); final String className = Character.toUpperCase(bName.charAt(0)) + bName.substring(1, bName.length()); LOG.debug("findClass " + className); // Special case for "nav". final String classNameFQ = xmlName.equals("nav") ? NavigationConfig.Nav.class.getName() : "no.sesat.search.view.SearchTab"; final Class<SearchTab> clazz = loadClass(context, classNameFQ, Spi.VIEW_CONFIG); LOG.debug("Found class " + clazz.getName()); return clazz; } } private static final class NavFactory extends AbstractConfigFactory<NavigationConfig.Nav> { NavigationConfig.Nav parseNav( final Element element, final NavigationConfig.Navigation navigation, final Context context, final NavigationConfig.Nav parent) throws ParserConfigurationException { try { Class<NavigationConfig.Nav> clazz = null; // TODO: Temporary to keep old-style modes.xml working. if ("reset".equals(element.getNodeName()) || "static-parameter".equals(element.getNodeName())) { clazz = findClass("nav", context); } else { clazz = findClass(element.getNodeName(), context); } final Constructor<NavigationConfig.Nav> c = clazz.getConstructor(NavigationConfig.Nav.class, NavigationConfig.Navigation.class, Element.class); final NavigationConfig.Nav nav = c.newInstance(parent, navigation, element); final NodeList children = element.getChildNodes(); for (int i = 0; i < children.getLength(); ++i) { final Node navNode = children.item(i); if (navNode instanceof Element && !NAV_CONFIG_ELEMENT.equals(navNode.getNodeName())) { nav.addChild(parseNav((Element) navNode, navigation, context, nav)); } } return nav; } catch (InstantiationException ex) { LOG.error(ex.getMessage(), ex); throw new ParserConfigurationException(ex.getMessage()); } catch (IllegalAccessException ex) { LOG.error(ex.getMessage(), ex); throw new ParserConfigurationException(ex.getMessage()); } catch (ClassNotFoundException e) { LOG.error(e.getMessage(), e); return null; } catch (NoSuchMethodException e) { LOG.error(e.getMessage(), e); return null; } catch (InvocationTargetException e) { LOG.error(e.getMessage(), e); return null; } } protected Class<NavigationConfig.Nav> findClass(final String xmlName, final Context context) throws ClassNotFoundException { final String bName = xmlToBeanName(xmlName); final String className = Character.toUpperCase(bName.charAt(0)) + bName.substring(1, bName.length()); LOG.debug("findClass " + className); // Special case for "nav". final String classNameFQ = xmlName.equals("nav") ? NavigationConfig.Nav.class.getName() : "no.sesat.search.view.navigation."+ className+ "NavigationConfig"; final Class<NavigationConfig.Nav> clazz = loadClass(context, classNameFQ, Spi.VIEW_CONFIG); LOG.debug("Found class " + clazz.getName()); return clazz; } } static { Interpreter.addFunction("tabs", new Interpreter.Function() { public String execute(Interpreter.Context ctx) { String res = ""; try{ INSTANCES_LOCK.readLock().lock(); for(Site site : INSTANCES.keySet()) { res += "Site: " + site.getName() + "\n"; SearchTabFactory factory = INSTANCES.get(site); for (String s : factory.tabsByKey.keySet()) { res += " View: " + s + "\n"; res += " " + factory.tabsByKey.get(s).toString(); res += "\n"; } res += "\n"; } }finally{ INSTANCES_LOCK.readLock().unlock(); } return res; } public String describe() { return "Print out the tabs in tabsByKey for each site."; } }); } }