summaryrefslogtreecommitdiffstats
path: root/container-search/src/main/java/com/yahoo/search/pagetemplates
diff options
context:
space:
mode:
authorJon Bratseth <bratseth@yahoo-inc.com>2016-06-15 23:09:44 +0200
committerJon Bratseth <bratseth@yahoo-inc.com>2016-06-15 23:09:44 +0200
commit72231250ed81e10d66bfe70701e64fa5fe50f712 (patch)
tree2728bba1131a6f6e5bdf95afec7d7ff9358dac50 /container-search/src/main/java/com/yahoo/search/pagetemplates
Publish
Diffstat (limited to 'container-search/src/main/java/com/yahoo/search/pagetemplates')
-rw-r--r--container-search/src/main/java/com/yahoo/search/pagetemplates/PageTemplate.java82
-rw-r--r--container-search/src/main/java/com/yahoo/search/pagetemplates/PageTemplateRegistry.java16
-rw-r--r--container-search/src/main/java/com/yahoo/search/pagetemplates/PageTemplateSearcher.java234
-rw-r--r--container-search/src/main/java/com/yahoo/search/pagetemplates/PlaceholderMappingVisitor.java36
-rw-r--r--container-search/src/main/java/com/yahoo/search/pagetemplates/PlaceholderReferenceCreatingVisitor.java30
-rw-r--r--container-search/src/main/java/com/yahoo/search/pagetemplates/SourceVisitor.java27
-rw-r--r--container-search/src/main/java/com/yahoo/search/pagetemplates/config/PageTemplateConfigurer.java33
-rw-r--r--container-search/src/main/java/com/yahoo/search/pagetemplates/config/PageTemplateXMLReader.java355
-rw-r--r--container-search/src/main/java/com/yahoo/search/pagetemplates/config/package-info.java7
-rw-r--r--container-search/src/main/java/com/yahoo/search/pagetemplates/engine/Organizer.java177
-rw-r--r--container-search/src/main/java/com/yahoo/search/pagetemplates/engine/RelevanceComparator.java29
-rw-r--r--container-search/src/main/java/com/yahoo/search/pagetemplates/engine/Resolution.java66
-rw-r--r--container-search/src/main/java/com/yahoo/search/pagetemplates/engine/Resolver.java114
-rw-r--r--container-search/src/main/java/com/yahoo/search/pagetemplates/engine/SourceOrderComparator.java57
-rw-r--r--container-search/src/main/java/com/yahoo/search/pagetemplates/engine/package-info.java7
-rw-r--r--container-search/src/main/java/com/yahoo/search/pagetemplates/engine/resolvers/DeterministicResolver.java56
-rw-r--r--container-search/src/main/java/com/yahoo/search/pagetemplates/engine/resolvers/RandomResolver.java50
-rw-r--r--container-search/src/main/java/com/yahoo/search/pagetemplates/engine/resolvers/ResolverRegistry.java59
-rw-r--r--container-search/src/main/java/com/yahoo/search/pagetemplates/engine/resolvers/package-info.java7
-rw-r--r--container-search/src/main/java/com/yahoo/search/pagetemplates/model/AbstractChoice.java31
-rw-r--r--container-search/src/main/java/com/yahoo/search/pagetemplates/model/Choice.java114
-rw-r--r--container-search/src/main/java/com/yahoo/search/pagetemplates/model/Layout.java50
-rw-r--r--container-search/src/main/java/com/yahoo/search/pagetemplates/model/MapChoice.java69
-rw-r--r--container-search/src/main/java/com/yahoo/search/pagetemplates/model/PageElement.java16
-rw-r--r--container-search/src/main/java/com/yahoo/search/pagetemplates/model/PageTemplateVisitor.java41
-rw-r--r--container-search/src/main/java/com/yahoo/search/pagetemplates/model/Placeholder.java49
-rw-r--r--container-search/src/main/java/com/yahoo/search/pagetemplates/model/Renderer.java88
-rw-r--r--container-search/src/main/java/com/yahoo/search/pagetemplates/model/Section.java177
-rw-r--r--container-search/src/main/java/com/yahoo/search/pagetemplates/model/Source.java137
-rw-r--r--container-search/src/main/java/com/yahoo/search/pagetemplates/model/package-info.java7
-rw-r--r--container-search/src/main/java/com/yahoo/search/pagetemplates/package-info.java7
-rw-r--r--container-search/src/main/java/com/yahoo/search/pagetemplates/result/SectionHitGroup.java52
-rw-r--r--container-search/src/main/java/com/yahoo/search/pagetemplates/result/package-info.java7
33 files changed, 2287 insertions, 0 deletions
diff --git a/container-search/src/main/java/com/yahoo/search/pagetemplates/PageTemplate.java b/container-search/src/main/java/com/yahoo/search/pagetemplates/PageTemplate.java
new file mode 100644
index 00000000000..8c421feae47
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/pagetemplates/PageTemplate.java
@@ -0,0 +1,82 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.pagetemplates;
+
+import com.yahoo.component.ComponentId;
+import com.yahoo.component.provider.FreezableComponent;
+import com.yahoo.search.pagetemplates.model.PageElement;
+import com.yahoo.search.pagetemplates.model.PageTemplateVisitor;
+import com.yahoo.search.pagetemplates.model.Section;
+import com.yahoo.search.pagetemplates.model.Source;
+
+import java.util.Collections;
+import java.util.Set;
+
+/**
+ * A page template represents a particular way to organize a return page. It is a recursive structure of
+ * page template elements.
+ *
+ * @author bratseth
+ */
+public final class PageTemplate extends FreezableComponent implements PageElement {
+
+ /** The root section of this page */
+ private Section section=new Section();
+
+ /** The sources mentioned (recursively) in this page template, or null if this is not frozen */
+ private Set<Source> sources=null;
+
+ public PageTemplate(ComponentId id) {
+ super(id);
+ }
+
+ public void setSection(Section section) {
+ ensureNotFrozen();
+ this.section=section;
+ }
+
+ /** Returns the root section of this. This is never null. */
+ public Section getSection() { return section; }
+
+ /**
+ * Returns an unmodifiable set of all the sources this template <i>may</i> include (depending on choice resolution).
+ * If the template allows (somewhere) the "any" source (*), Source.any will be in the set returned.
+ * This operation is fast on frozen page templates (i.e at execution time).
+ */
+ public Set<Source> getSources() {
+ if (isFrozen()) return sources;
+ SourceVisitor sourceVisitor=new SourceVisitor();
+ getSection().accept(sourceVisitor);
+ return Collections.unmodifiableSet(sourceVisitor.getSources());
+ }
+
+ public @Override void freeze() {
+ if (isFrozen()) return;
+ resolvePlaceholders();
+ section.freeze();
+ sources=getSources();
+ super.freeze();
+ }
+
+ /** Validates and creates the necessary internal references between placeholders and their resolving choices */
+ private void resolvePlaceholders() {
+ try {
+ PlaceholderMappingVisitor placeholderMappingVisitor=new PlaceholderMappingVisitor();
+ accept(placeholderMappingVisitor);
+ accept(new PlaceholderReferenceCreatingVisitor(placeholderMappingVisitor.getMap()));
+ }
+ catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException(this + " is invalid",e);
+ }
+ }
+
+ /** Accepts a visitor to this structure */
+ public @Override void accept(PageTemplateVisitor visitor) {
+ visitor.visit(this);
+ section.accept(visitor);
+ }
+
+ public @Override String toString() {
+ return "page template '" + getId() + "'";
+ }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/pagetemplates/PageTemplateRegistry.java b/container-search/src/main/java/com/yahoo/search/pagetemplates/PageTemplateRegistry.java
new file mode 100644
index 00000000000..ffeec4b5dd1
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/pagetemplates/PageTemplateRegistry.java
@@ -0,0 +1,16 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.pagetemplates;
+
+import com.yahoo.component.provider.ComponentRegistry;
+import com.yahoo.search.pagetemplates.engine.Resolver;
+
+/**
+ * @author bratseth
+ */
+public class PageTemplateRegistry extends ComponentRegistry<PageTemplate> {
+
+ public void register(PageTemplate pageTemplate) {
+ super.register(pageTemplate.getId(), pageTemplate);
+ }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/pagetemplates/PageTemplateSearcher.java b/container-search/src/main/java/com/yahoo/search/pagetemplates/PageTemplateSearcher.java
new file mode 100644
index 00000000000..eb928097e2d
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/pagetemplates/PageTemplateSearcher.java
@@ -0,0 +1,234 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.pagetemplates;
+
+import com.google.inject.Inject;
+import com.yahoo.component.ComponentId;
+import com.yahoo.component.chain.dependencies.Provides;
+import com.yahoo.component.provider.ComponentRegistry;
+import com.yahoo.search.Query;
+import com.yahoo.search.Result;
+import com.yahoo.search.Searcher;
+import com.yahoo.search.intent.model.IntentModel;
+import com.yahoo.search.pagetemplates.config.PageTemplateConfigurer;
+import com.yahoo.search.pagetemplates.engine.Organizer;
+import com.yahoo.search.pagetemplates.engine.Resolution;
+import com.yahoo.search.pagetemplates.engine.Resolver;
+import com.yahoo.search.pagetemplates.engine.resolvers.DeterministicResolver;
+import com.yahoo.search.pagetemplates.engine.resolvers.RandomResolver;
+import com.yahoo.search.pagetemplates.engine.resolvers.ResolverRegistry;
+import com.yahoo.search.pagetemplates.model.Choice;
+import com.yahoo.search.pagetemplates.model.PageElement;
+import com.yahoo.search.pagetemplates.model.Source;
+import com.yahoo.processing.request.CompoundName;
+import com.yahoo.search.result.ErrorMessage;
+import com.yahoo.search.searchchain.Execution;
+
+import java.util.*;
+
+/**
+ * Enables page optimization templates.
+ * This searcher should be placed before federation points in the search chain.
+ * <p>
+ * <b>Input query properties:</b>
+ * <ul>
+ * <li><code>page.idList</code> - a List&lt;String&gt; of id strings of the page templates this should choose between</li>
+ * <li><code>page.id</code> - a space-separated string of ids of the page templates this should choose between.
+ * This property is ignored if <code>page.idList</code> is set</li>
+ * <li><code>page.resolver</code> the id of the resolver to use to resolve choices. This is either the component id
+ * of a deployed resolver component, or one of the strings
+ * <code>native.deterministic</code> (which always pics the last choice) or <code>native.random</code></li>
+ * </ul>
+ *
+ * <b>Output query properties:</b>
+ * <ul>
+ * <li><code>page.ListOfPageTemplate</code>A List&lt;PageTemplate&gt;
+ * containing a list of the page templates used for this query
+ * </ul>
+ *
+ * <p>
+ * The set of page templates chosen for the query specifies a list of sources to be queries (the page template sources).
+ * In addition, the query may contain
+ * <ul>
+ * <li>a set of sources set explicitly in the Request, a query property or a searcher (the query model sources)
+ * <li>a set of sources specified in the {@link com.yahoo.search.intent.model.IntentModel} (the intent model sources)
+ * </ul>
+ * This searcher combines these sources into a single set in query.model by the following rules:
+ * <ul>
+ * <li>If the query model sources is set (not empty), it is not changed
+ * <li>If the page template sources contains the ANY source AND there is an intent model
+ * the query model sources is set to the union of the page template sources and the intent model sources
+ * <li>If the page template sources contains the ANY source AND there is no intent model,
+ * the query model sources is left empty (causing all sources to be queried)
+ * <li>Otherwise, the query model sources is set to the page template sources
+ * </ul>
+ *
+ * @author bratseth
+ */
+@Provides("PageTemplates")
+public class PageTemplateSearcher extends Searcher {
+
+ /** The name of the query property containing the resolved candidate page template list */
+ public static final CompoundName pagePageTemplateListName=new CompoundName("page.PageTemplateList");
+ /** The name of the query property containing a list of candidate pages to consider */
+ public static final CompoundName pageIdListName=new CompoundName("page.idList");
+ /** The name of the query property containing the page id to use */
+ public static final CompoundName pageIdName=new CompoundName("page.id");
+ /** The name of the query property containing the resolver id to use */
+ public static final CompoundName pageResolverName=new CompoundName("page.resolver");
+
+ private final ResolverRegistry resolverRegistry;
+
+ private final Organizer organizer = new Organizer();
+
+ private final PageTemplateRegistry templateRegistry;
+
+ /** Creates this from a configuration. This will be called by the container. */
+ @Inject
+ public PageTemplateSearcher(PageTemplatesConfig pageTemplatesConfig, ComponentRegistry<Resolver> resolverRegistry) {
+ this(PageTemplateConfigurer.toRegistry(pageTemplatesConfig), resolverRegistry.allComponents());
+ }
+
+ /**
+ * Creates this from an existing page template registry, using only built-in resolvers
+ *
+ * @param templateRegistry the page template registry. This will be frozen by this call.
+ * @param resolvers the resolvers to use, in addition to the default resolvers
+ */
+ public PageTemplateSearcher(PageTemplateRegistry templateRegistry, Resolver... resolvers) {
+ this(templateRegistry, Arrays.asList(resolvers));
+ }
+
+ private PageTemplateSearcher(PageTemplateRegistry templateRegistry, List<Resolver> resolvers) {
+ this.templateRegistry = templateRegistry;
+ templateRegistry.freeze();
+ this.resolverRegistry = new ResolverRegistry(resolvers);
+ }
+
+ @Override
+ public Result search(Query query, Execution execution) {
+ // Pre execution: Choose template and sources
+ List<PageElement> pages=selectPageTemplates(query);
+ if (pages.isEmpty()) return execution.search(query); // Bypass if no page template chosen
+ addSources(pages,query);
+
+ // Set the page template list for inspection by other searchers
+ query.properties().set(pagePageTemplateListName, pages);
+
+ // Execute
+ Result result=execution.search(query);
+
+ // Post execution: Resolve choices and organize the result as dictated by the resolved template
+ Choice pageTemplateChoice=Choice.createSingletons(pages);
+ Resolution resolution=selectResolver(query).resolve(pageTemplateChoice,query,result);
+ organizer.organize(pageTemplateChoice,resolution,result);
+ return result;
+ }
+
+ /**
+ * Returns the list of page templates specified in the query, or the default if none, or the
+ * empty list if no default, never null.
+ */
+ private List<PageElement> selectPageTemplates(Query query) {
+ // Determine the list of page template ids
+ @SuppressWarnings("unchecked")
+ List<String> pageIds = (List<String>) query.properties().get(pageIdListName);
+ if (pageIds==null) {
+ String pageIdString=query.properties().getString(pageIdName,"").trim();
+ if (pageIdString.length()>0)
+ pageIds=Arrays.asList(pageIdString.split(" "));
+ }
+
+ // If none set, just return the default or null if none
+ if (pageIds==null) {
+ PageElement defaultPage=templateRegistry.getComponent("default");
+ return (defaultPage==null ? Collections.<PageElement>emptyList() : Collections.singletonList(defaultPage));
+ }
+
+ // Resolve the id list to page templates
+ List<PageElement> pages=new ArrayList<>(pageIds.size());
+ for (String pageId : pageIds) {
+ PageTemplate page=templateRegistry.getComponent(pageId);
+ if (page==null)
+ query.errors().add(ErrorMessage.createInvalidQueryParameter("Could not resolve requested page template '" +
+ pageId + "'"));
+ else
+ pages.add(page);
+ }
+
+ return pages;
+ }
+
+ private Resolver selectResolver(Query query) {
+ String resolverId=query.properties().getString(pageResolverName);
+ if (resolverId==null) return resolverRegistry.defaultResolver();
+ Resolver resolver=resolverRegistry.getComponent(resolverId);
+ if (resolver==null) throw new IllegalArgumentException("No page template resolver '" + resolverId + "'");
+ return resolver;
+ }
+
+ /** Sets query.getModel().getSources() to the right value and add source parameters specified in templates */
+ private void addSources(List<PageElement> pages,Query query) {
+ // Determine all wanted sources
+ Set<Source> pageSources=new HashSet<>();
+ for (PageElement page : pages)
+ pageSources.addAll(((PageTemplate)page).getSources());
+
+ addErrorIfSameSourceMultipleTimes(pages,pageSources,query);
+
+ if (query.getModel().getSources().size() > 0) {
+ // Add properties if the source list is set explicitly, but do not modify otherwise
+ addParametersForIncludedSources(pageSources,query);
+ return;
+ }
+
+ if (pageSources.contains(Source.any)) {
+ IntentModel intentModel=IntentModel.getFrom(query);
+ if (intentModel!=null) {
+ query.getModel().getSources().addAll(intentModel.getSourceNames());
+ addPageTemplateSources(pageSources,query);
+ }
+ // otherwise leave empty to search all
+ }
+ else { // Let the page templates decide
+ addPageTemplateSources(pageSources,query);
+ }
+ }
+
+ private void addPageTemplateSources(Set<Source> pageSources,Query query) {
+ for (Source pageSource : pageSources) {
+ if (pageSource==Source.any) continue;
+ query.getModel().getSources().add(pageSource.getName());
+ addParameters(pageSource,query);
+ }
+ }
+
+ private void addParametersForIncludedSources(Set<Source> sources,Query query) {
+ for (Source source : sources) {
+ if (source.parameters().size()>0 && query.getModel().getSources().contains(source.getName()))
+ addParameters(source,query);
+ }
+ }
+
+ /** Adds parameters specified in the source to the correct namespace in the query */
+ private void addParameters(Source source,Query query) {
+ for (Map.Entry<String,String> parameter : source.parameters().entrySet())
+ query.properties().set("source." + source.getName() + "." + parameter.getKey(),parameter.getValue());
+ }
+
+ /**
+ * Currently executing multiple queries to the same source with different parameter sets,
+ * is not supported. (Same parameter sets in multiple templates is supported,
+ * and will be just one entry in this set).
+ */
+ private void addErrorIfSameSourceMultipleTimes(List<PageElement> pages,Set<Source> sources,Query query) {
+ Set<String> sourceNames=new HashSet<>();
+ for (Source source : sources) {
+ if (sourceNames.contains(source.getName()))
+ query.errors().add(ErrorMessage.createInvalidQueryParameter(
+ "Querying the same source multiple times with different parameter sets as part of one query " +
+ "is not supported. " + pages + " requests this for source '" + source + "'"));
+ sourceNames.add(source.getName());
+ }
+ }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/pagetemplates/PlaceholderMappingVisitor.java b/container-search/src/main/java/com/yahoo/search/pagetemplates/PlaceholderMappingVisitor.java
new file mode 100644
index 00000000000..2d61d17ade8
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/pagetemplates/PlaceholderMappingVisitor.java
@@ -0,0 +1,36 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.pagetemplates;
+
+import com.yahoo.search.pagetemplates.model.*;
+
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Creates a map from placeholder id to the choice providing its value
+ * for all placeholder values visited.
+ * <p>
+ * This visitor will throw an IllegalArgumentException if the same placeholder id
+ * is referenced by two choices.
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+class PlaceholderMappingVisitor extends PageTemplateVisitor {
+
+ private Map<String, MapChoice> placeholderIdToChoice=new LinkedHashMap<>();
+
+ public @Override void visit(MapChoice mapChoice) {
+ List<String> placeholderIds=mapChoice.placeholderIds();
+ for (String placeholderId : placeholderIds) {
+ MapChoice existingChoice=placeholderIdToChoice.put(placeholderId,mapChoice);
+ if (existingChoice!=null)
+ throw new IllegalArgumentException("placeholder id '" + placeholderId + "' is referenced by both " +
+ mapChoice + " and " + existingChoice + ": Only one reference is allowed");
+ }
+ }
+
+ public Map<String, MapChoice> getMap() { return placeholderIdToChoice; }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/pagetemplates/PlaceholderReferenceCreatingVisitor.java b/container-search/src/main/java/com/yahoo/search/pagetemplates/PlaceholderReferenceCreatingVisitor.java
new file mode 100644
index 00000000000..2e22ad7291e
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/pagetemplates/PlaceholderReferenceCreatingVisitor.java
@@ -0,0 +1,30 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.pagetemplates;
+
+import com.yahoo.search.pagetemplates.model.*;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Creates references from all placeholders to the choices which resolves them.
+ * If a placeholder is encountered which is not resolved by any choice, an IllegalArgumentException is thrown.
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+class PlaceholderReferenceCreatingVisitor extends PageTemplateVisitor {
+
+ private Map<String, MapChoice> placeholderIdToChoice=new HashMap<>();
+
+ public PlaceholderReferenceCreatingVisitor(Map<String, MapChoice> placeholderIdToChoice) {
+ this.placeholderIdToChoice=placeholderIdToChoice;
+ }
+
+ public @Override void visit(Placeholder placeholder) {
+ MapChoice choice=placeholderIdToChoice.get(placeholder.getId());
+ if (choice==null)
+ throw new IllegalArgumentException(placeholder + " is not referenced by any choice");
+ placeholder.setValueContainer(choice);
+ }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/pagetemplates/SourceVisitor.java b/container-search/src/main/java/com/yahoo/search/pagetemplates/SourceVisitor.java
new file mode 100644
index 00000000000..bf2685da56f
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/pagetemplates/SourceVisitor.java
@@ -0,0 +1,27 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.pagetemplates;
+
+import com.yahoo.search.pagetemplates.model.PageTemplateVisitor;
+import com.yahoo.search.pagetemplates.model.Source;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Visits a page template object structure and records the sources mentioned.
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+class SourceVisitor extends PageTemplateVisitor {
+
+ private Set<Source> sources=new HashSet<>();
+
+ @Override
+ public void visit(Source source) {
+ sources.add(source);
+ }
+
+ /** Returns the live list of sources collected by this during visiting */
+ public Set<Source> getSources() { return sources; }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/pagetemplates/config/PageTemplateConfigurer.java b/container-search/src/main/java/com/yahoo/search/pagetemplates/config/PageTemplateConfigurer.java
new file mode 100644
index 00000000000..5d106a6df8e
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/pagetemplates/config/PageTemplateConfigurer.java
@@ -0,0 +1,33 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.pagetemplates.config;
+
+import com.yahoo.config.subscription.ConfigSubscriber;
+import com.yahoo.search.pagetemplates.PageTemplatesConfig;
+import com.yahoo.io.reader.NamedReader;
+import com.yahoo.search.pagetemplates.PageTemplateRegistry;
+
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Provides a static method to convert a page template config into a PageTemplateRegistry.
+ * In addition, instances of this can be created to subscribe to config and keep an up to date registry reference.
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+public class PageTemplateConfigurer {
+
+ /**
+ * Creates a new page template registry from the content of a config and returns it.
+ * The returned registry will <b>not</b> be frozen. This should be done, by calling freeze(), before it is used.
+ */
+ public static PageTemplateRegistry toRegistry(PageTemplatesConfig config) {
+ List<NamedReader> pageReaders=new ArrayList<>();
+ int pageNumber=0;
+ for (String pageString : config.page())
+ pageReaders.add(new NamedReader("page[" + pageNumber++ + "]",new StringReader(pageString)));
+ return new PageTemplateXMLReader().read(pageReaders,false);
+ }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/pagetemplates/config/PageTemplateXMLReader.java b/container-search/src/main/java/com/yahoo/search/pagetemplates/config/PageTemplateXMLReader.java
new file mode 100644
index 00000000000..46823f30cf2
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/pagetemplates/config/PageTemplateXMLReader.java
@@ -0,0 +1,355 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.pagetemplates.config;
+
+import com.yahoo.component.ComponentId;
+import com.yahoo.io.reader.NamedReader;
+import com.yahoo.search.pagetemplates.PageTemplate;
+import com.yahoo.search.pagetemplates.PageTemplateRegistry;
+import com.yahoo.search.pagetemplates.model.*;
+import com.yahoo.search.query.Sorting;
+import com.yahoo.text.XML;
+import org.w3c.dom.Element;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.*;
+import java.util.logging.Logger;
+
+/**
+ * Reads all page template XML files from a given directory (or list of readers).
+ * Instances of this are for single-thread usage only.
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+public class PageTemplateXMLReader {
+
+ private static Logger logger=Logger.getLogger(PageTemplateXMLReader.class.getName());
+
+ /** The registry being constructed */
+ private PageTemplateRegistry registry;
+
+ /** XML elements by page id - available after phase 1. Needed for includes. */
+ private Map<ComponentId, Element> pageElementsByPageId=new LinkedHashMap<>();
+
+ /**
+ * Reads all page template xml files in a given directory.
+ *
+ * @throws RuntimeException if <code>directory</code> is not a readable directory, or if there is some error in the XML
+ */
+ public PageTemplateRegistry read(String directory) {
+ List<NamedReader> pageReaders=new ArrayList<>();
+ try {
+ File dir=new File(directory);
+ if ( !dir.isDirectory() ) throw new IllegalArgumentException("Could not read page templates: '" +
+ directory + "' is not a valid directory.");
+
+ for (File file : sortFiles(dir)) {
+ if ( ! file.getName().endsWith(".xml")) continue;
+ pageReaders.add(new NamedReader(file.getName(),new FileReader(file)));
+ }
+
+ return read(pageReaders,true);
+ }
+ catch (IOException e) {
+ throw new IllegalArgumentException("Could not read page templates from '" + directory + "'",e);
+ }
+ finally {
+ for (NamedReader reader : pageReaders) {
+ try { reader.close(); } catch (IOException e) { }
+ }
+ }
+ }
+
+ /**
+ * Reads a single page template file.
+ *
+ * @throws RuntimeException if <code>fileName</code> is not a readable file, or if there is some error in the XML
+ */
+ public PageTemplate readFile(String fileName) {
+ NamedReader pageReader=null;
+ try {
+ File file=new File(fileName);
+ pageReader=new NamedReader(fileName,new FileReader(file));
+ String firstName=file.getName().substring(0,file.getName().length()-4);
+ return read(Collections.singletonList(pageReader),true).getComponent(firstName);
+ }
+ catch (IOException e) {
+ throw new IllegalArgumentException("Could not read the page template '" + fileName + "'",e);
+ }
+ finally {
+ if (pageReader!=null)
+ try { pageReader.close(); } catch (IOException e) { }
+ }
+ }
+
+ private List<File> sortFiles(File dir) {
+ ArrayList<File> files = new ArrayList<>();
+ files.addAll(Arrays.asList(dir.listFiles()));
+ Collections.sort(files);
+ return files;
+ }
+
+ /**
+ * Reads all page template xml files in a given list of readers. This is called from the Vespa configuration model.
+ *
+ * @param validateReaderNames should be set to true if the readers were created by files, not otherwise
+ * @throws RuntimeException if <code>directory</code> is not a readable directory, or if there is some error in the XML
+ */
+ public PageTemplateRegistry read(List<NamedReader> pageReaders,boolean validateReaderNames) {
+ // Initialize state
+ registry=new PageTemplateRegistry();
+
+ // Phase 1
+ pageElementsByPageId=createPages(pageReaders,validateReaderNames);
+ // Phase 2
+ readPages();
+ return registry;
+ }
+
+ private Map<ComponentId,Element> createPages(List<NamedReader> pageReaders,boolean validateReaderNames) {
+ Map<ComponentId,Element> pageElementsByPageId=new LinkedHashMap<>();
+ for (NamedReader reader : pageReaders) {
+ Element pageElement= XML.getDocument(reader).getDocumentElement();
+ if ( ! pageElement.getNodeName().equals("page")) {
+ logger.info("Ignoring '" + reader.getName() +
+ "': Expected XML root element 'page' but was '" + pageElement.getNodeName() + "'");
+ continue;
+ }
+ String idString=pageElement.getAttribute("id");
+
+ if (idString==null || idString.isEmpty())
+ throw new IllegalArgumentException("Page template '" + reader.getName() + "' has no 'id' attribute in the root element");
+ ComponentId id=new ComponentId(idString);
+ if (validateReaderNames)
+ validateFileName(reader.getName(),id,"page template");
+ registry.register(new PageTemplate(id));
+ pageElementsByPageId.put(id,pageElement);
+ }
+ return pageElementsByPageId;
+ }
+
+ /** Throws an exception if the name is not corresponding to the id */
+ private void validateFileName(final String actualName,ComponentId id,String artifactName) {
+ String expectedCanonicalFileName=id.toFileName();
+ String fileName=new File(actualName).getName();
+ fileName=stripXmlEnding(fileName);
+ String canonicalFileName=ComponentId.fromFileName(fileName).toFileName();
+ if ( ! canonicalFileName.equals(expectedCanonicalFileName))
+ throw new IllegalArgumentException("The file name of " + artifactName + " '" + id +
+ "' must be '" + expectedCanonicalFileName + ".xml' but was '" + actualName + "'");
+ }
+
+ private String stripXmlEnding(String fileName) {
+ if (!fileName.endsWith(".xml"))
+ throw new IllegalArgumentException("'" + fileName + "' should have a .xml ending");
+ else
+ return fileName.substring(0,fileName.length()-4);
+ }
+
+ private void readPages() {
+ for (Map.Entry<ComponentId,Element> pageElement : pageElementsByPageId.entrySet()) {
+ try {
+ PageTemplate page=registry.getComponent(pageElement.getValue().getAttribute("id"));
+ readPageContent(pageElement.getValue(),page);
+ }
+ catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException("Could not read page template '" + pageElement.getKey() + "'",e);
+ }
+ }
+ }
+
+ private void readPageContent(Element pageElement,PageTemplate page) {
+ if (page.isFrozen()) return; // Already read
+ Section rootSection=new Section(page.getId().toString());
+ readSection(pageElement,rootSection);
+ page.setSection(rootSection);
+ page.freeze();
+ }
+
+ /** Fills a section with attributes and sub-elements from a "section" or "page" element */
+ private Section readSection(Element sectionElement,Section section) {
+ section.setLayout(Layout.fromString(sectionElement.getAttribute("layout")));
+ section.setRegion(sectionElement.getAttribute("region"));
+ section.setOrder(Sorting.fromString(sectionElement.getAttribute("order")));
+ section.setMax(readOptionalNumber(sectionElement,"max"));
+ section.setMin(readOptionalNumber(sectionElement,"min"));
+ section.elements().addAll(readSourceAttribute(sectionElement));
+ section.elements().addAll(readPageElements(sectionElement));
+ return section;
+ }
+
+ /** Returns all page elements found under the given node */
+ private List<PageElement> readPageElements(Element parent) {
+ List<PageElement> pageElements=new ArrayList<>();
+ for (Element child : XML.getChildren(parent)) {
+ if (child.getNodeName().equals("include"))
+ pageElements.addAll(readInclude(child));
+ else
+ addIfNonNull(readPageElement(child),pageElements);
+ }
+ return pageElements;
+ }
+
+ private void addIfNonNull(PageElement pageElement,List<PageElement> pageElements) {
+ if (pageElement!=null)
+ pageElements.add(pageElement);
+ }
+
+ /** Reads the direct descendant elements of an include */
+ private List<PageElement> readInclude(Element element) {
+ PageTemplate included=registry.getComponent(element.getAttribute("idref"));
+ if (included==null)
+ throw new IllegalArgumentException("Could not find page template '" + element.getAttribute("idref"));
+ readPageContent(pageElementsByPageId.get(included.getId()),included);
+ return included.getSection().elements(Section.class);
+ }
+
+ /** Returns the page element corresponding to the given node, never null */
+ private PageElement readPageElement(Element child) {
+ if (child.getNodeName().equals("choice"))
+ return readChoice(child);
+ else if (child.getNodeName().equals("source"))
+ return readSource(child);
+ else if (child.getNodeName().equals("placeholder"))
+ return readPlaceholder(child);
+ else if (child.getNodeName().equals("section"))
+ return readSection(child,new Section(child.getAttribute("id")));
+ else if (child.getNodeName().equals("renderer"))
+ return readRenderer(child);
+ else if (child.getNodeName().equals("parameter"))
+ return null; // read elsewhere
+ throw new IllegalArgumentException("Unknown node type '" + child.getNodeName() + "'");
+ }
+
+ private List<Source> readSourceAttribute(Element sectionElement) {
+ List<Source> sources=new ArrayList<>();
+ String sourceAttributeString=sectionElement.getAttribute("source");
+ if (sourceAttributeString!=null) {
+ for (String sourceName : sourceAttributeString.split(" ")) {
+ if (sourceName.isEmpty()) continue;
+ if ("*".equals(sourceName))
+ sources.add(Source.any);
+ else
+ sources.add(new Source(sourceName));
+ }
+ }
+ return sources;
+ }
+
+ private Source readSource(Element sourceElement) {
+ Source source=new Source(sourceElement.getAttribute("name"));
+ source.setUrl(nullIfEmpty(sourceElement.getAttribute("url")));
+ source.renderers().addAll(readPageElements(sourceElement));
+ /*
+ source.renderers().addAll(readRenderers(XML.children(sourceElement,"renderer")));
+ readChoices(sourceElement,source);
+ */
+ source.parameters().putAll(readParameters(sourceElement));
+ return source;
+ }
+
+ private String nullIfEmpty(String s) {
+ if (s==null) return s;
+ s=s.trim();
+ if (s.isEmpty()) return null;
+ return s;
+ }
+
+ private Placeholder readPlaceholder(Element placeholderElement) {
+ return new Placeholder(placeholderElement.getAttribute("id"));
+ }
+
+ private Renderer readRenderer(Element rendererElement) {
+ Renderer renderer =new Renderer(rendererElement.getAttribute("name"));
+ renderer.setRendererFor(nullIfEmpty(rendererElement.getAttribute("for")));
+ renderer.parameters().putAll(readParameters(rendererElement));
+ return renderer;
+ }
+
+ private int readOptionalNumber(Element element,String attributeName) {
+ String attributeValue=element.getAttribute(attributeName);
+ try {
+ if (attributeValue.isEmpty()) return -1;
+ return Integer.parseInt(attributeValue);
+ }
+ catch (NumberFormatException e) { // Suppress original exception as it conveys no useful information
+ throw new IllegalArgumentException("'" + attributeName + "' in " + element + " must be a number, not '" + attributeValue + "'");
+ }
+ }
+
+ private AbstractChoice readChoice(Element choiceElement) {
+ String method=nullIfEmpty(choiceElement.getAttribute("method"));
+ if (XML.getChildren(choiceElement,"map").size()>0)
+ return readMapChoice(choiceElement,method);
+ else
+ return readNonMapChoice(choiceElement,method);
+ }
+
+ private MapChoice readMapChoice(Element choiceElement,String method) {
+ Element mapElement=XML.getChildren(choiceElement,"map").get(0);
+ MapChoice map=new MapChoice();
+ map.setMethod(method);
+
+ map.placeholderIds().addAll(readSpaceSeparatedAttribute("to",mapElement));
+ for (Element value : XML.getChildren(mapElement)) {
+ if ("item".equals(value.getNodeName()))
+ map.values().add(readPageElements(value));
+ else
+ map.values().add(Collections.singletonList(readPageElement(value)));
+ }
+ return map;
+ }
+
+ private Choice readNonMapChoice(Element choiceElement,String method) {
+ Choice choice=new Choice();
+ choice.setMethod(method);
+
+ for (Element alternative : XML.getChildren(choiceElement)) {
+ if (alternative.getNodeName().equals("alternative")) // Explicit alternative container
+ choice.alternatives().add(readPageElements(alternative));
+ else if (alternative.getNodeName().equals("include")) // Implicit include
+ choice.alternatives().add(readInclude(alternative));
+ else // Other implicit
+ choice.alternatives().add(Collections.singletonList(readPageElement(alternative)));
+ }
+ return choice;
+ }
+
+ /*
+ private void readChoices(Element sourceElement,Source source) {
+ for (Element choiceElement : XML.children(sourceElement,"choice")) {
+ for (Element alternative : XML.children(choiceElement)) {
+ if ("alternative".equals(alternative.getNodeName())) // Explicit alternative container
+ source.renderer().alternatives().addAll(readRenderers(XML.children(alternative)));
+ else // Implicit alternative - yes implicit and explicit may be combined
+ source.renderer().alternatives().addAll(readRenderers(Collections.singletonList(alternative)));
+ }
+ }
+ }
+ */
+
+ private Map<String,String> readParameters(Element containingElement) {
+ List<Element> parameterElements=XML.getChildren(containingElement,"parameter");
+ if (parameterElements.size()==0) return Collections.emptyMap(); // Shortcut
+
+ Map<String,String> parameters=new LinkedHashMap<>();
+ for (Element parameter : parameterElements) {
+ String key=parameter.getAttribute("name");
+ String value=XML.getValue(parameter);
+ parameters.put(key,value);
+ }
+ return parameters;
+ }
+
+ private List<String> readSpaceSeparatedAttribute(String attributeName, Element containingElement) {
+ List<String> values=new ArrayList<>();
+ String attributeString=nullIfEmpty(containingElement.getAttribute(attributeName));
+ if (attributeString!=null) {
+ for (String value : attributeString.split(" "))
+ values.add(value);
+ }
+ return values;
+ }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/pagetemplates/config/package-info.java b/container-search/src/main/java/com/yahoo/search/pagetemplates/config/package-info.java
new file mode 100644
index 00000000000..40cdfb691ab
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/pagetemplates/config/package-info.java
@@ -0,0 +1,7 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+@PublicApi
+package com.yahoo.search.pagetemplates.config;
+
+import com.yahoo.api.annotations.PublicApi;
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/container-search/src/main/java/com/yahoo/search/pagetemplates/engine/Organizer.java b/container-search/src/main/java/com/yahoo/search/pagetemplates/engine/Organizer.java
new file mode 100644
index 00000000000..00e154d460b
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/pagetemplates/engine/Organizer.java
@@ -0,0 +1,177 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.pagetemplates.engine;
+
+import com.yahoo.search.Result;
+import com.yahoo.search.pagetemplates.PageTemplate;
+import com.yahoo.search.pagetemplates.model.*;
+import com.yahoo.search.pagetemplates.result.SectionHitGroup;
+import com.yahoo.search.query.Sorting;
+import com.yahoo.search.result.*;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Reorganizes and prunes a result as prescribed by a resolved template.
+ * This class is multithread safe.
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+public class Organizer {
+
+ /**
+ * Organizes the given result
+ *
+ * @param templateChoice a choice between singleton lists of PageTemplates
+ * @param resolution the resolution of (at least) the template choice and all choices contained in that template
+ * @param result the result to organize
+ */
+ public void organize(Choice templateChoice, Resolution resolution, Result result) {
+ PageTemplate template=(PageTemplate)templateChoice.get(resolution.getResolution(templateChoice)).get(0);
+ SectionHitGroup sectionGroup =toGroup(template.getSection(),resolution,result);
+ ErrorHit errors=result.hits().getErrorHit();
+
+ // transfer state from existing hit
+ sectionGroup.setQuery(result.hits().getQuery());
+ if (errors!=null && errors instanceof DefaultErrorHit)
+ sectionGroup.add((DefaultErrorHit)errors);
+ for (Iterator<Map.Entry<String, Object>> it = result.hits().fieldIterator(); it.hasNext(); ) {
+ Map.Entry<String, Object> field = it.next();
+ sectionGroup.setField(field.getKey(), field.getValue());
+ }
+
+ result.setHits(sectionGroup);
+ }
+
+ /** Creates the hit group corresponding to a section, drawing data from the given result */
+ private SectionHitGroup toGroup(Section section,Resolution resolution,Result result) {
+ SectionHitGroup sectionGroup=new SectionHitGroup("section:" + section.getId());
+ setField("id",section.getId(),sectionGroup);
+ sectionGroup.setLeaf(section.elements(Section.class).size()==0);
+ setField("layout",section.getLayout().getName(),sectionGroup);
+ setField("region",section.getRegion(),sectionGroup);
+
+ List<String> sourceList=new ArrayList<>();
+ renderElements(resolution, result, sectionGroup, sourceList, section.elements());
+
+ // Trim to max
+ if (section.getMax()>=0)
+ sectionGroup.trim(0,section.getMax());
+ if (sectionGroup.size()>1)
+ assignOrderer(section,resolution,sourceList,sectionGroup);
+
+ return sectionGroup;
+ }
+
+ private void renderElements(Resolution resolution, Result result, SectionHitGroup sectionGroup, List<String> sourceList, List<PageElement> elements) {
+ for (PageElement element : elements) {
+ if (element instanceof Section) {
+ sectionGroup.add(toGroup((Section)element,resolution,result));
+ }
+ else if (element instanceof Source) {
+ addSource(resolution,(Source)element,sectionGroup,result,sourceList);
+ }
+ else if (element instanceof Renderer) {
+ sectionGroup.renderers().add((Renderer)element);
+ }
+ else if (element instanceof Choice) {
+ Choice choice=(Choice)element;
+ if (choice.isEmpty()) continue; // Ignore
+ int chosen=resolution.getResolution(choice);
+ renderElements(resolution, result, sectionGroup, sourceList, choice.alternatives().get(chosen));
+ }
+ else if (element instanceof Placeholder) {
+ Placeholder placeholder =(Placeholder)element;
+ List<PageElement> mappedElements=
+ resolution.getResolution(placeholder.getValueContainer()).get(placeholder.getId());
+ renderElements(resolution,result,sectionGroup,sourceList,mappedElements);
+ }
+ }
+ }
+
+ private void setField(String fieldName,Object value,Hit to) {
+ if (value==null) return;
+ to.setField(fieldName,value);
+ }
+
+ private void addSource(Resolution resolution,Source source,SectionHitGroup sectionGroup,Result result,List<String> sourceList) {
+ renderElements(resolution,result,sectionGroup, sourceList, source.renderers());
+ /*
+ for (PageElement element : source.renderers()) {
+ if (element instanceof Renderer)
+ if (renderer.isEmpty()) continue;
+ sectionGroup.renderers().add(renderer.get(resolution.getResolution(renderer)));
+ }
+ */
+
+ if (source.getUrl()==null)
+ addHitsFromSource(source,sectionGroup,result,sourceList);
+ else
+ sectionGroup.sources().add(source); // source to be rendered by the frontend
+ }
+
+ private void addHitsFromSource(Source source,SectionHitGroup sectionGroup,Result result,List<String> sourceList) {
+ if (source==Source.any) { // Add any source not added yet
+ for (Hit hit : result.hits()) {
+ if ( ! (hit instanceof HitGroup)) continue;
+ String groupId=hit.getId().stringValue();
+ if ( ! groupId.startsWith("source:")) continue;
+ String sourceName=groupId.substring(7);
+ if (sourceList.contains(sourceName)) continue;
+ sectionGroup.addAll(((HitGroup)hit).asList());
+ sourceList.add(sourceName); // Add *'ed sources explicitly
+ }
+ }
+ else {
+ HitGroup sourceGroup=(HitGroup)result.hits().get("source:" + source.getName());
+ if (sourceGroup!=null)
+ sectionGroup.addAll(sourceGroup.asList());
+ sourceList.add(source.getName()); // Add even if not found - may be added later
+ }
+ }
+
+ private void assignOrderer(Section section,Resolution resolution,List<String> sourceList,HitGroup group) {
+ if (section.getOrder()==null) { // then sort by relevance, source
+ group.setOrderer(new HitSortOrderer(new RelevanceComparator(new SourceOrderComparator(sourceList))));
+ return;
+ }
+
+ // replace a source field comparison by one which knows the source list order
+ // and add default sorting at the end if necessary
+ Sorting sorting=section.getOrder();
+ int rankIndex=-1;
+ int sourceIndex=-1;
+ for (int i=0; i<sorting.fieldOrders().size(); i++) {
+ Sorting.FieldOrder order=sorting.fieldOrders().get(i);
+ if ("[relevance]".equals(order.getFieldName()) || "[rank]".equals(order.getFieldName()))
+ rankIndex=i;
+ else if (order.getFieldName().equals("[source]"))
+ sourceIndex=i;
+ }
+
+ ChainableComparator comparator;
+ Sorting beforeSource=null;
+ Sorting afterSource=null;
+ if (sourceIndex>=0) { // replace alphabetical sorting on source by sourceList order sorting
+ if (sourceIndex>0) // sort fields before the source
+ beforeSource=new Sorting(new ArrayList<>(sorting.fieldOrders().subList(0,sourceIndex)));
+ if (sorting.fieldOrders().size()>sourceIndex+1) // sort fields after the source
+ afterSource=new Sorting(new ArrayList<>(sorting.fieldOrders().subList(sourceIndex+1,sorting.fieldOrders().size()+1)));
+
+ comparator=new SourceOrderComparator(sourceList, FieldComparator.create(afterSource));
+ if (beforeSource!=null)
+ comparator=new FieldComparator(beforeSource,comparator);
+
+ }
+ else if (rankIndex>=0) { // add sort by source at the end
+ comparator=new FieldComparator(sorting,new SourceOrderComparator(sourceList));
+ }
+ else { // add sort by rank,source at the end
+ comparator=new FieldComparator(sorting,new RelevanceComparator(new SourceOrderComparator(sourceList)));
+ }
+ group.setOrderer(new HitSortOrderer(comparator));
+ }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/pagetemplates/engine/RelevanceComparator.java b/container-search/src/main/java/com/yahoo/search/pagetemplates/engine/RelevanceComparator.java
new file mode 100644
index 00000000000..7489768b5a3
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/pagetemplates/engine/RelevanceComparator.java
@@ -0,0 +1,29 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.pagetemplates.engine;
+
+import com.yahoo.search.result.ChainableComparator;
+import com.yahoo.search.result.Hit;
+
+import java.util.Comparator;
+
+/**
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+class RelevanceComparator extends ChainableComparator {
+
+ /**
+ * Creates a relevance comparator, with an optional secondary comparator.
+ * If the secondary is null, the intrinsic hit order is used as secondary.
+ */
+ public RelevanceComparator(Comparator<Hit> secondaryComparator) {
+ super(secondaryComparator);
+ }
+
+ public @Override int compare(Hit h1,Hit h2) {
+ int relevanceComparison=h2.getRelevance().compareTo(h1.getRelevance());
+ if (relevanceComparison!=0) return relevanceComparison;
+
+ return super.compare(h1,h2);
+ }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/pagetemplates/engine/Resolution.java b/container-search/src/main/java/com/yahoo/search/pagetemplates/engine/Resolution.java
new file mode 100644
index 00000000000..d67faf805ad
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/pagetemplates/engine/Resolution.java
@@ -0,0 +1,66 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.pagetemplates.engine;
+
+import com.yahoo.search.pagetemplates.model.Choice;
+import com.yahoo.search.pagetemplates.model.MapChoice;
+import com.yahoo.search.pagetemplates.model.PageElement;
+
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A resolution of choices within a template.
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+public class Resolution {
+
+ /** A record of choices made as choice → alternative index (id) */
+ private Map<Choice,Integer> choiceResolutions=new IdentityHashMap<>();
+
+ /** A of map choices made as choice → mapping */
+ private Map<MapChoice,Map<String,List<PageElement>>> mapChoiceResolutions=
+ new IdentityHashMap<>();
+
+ public void addChoiceResolution(Choice choice,int alternativeIndex) {
+ choiceResolutions.put(choice,alternativeIndex);
+ }
+
+ public void addMapChoiceResolution(MapChoice choice, Map<String,List<PageElement>> mapping) {
+ mapChoiceResolutions.put(choice,mapping);
+ }
+
+ /**
+ * Returns the resolution of a choice.
+ *
+ * @return the (0-base) index of the choice made. If the given choice has exactly one alternative,
+ * 0 is always returned (whether or not the choice has been attempted resolved).
+ * @throws IllegalArgumentException if the choice is empty, or if it has multiple alternatives but have not
+ * been resolved in this
+ */
+ public int getResolution(Choice choice) {
+ if (choice.alternatives().size()==1) return 0;
+ if (choice.isEmpty()) throw new IllegalArgumentException("Cannot return a resolution of empty " + choice);
+ Integer resolution=choiceResolutions.get(choice);
+ if (resolution==null) throw new IllegalArgumentException(this + " has no resolution of " + choice);
+ return resolution;
+ }
+
+ /**
+ * Returns the resolution of a map choice.
+ *
+ * @return the chosen mapping - entries from placeholder id to the values to use at the location of that placeholder
+ * @throws IllegalArgumentException if this choice has not been resolved in this
+ */
+ public Map<String,List<PageElement>> getResolution(MapChoice choice) {
+ Map<String,List<PageElement>> resolution=mapChoiceResolutions.get(choice);
+ if (resolution==null) throw new IllegalArgumentException(this + " has no resolution of " + choice);
+ return resolution;
+ }
+
+ public @Override String toString() {
+ return "a resolution of " + choiceResolutions.size() + " choices";
+ }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/pagetemplates/engine/Resolver.java b/container-search/src/main/java/com/yahoo/search/pagetemplates/engine/Resolver.java
new file mode 100644
index 00000000000..4972b0e4689
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/pagetemplates/engine/Resolver.java
@@ -0,0 +1,114 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.pagetemplates.engine;
+
+import com.yahoo.component.AbstractComponent;
+import com.yahoo.component.ComponentId;
+import com.yahoo.search.Query;
+import com.yahoo.search.Result;
+import com.yahoo.search.pagetemplates.PageTemplate;
+import com.yahoo.search.pagetemplates.model.Choice;
+import com.yahoo.search.pagetemplates.model.MapChoice;
+import com.yahoo.search.pagetemplates.model.PageTemplateVisitor;
+
+/**
+ * Superclass of page template choice resolvers.
+ * <p>
+ * Subclasses overrides one of the two resolve methods to either resolve each choices individually
+ * or look at all choices at once.
+ * <p>
+ * All subclasses of this must be multithread safe. I.e multiple calls may be made
+ * to resolve at the same time from different threads.
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+public abstract class Resolver extends AbstractComponent {
+
+ public Resolver(String id) {
+ super(new ComponentId(id));
+ }
+
+ public Resolver(ComponentId id) {
+ super(id);
+ }
+
+ protected Resolver() {}
+
+ /**
+ * Override this to resolve choices. Before retuning this method <i>must</i> resolve the given choice
+ * between a set of page templates <i>and</i> all choices found recursively within the <i>chosen</i>
+ * page template. It is permissible but not required to add solutions also to choices present within those
+ * templates which are not chosen.
+ * <p>
+ * This default implementation creates a Resolution and calls
+ * <code>resolve(choice/mapChoice,query,result,resolution)</code> first on the given page template choice, then
+ * on each choice found in that temnplate. This provides a simple API to resolvers which make each choice
+ * independently.
+ *
+ * @param pageTemplate the choice of page templates to resolve - a choice containing singleton lists of PageTemplate elements
+ * @param query the query, from which information useful for correct resolution can be found
+ * @param result the result, from which further information useful for correct resolution can be found
+ * @return the resolution of the choices contained in the given page template
+ */
+ public Resolution resolve(Choice pageTemplate, Query query, Result result) {
+ Resolution resolution=new Resolution();
+ resolve(pageTemplate,query,result,resolution);
+ PageTemplate chosenPageTemplate=(PageTemplate)pageTemplate.get(resolution.getResolution(pageTemplate)).get(0);
+ ChoiceResolverVisitor choiceResolverVisitor=new ChoiceResolverVisitor(query,result,resolution);
+ chosenPageTemplate.accept(choiceResolverVisitor);
+ return choiceResolverVisitor.getResolution();
+ }
+
+ /**
+ * Override this to resolve <i>each</i> choice independently.
+ * This default implementation does nothing.
+ *
+ * @param choice the choice to resolve
+ * @param query the query for which this should be resolved, typically used to extract features
+ * @param result the result for which this should be resolved, typically used to extract features
+ * @param resolution the set of resolutions made so far, to which this should be added:
+ * <code>resolution.addChoiceResolution(choice,chosenAlternativeIndex)</code>
+ */
+ public void resolve(Choice choice,Query query,Result result,Resolution resolution) {
+ }
+
+ /**
+ * Override this to resolve <i>each</i> map choice independently.
+ * This default implementation does nothing.
+ *
+ * @param choice the choice to resolve
+ * @param query the query for which this should be resolved, typically used to extract features
+ * @param result the result for which this should be resolved, typically used to extract features
+ * @param resolution the set of resolutions made so far, to which this should be added:
+ * <code>resolution.addMapChoiceResolution(choice,chosenMapping)</code>
+ */
+ public void resolve(MapChoice choice,Query query,Result result,Resolution resolution) {
+ }
+
+ private class ChoiceResolverVisitor extends PageTemplateVisitor {
+
+ private Resolution resolution;
+
+ private Query query;
+
+ private Result result;
+
+ public ChoiceResolverVisitor(Query query,Result result,Resolution resolution) {
+ this.query=query;
+ this.result=result;
+ this.resolution=resolution;
+ }
+
+ public @Override void visit(Choice choice) {
+ if (choice.alternatives().size()<2) return; // No choice...
+ resolve(choice,query,result,resolution);
+ }
+
+ public @Override void visit(MapChoice choice) {
+ resolve(choice,query,result,resolution);
+ }
+
+ public Resolution getResolution() { return resolution; }
+
+ }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/pagetemplates/engine/SourceOrderComparator.java b/container-search/src/main/java/com/yahoo/search/pagetemplates/engine/SourceOrderComparator.java
new file mode 100644
index 00000000000..b4cd01f0c36
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/pagetemplates/engine/SourceOrderComparator.java
@@ -0,0 +1,57 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.pagetemplates.engine;
+
+import com.yahoo.search.result.ChainableComparator;
+import com.yahoo.search.result.Hit;
+
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+class SourceOrderComparator extends ChainableComparator {
+
+ private final List<String> sourceOrder;
+
+ /**
+ * Creates a source order comparator, with no secondary
+ *
+ * @param sourceOrder the sort order of list names. This list gets owned by this and must not be modified
+ */
+ public SourceOrderComparator(List<String> sourceOrder) {
+ this(sourceOrder,null);
+ }
+
+ /**
+ * Creates a source order comparator, with an optional secondary comparator.
+ *
+ * @param sourceOrder the sort order of list names. This list gets owned by this and must not be modified
+ * @param secondaryComparator the comparator to use as secondary, or null to use the intrinsic hit order
+ */
+ public SourceOrderComparator(List<String> sourceOrder,Comparator<Hit> secondaryComparator) {
+ super(secondaryComparator);
+ this.sourceOrder=sourceOrder;
+ }
+
+ public @Override int compare(Hit h1,Hit h2) {
+ int primaryOrder=sourceOrderCompare(h1,h2);
+ if (primaryOrder!=0) return primaryOrder;
+
+ return super.compare(h1,h2);
+ }
+
+ private int sourceOrderCompare(Hit h1,Hit h2) {
+ String h1Source=h1.getSource();
+ String h2Source=h2.getSource();
+
+ if (h1Source==null && h2Source==null) return 0;
+ if (h1Source==null) return 1; // No source -> last
+ if (h2Source==null) return -1; // No source -> last
+
+ if (h1Source.equals(h2Source)) return 0;
+
+ return sourceOrder.indexOf(h1Source)-sourceOrder.indexOf(h2Source);
+ }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/pagetemplates/engine/package-info.java b/container-search/src/main/java/com/yahoo/search/pagetemplates/engine/package-info.java
new file mode 100644
index 00000000000..6628156cb33
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/pagetemplates/engine/package-info.java
@@ -0,0 +1,7 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+@PublicApi
+package com.yahoo.search.pagetemplates.engine;
+
+import com.yahoo.api.annotations.PublicApi;
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/container-search/src/main/java/com/yahoo/search/pagetemplates/engine/resolvers/DeterministicResolver.java b/container-search/src/main/java/com/yahoo/search/pagetemplates/engine/resolvers/DeterministicResolver.java
new file mode 100644
index 00000000000..32ed54a6775
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/pagetemplates/engine/resolvers/DeterministicResolver.java
@@ -0,0 +1,56 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.pagetemplates.engine.resolvers;
+
+import com.yahoo.search.Query;
+import com.yahoo.search.Result;
+import com.yahoo.search.pagetemplates.engine.Resolution;
+import com.yahoo.search.pagetemplates.engine.Resolver;
+import com.yahoo.search.pagetemplates.model.Choice;
+import com.yahoo.search.pagetemplates.model.MapChoice;
+import com.yahoo.search.pagetemplates.model.PageElement;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A resolver which
+ * <ul>
+ * <li>Always chooses the <i>last</i> alternative of any Choice
+ * <li>Always maps values to placeholders in the order they are listed in the map definition of any MapChoice
+ * </ul>
+ * This is useful for testing.
+ * <p>
+ * The id of this if <code>native.deterministic</code>
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+public class DeterministicResolver extends Resolver {
+ public static final String nativeId = "native.deterministic";
+
+ public DeterministicResolver() {}
+
+ protected DeterministicResolver(String id) {
+ super(id);
+ }
+
+ /** Chooses the last alternative of any choice */
+ @Override
+ public void resolve(Choice choice, Query query, Result result, Resolution resolution) {
+ resolution.addChoiceResolution(choice,choice.alternatives().size()-1);
+ }
+
+ /** Chooses a mapping which is always by the literal order given in the source template */
+ @Override
+ public void resolve(MapChoice choice,Query query,Result result,Resolution resolution) {
+ Map<String, List<PageElement>> mapping=new HashMap<>();
+ // Map 1-1 by order
+ List<String> placeholderIds=choice.placeholderIds();
+ List<List<PageElement>> valueList=choice.values();
+ int i=0;
+ for (String placeholderId : placeholderIds)
+ mapping.put(placeholderId,valueList.get(i++));
+ resolution.addMapChoiceResolution(choice,mapping);
+ }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/pagetemplates/engine/resolvers/RandomResolver.java b/container-search/src/main/java/com/yahoo/search/pagetemplates/engine/resolvers/RandomResolver.java
new file mode 100644
index 00000000000..5f06c66795d
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/pagetemplates/engine/resolvers/RandomResolver.java
@@ -0,0 +1,50 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.pagetemplates.engine.resolvers;
+
+import com.yahoo.search.Query;
+import com.yahoo.search.Result;
+import com.yahoo.search.pagetemplates.engine.Resolution;
+import com.yahoo.search.pagetemplates.engine.Resolver;
+import com.yahoo.search.pagetemplates.model.Choice;
+import com.yahoo.search.pagetemplates.model.MapChoice;
+import com.yahoo.search.pagetemplates.model.PageElement;
+
+import java.util.*;
+
+/**
+ * A resolver which makes all choices by random.
+ * The id of this is <code>native.random</code>.
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+public class RandomResolver extends Resolver {
+
+ public static final String nativeId = "native.random";
+
+ private Random random = new Random(System.currentTimeMillis()); // Use of this is multithread safe
+
+ public RandomResolver() {}
+
+ protected RandomResolver(String id) {
+ super(id);
+ }
+
+ /** Chooses the last alternative of any choice */
+ @Override
+ public void resolve(Choice choice, Query query, Result result, Resolution resolution) {
+ resolution.addChoiceResolution(choice,random.nextInt(choice.alternatives().size()));
+ }
+
+ /** Chooses a mapping which is always by the literal order given in the source template */
+ @Override
+ public void resolve(MapChoice choice,Query query,Result result,Resolution resolution) {
+ Map<String, List<PageElement>> mapping=new HashMap<>();
+ // Draw a random element from the value list on each iteration and assign it to a placeholder
+ List<String> placeholderIds=choice.placeholderIds();
+ List<List<PageElement>> valueList=new ArrayList<>(choice.values());
+ for (String placeholderId : placeholderIds)
+ mapping.put(placeholderId,valueList.remove(random.nextInt(valueList.size())));
+ resolution.addMapChoiceResolution(choice,mapping);
+ }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/pagetemplates/engine/resolvers/ResolverRegistry.java b/container-search/src/main/java/com/yahoo/search/pagetemplates/engine/resolvers/ResolverRegistry.java
new file mode 100644
index 00000000000..0bbbec655bd
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/pagetemplates/engine/resolvers/ResolverRegistry.java
@@ -0,0 +1,59 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.pagetemplates.engine.resolvers;
+
+import com.google.inject.Inject;
+import com.yahoo.component.AbstractComponent;
+import com.yahoo.component.ComponentId;
+import com.yahoo.component.provider.ComponentRegistry;
+import com.yahoo.search.pagetemplates.engine.Resolver;
+
+import java.util.List;
+import java.util.logging.Logger;
+
+/**
+ * A registry of available resolver components
+ *
+ * @author bratseth
+ */
+public class ResolverRegistry extends ComponentRegistry<Resolver> {
+
+ private final Resolver defaultResolver;
+
+ public ResolverRegistry(List<Resolver> resolvers) {
+ addBuiltInResolvers();
+ for (Resolver component : resolvers)
+ registerResolver(component);
+ defaultResolver = decideDefaultResolver();
+ freeze();
+ }
+
+ private void addBuiltInResolvers() {
+ registerResolver(createNativeDeterministicResolver());
+ registerResolver(createNativeRandomResolver());
+ }
+
+ private Resolver decideDefaultResolver() {
+ Resolver defaultResolver = getComponent("default");
+ if (defaultResolver != null) return defaultResolver;
+ return getComponent("native.random");
+ }
+
+ private Resolver createNativeRandomResolver() {
+ RandomResolver resolver = new RandomResolver();
+ resolver.initId(ComponentId.fromString(RandomResolver.nativeId));
+ return resolver;
+ }
+
+ private DeterministicResolver createNativeDeterministicResolver() {
+ DeterministicResolver resolver = new DeterministicResolver();
+ resolver.initId(ComponentId.fromString(DeterministicResolver.nativeId));
+ return resolver;
+ }
+
+ private void registerResolver(Resolver resolver) {
+ super.register(resolver.getId(), resolver);
+ }
+
+ public Resolver defaultResolver() { return defaultResolver; }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/pagetemplates/engine/resolvers/package-info.java b/container-search/src/main/java/com/yahoo/search/pagetemplates/engine/resolvers/package-info.java
new file mode 100644
index 00000000000..c1e3f218480
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/pagetemplates/engine/resolvers/package-info.java
@@ -0,0 +1,7 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+@PublicApi
+package com.yahoo.search.pagetemplates.engine.resolvers;
+
+import com.yahoo.api.annotations.PublicApi;
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/container-search/src/main/java/com/yahoo/search/pagetemplates/model/AbstractChoice.java b/container-search/src/main/java/com/yahoo/search/pagetemplates/model/AbstractChoice.java
new file mode 100644
index 00000000000..069598b2e02
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/pagetemplates/model/AbstractChoice.java
@@ -0,0 +1,31 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.pagetemplates.model;
+
+import com.yahoo.component.provider.FreezableClass;
+
+/**
+ * Abstract superclass of various kinds of choices.
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+public abstract class AbstractChoice extends FreezableClass implements PageElement {
+
+ private String method;
+
+ /**
+ * Returns the choice method to use - a string interpreted by the resolver in use,
+ * or null to use any available method
+ */
+ public String getMethod() { return method; }
+
+ public void setMethod(String method) {
+ ensureNotFrozen();
+ this.method=method;
+ }
+
+ // TODO: is this really choices between classes in general, or e.g. subclasses of Section?
+ /** Returns true if this choice is (partially or completely) a choice between the given type */
+ @SuppressWarnings("rawtypes")
+ public abstract boolean isChoiceBetween(Class pageTemplateModelClass);
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/pagetemplates/model/Choice.java b/container-search/src/main/java/com/yahoo/search/pagetemplates/model/Choice.java
new file mode 100644
index 00000000000..a1932012236
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/pagetemplates/model/Choice.java
@@ -0,0 +1,114 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.pagetemplates.model;
+
+import java.util.*;
+
+/**
+ * A choice between some alternative lists of page elements.
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+public final class Choice extends AbstractChoice {
+
+ private List<List<PageElement>> alternatives=new ArrayList<>(3);
+
+ /** Creates an empty choice */
+ public Choice() { }
+
+ /** Creates a choice having a single alternative having a single page element */
+ public static Choice createSingleton(PageElement singletonAlternative) {
+ Choice choice=new Choice();
+ choice.alternatives().add(createSingletonList(singletonAlternative));
+ return choice;
+ }
+
+ /** Creates a choice in which each alternative consists of a single element */
+ public static Choice createSingletons(List<PageElement> alternatives) {
+ Choice choice=new Choice();
+ for (PageElement alternative : alternatives)
+ choice.alternatives().add(createSingletonList(alternative));
+ return choice;
+ }
+
+ private static List<PageElement> createSingletonList(PageElement member) {
+ List<PageElement> list=new ArrayList<>();
+ list.add(member);
+ return list;
+ }
+
+ /**
+ * Creates a choice between some alternatives. This method takes a copy of the given lists.
+ */
+ public Choice(List<List<PageElement>> alternatives) {
+ for (List<PageElement> alternative : alternatives)
+ this.alternatives.add(new ArrayList<>(alternative));
+ }
+
+ /**
+ * Returns the alternatives of this as a live reference to the alternatives of this.
+ * The list and elements may be modified unless this is frozen. This is never null.
+ */
+ public List<List<PageElement>> alternatives() { return alternatives; }
+
+ /** Convenience shorthand of <code>return alternatives().get(index)</code> */
+ public List<PageElement> get(int index) {
+ return alternatives.get(index);
+ }
+
+ /** Convenience shorthand for <code>if (alternative!=null) alternatives().add(alternative)</code> */
+ public void add(List<PageElement> alternative) {
+ if (alternative!=null)
+ alternatives.add(new ArrayList<>(alternative));
+ }
+
+ /** Returns true only if there are no alternatives in this */
+ public boolean isEmpty() { return alternatives.size()==0; }
+
+ /** Answers true if this is either a choice between the given class, or between Lists of the given class */
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ @Override
+ public boolean isChoiceBetween(Class pageTemplateModelElementClass) {
+ List firstNonEmpty=null;
+ for (List<PageElement> value : alternatives) {
+ if (pageTemplateModelElementClass.isAssignableFrom(value.getClass())) return true;
+ if (value instanceof List) {
+ List listValue=(List)value;
+ if (listValue.size()>0)
+ firstNonEmpty=listValue;
+ }
+ }
+ if (firstNonEmpty==null) return false;
+ return (pageTemplateModelElementClass.isAssignableFrom(firstNonEmpty.get(0).getClass()));
+ }
+
+ @Override
+ public void freeze() {
+ if (isFrozen()) return;
+ super.freeze();
+ for (ListIterator<List<PageElement>> i=alternatives.listIterator(); i.hasNext(); ) {
+ List<PageElement> alternative=i.next();
+ for (PageElement alternativeElement : alternative)
+ alternativeElement.freeze();
+ i.set(Collections.unmodifiableList(alternative));
+ }
+ alternatives= Collections.unmodifiableList(alternatives);
+ }
+
+ /** Accepts a visitor to this structure */
+ @Override
+ public void accept(PageTemplateVisitor visitor) {
+ visitor.visit(this);
+ for (List<PageElement> alternative : alternatives) {
+ for (PageElement alternativeElement : alternative)
+ alternativeElement.accept(visitor);
+ }
+ }
+
+ @Override
+ public String toString() {
+ if (alternatives.isEmpty()) return "(empty choice)";
+ if (alternatives.size()==1) return alternatives.get(0).toString();
+ return "a choice between " + alternatives;
+ }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/pagetemplates/model/Layout.java b/container-search/src/main/java/com/yahoo/search/pagetemplates/model/Layout.java
new file mode 100644
index 00000000000..f8e00b78787
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/pagetemplates/model/Layout.java
@@ -0,0 +1,50 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.pagetemplates.model;
+
+/**
+ * The layout of a section
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+// This is not made an enum, to allow the value set to be extendible.
+// It is not explicitly made immutable
+// to enable adding of internal state later (esp. parameters).
+// If this becomes mutable, the creation scheme must be changed
+// such that each fromString returns a unique instance, and
+// the name must become a (immutable) type.
+public class Layout {
+
+ /** The built in "column" layout */
+ public static final Layout column=new Layout("column");
+ /** The built in "row" layout */
+ public static final Layout row=new Layout("row");
+
+ private String name;
+
+ public Layout(String name) {
+ this.name=name;
+ }
+
+ public String getName() { return name; }
+
+ public @Override int hashCode() { return name.hashCode(); }
+
+ public @Override boolean equals(Object o) {
+ if (o==this) return true;
+ if (! (o instanceof Layout)) return false;
+ Layout other=(Layout)o;
+ return this.name.equals(other.name);
+ }
+
+ /** Returns a layout having this string as name, or null if the given string is null or empty */
+ public static Layout fromString(String layout) {
+ //if (layout==null) return null;
+ //if (layout)
+ if (layout.equals("column")) return column;
+ if (layout.equals("row")) return row;
+ return new Layout(layout);
+ }
+
+ public @Override String toString() { return "layout '" + name + "'"; }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/pagetemplates/model/MapChoice.java b/container-search/src/main/java/com/yahoo/search/pagetemplates/model/MapChoice.java
new file mode 100644
index 00000000000..33c3bba9a77
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/pagetemplates/model/MapChoice.java
@@ -0,0 +1,69 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.pagetemplates.model;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A choice between different possible mapping functions of a set of values to a set of placeholder ids.
+ * A <i>resolution</i> of this choice consists of choosing a unique value for each placeholder id
+ * (hence a map choice is valid iff there are at least as many values as placeholder ids).
+ * <p>
+ * Each unique set of mappings (pairs) from values to placeholder ids is a separate possible
+ * alternative of this choice. The alternatives are not listed explicitly but are generated as needed.
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+public class MapChoice extends AbstractChoice {
+
+ private List<String> placeholderIds=new ArrayList<>();
+
+ private List<List<PageElement>> values=new ArrayList<>();
+
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ @Override
+ public boolean isChoiceBetween(Class pageTemplateModelElementClass) {
+ List<PageElement> firstNonEmpty=null;
+ for (List<PageElement> value : values)
+ if (value.size()>0)
+ firstNonEmpty=value;
+ if (firstNonEmpty==null) return false;
+ return (pageTemplateModelElementClass.isAssignableFrom(firstNonEmpty.get(0).getClass()));
+ }
+
+ /**
+ * Returns the placeholder ids (the "to" of the mapping) of this as a live reference which can be modified unless
+ * this is frozen.
+ */
+ public List<String> placeholderIds() { return placeholderIds; }
+
+ /**
+ * Returns the values (the "from" of the mapping) of this as a live reference which can be modified unless
+ * this is frozen. Note that each single choice of values within this is also a list of values. This is
+ * the inner list.
+ */
+ public List<List<PageElement>> values() { return values; }
+
+ @Override
+ public void freeze() {
+ if (isFrozen()) return;
+ super.freeze();
+ placeholderIds=Collections.unmodifiableList(placeholderIds);
+ values=Collections.unmodifiableList(values);
+ }
+
+ /** Accepts a visitor to this structure */
+ public @Override void accept(PageTemplateVisitor visitor) {
+ visitor.visit(this);
+ for (List<PageElement> valueEntry : values)
+ for (PageElement value : valueEntry)
+ value.accept(visitor);
+ }
+
+ @Override
+ public String toString() {
+ return "mapping to placeholders " + placeholderIds;
+ }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/pagetemplates/model/PageElement.java b/container-search/src/main/java/com/yahoo/search/pagetemplates/model/PageElement.java
new file mode 100644
index 00000000000..fba58f069ec
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/pagetemplates/model/PageElement.java
@@ -0,0 +1,16 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.pagetemplates.model;
+
+import com.yahoo.component.provider.Freezable;
+
+/**
+ * Implemented by all page template model classes
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+public interface PageElement extends Freezable {
+
+ /** Accepts a visitor to this structure */
+ public void accept(PageTemplateVisitor visitor);
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/pagetemplates/model/PageTemplateVisitor.java b/container-search/src/main/java/com/yahoo/search/pagetemplates/model/PageTemplateVisitor.java
new file mode 100644
index 00000000000..d7ebd3d1169
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/pagetemplates/model/PageTemplateVisitor.java
@@ -0,0 +1,41 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.pagetemplates.model;
+
+import com.yahoo.search.pagetemplates.PageTemplate;
+
+/**
+ * Superclass of visitors over the page template object structure
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+public class PageTemplateVisitor {
+
+ /** Called each time a page template is encountered. This default implementation does nothing */
+ public void visit(PageTemplate pageTemplate) {
+ }
+
+ /** Called each time a source or source placeholder is encountered. This default implementation does nothing */
+ public void visit(Source source) {
+ }
+
+ /** Called each time a section or section placeholder is encountered. This default implementation does nothing */
+ public void visit(Section section) {
+ }
+
+ /** Called each time a renderer is encountered. This default implementation does nothing */
+ public void visit(Renderer renderer) {
+ }
+
+ /** Called each time a choice is encountered. This default implementation does nothing */
+ public void visit(Choice choice) {
+ }
+
+ /** Called each time a map choice is encountered. This default implementation does nothing */
+ public void visit(MapChoice choice) {
+ }
+
+ /** Called each time a placeholder is encountered. This default implementation does nothing */
+ public void visit(Placeholder placeholder) {
+ }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/pagetemplates/model/Placeholder.java b/container-search/src/main/java/com/yahoo/search/pagetemplates/model/Placeholder.java
new file mode 100644
index 00000000000..cf7a85fc779
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/pagetemplates/model/Placeholder.java
@@ -0,0 +1,49 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.pagetemplates.model;
+
+/**
+ * A source placeholder is replaced with a list of source instances at evaluation time.
+ * Source placeholders may not have any content themselves - attempting to call any setter on this
+ * results in a IllegalStateException.
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+public class Placeholder implements PageElement {
+
+ private String id;
+
+ private MapChoice valueContainer=null;
+
+ /** Creates a source placeholder with an id. */
+ public Placeholder(String id) {
+ this.id=id;
+ }
+
+ public String getId() { return id; }
+
+ /** Returns the element which contains the value(s) of this placeholder. Never null. */
+ public MapChoice getValueContainer() { return valueContainer; }
+
+ public void setValueContainer(MapChoice valueContainer) { this.valueContainer=valueContainer; }
+
+ public @Override void freeze() {}
+
+ /** Accepts a visitor to this structure */
+ public @Override void accept(PageTemplateVisitor visitor) {
+ visitor.visit(this);
+ }
+
+ public @Override String toString() {
+ return "source placeholder '" + id + "'";
+ }
+
+ /**
+ * This method always returns false, is a Placeholder always is mutable.
+ * (freeze() is a NOOP.)
+ */
+ @Override
+ public boolean isFrozen() {
+ return false;
+ }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/pagetemplates/model/Renderer.java b/container-search/src/main/java/com/yahoo/search/pagetemplates/model/Renderer.java
new file mode 100644
index 00000000000..4564ceeef3c
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/pagetemplates/model/Renderer.java
@@ -0,0 +1,88 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.pagetemplates.model;
+
+import com.yahoo.component.provider.FreezableClass;
+import com.yahoo.protect.Validator;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * A description of a way to present data items from a source.
+ * All data items has a default renderer. This can be overridden or parametrized by
+ * an explicit renderer.
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+public final class Renderer extends FreezableClass implements PageElement {
+
+ private String name;
+
+ private String rendererFor;
+
+ private Map<String,String> parameters =new LinkedHashMap<>();
+
+ public Renderer(String name) {
+ setName(name);
+ }
+
+ /**
+ * Returns the name of this renderer (never null).
+ * The name should be recognized by the system receiving results for rendering
+ */
+ public String getName() { return name; }
+
+ public final void setName(String name) {
+ ensureNotFrozen();
+ Validator.ensureNotNull("renderer name",name);
+ this.name=name;
+ }
+
+ /**
+ * Returns the name of the kind of data this is a renderer for.
+ * This is used to allow frontends to dispatch the right data items (hits) to
+ * the right renderer in the case where the data consists of a heterogeneous list.
+ * <p>
+ * This is null if this is a renderer for a whole section, or if this is a renderer
+ * for all kinds of data from a particular source <i>and</i> this is not frozen.
+ * <p>
+ * Otherwise, it is either the name of the source this is the renderer for,
+ * <i>or</i> the renderer for all data items having this name as a <i>type</i>.
+ * <p>
+ * This, a (frontend) dispatcher of data to renderers should for each data item:
+ * <ul>
+ * <li>use the renderer having the same name as any <code>type</code> name set of the data item
+ * <li>if no such renderer, use the renderer having <code>rendererFor</code> equal to the data items <code>source</code>
+ * <li>if no such renderer, use a default renderer
+ * </ul>
+ */
+ public String getRendererFor() { return rendererFor; }
+
+ public void setRendererFor(String rendererFor) {
+ ensureNotFrozen();
+ this.rendererFor=rendererFor;
+ }
+
+ /**
+ * Returns the parameters of this renderer as a live reference (never null).
+ * The parameters will be passed to the renderer with each result
+ */
+ public Map<String,String> parameters() { return parameters; }
+
+ public @Override void freeze() {
+ if (isFrozen()) return;
+ super.freeze();
+ parameters = Collections.unmodifiableMap(parameters);
+ }
+
+ /** Accepts a visitor to this structure */
+ public @Override void accept(PageTemplateVisitor visitor) {
+ visitor.visit(this);
+ }
+ public @Override String toString() {
+ return "renderer '" + name + "'";
+ }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/pagetemplates/model/Section.java b/container-search/src/main/java/com/yahoo/search/pagetemplates/model/Section.java
new file mode 100644
index 00000000000..0a980419853
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/pagetemplates/model/Section.java
@@ -0,0 +1,177 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.pagetemplates.model;
+
+import com.yahoo.component.provider.FreezableClass;
+import com.yahoo.search.query.Sorting;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * An element of a page template corresponding to a physical area of the layout of the final physical page.
+ * Pages are freezable - once frozen calling a setter will cause an IllegalStateException, and returned
+ * live collection references are unmodifiable
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+public class Section extends FreezableClass implements PageElement {
+
+ private final String id;
+
+ private Layout layout=Layout.column;
+
+ private String region;
+
+ /** The elements of this - sources, subsections etc. and/or choices of the same */
+ private List<PageElement> elements=new ArrayList<>();
+
+ /** Filtered versions of elements pre-calculated at freeze time */
+ private List<PageElement> sections, sources, renderers;
+
+ private int max=-1;
+
+ private int min=-1;
+
+ private Sorting order=null;
+
+ private static AtomicInteger nextId=new AtomicInteger();
+
+ public Section() {
+ this(null);
+ }
+
+ /** Creates a section with an id (or null if no id) */
+ public Section(String id) {
+ if (id==null || id.isEmpty())
+ this.id=String.valueOf("section_" + nextId.incrementAndGet());
+ else
+ this.id=id;
+ }
+
+ /** Returns a unique id of this section within the page. Used for referencing and identification. Never null. */
+ public String getId() { return id; }
+
+ /**
+ * Returns the layout identifier describing the kind of layout which should be used by the rendering engine to
+ * lay out the content of this section. This is never null. Default: "column".
+ */
+ public Layout getLayout() { return layout; }
+
+ /** Sets the layout. If the layout is set to null it will become Layout.column */
+ public void setLayout(Layout layout) {
+ ensureNotFrozen();
+ if (layout==null) layout=Layout.column;
+ this.layout=layout;
+ }
+
+ /**
+ * Returns the identifier telling the layout of the containing section where this section should be placed.
+ * Permissible values, and whether this is mandatory is determined by the particular layout identifier of the parent.
+ * May be null if a placement is not required by the containing layout, or if this is the top-level section.
+ * This is null by default.
+ */
+ public String getRegion() { return region; }
+
+ public void setRegion(String region) {
+ ensureNotFrozen();
+ this.region=region;
+ }
+
+ /**
+ * Returns the elements of this - sources, subsections and presentations and/or choices of these,
+ * as a live reference which can be modified to change the content of this (unless this is frozen).
+ * <p>
+ * All elements are kept in a single list to allow multiple elements of each type to be nested within separate
+ * choices, and to maintain the internal order of elements of various types, which is sometimes significant.
+ * To extract a certain kind of elements (say, sources), the element list must be traversed to collect
+ * all source elements as well as all choices of sources.
+ * <p>
+ * This list is never null but may be empty.
+ */
+ public List<PageElement> elements() { return elements; }
+
+ /**
+ * Convenience method which returns the elements <b>and choices</b> of the given type in elements as a
+ * read-only list. Not that as this returns both concrete elements and choices betwen them,
+ * the list element cannot be case to the given class - this must be used in conjunction
+ * with a resolve which contains the resolution to the choices.
+ *
+ * @param pageTemplateModelElementClass type to returns elements and choices of, a subtype of PageElement
+ */
+ public List<PageElement> elements(@SuppressWarnings("rawtypes") Class pageTemplateModelElementClass) {
+ if (isFrozen()) { // Use precalculated lists
+ if (pageTemplateModelElementClass==Section.class)
+ return sections;
+ else if (pageTemplateModelElementClass==Source.class)
+ return sources;
+ else if (pageTemplateModelElementClass==Renderer.class)
+ return renderers;
+ }
+ return createElementList(pageTemplateModelElementClass);
+ }
+
+ @SuppressWarnings("unchecked")
+ private List<PageElement> createElementList(@SuppressWarnings("rawtypes") Class pageTemplateModelElementClass) {
+ List<PageElement> filteredElements=new ArrayList<>();
+ for (PageElement element : elements) {
+ if (pageTemplateModelElementClass.isAssignableFrom(element.getClass()))
+ filteredElements.add(element);
+ else if (element instanceof AbstractChoice)
+ if (((AbstractChoice)element).isChoiceBetween(pageTemplateModelElementClass))
+ filteredElements.add(element);
+ }
+ return Collections.unmodifiableList(filteredElements);
+ }
+
+ /** Returns the choice of ways to sort immediate children in this, or empty meaning sort by default order (relevance) */
+ public Sorting getOrder() { return order; }
+
+ public void setOrder(Sorting order) {
+ ensureNotFrozen();
+ this.order=order;
+ }
+
+ /** Returns max number of (immediate) elements/sections permissible within this, -1 means unrestricted. Default: -1. */
+ public int getMax() { return max; }
+
+ public void setMax(int max) {
+ ensureNotFrozen();
+ this.max=max;
+ }
+
+ /** Returns min number of (immediate) elements/sections desired within this, -1 means unrestricted. Default: -1. */
+ public int getMin() { return min; }
+
+ public void setMin(int min) {
+ ensureNotFrozen();
+ this.min=min;
+ }
+
+ public @Override void freeze() {
+ if (isFrozen()) return;
+
+ for (PageElement element : elements)
+ element.freeze();
+ elements=Collections.unmodifiableList(elements);
+ sections=createElementList(Section.class);
+ sources=createElementList(Source.class);
+ renderers=createElementList(Renderer.class);
+
+ super.freeze();
+ }
+
+ /** Accepts a visitor to this structure */
+ public @Override void accept(PageTemplateVisitor visitor) {
+ visitor.visit(this);
+ for (PageElement element : elements)
+ element.accept(visitor);
+ }
+
+ public @Override String toString() {
+ if (id==null || id.isEmpty()) return "a section";
+ return "section '" + id + "'";
+ }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/pagetemplates/model/Source.java b/container-search/src/main/java/com/yahoo/search/pagetemplates/model/Source.java
new file mode 100644
index 00000000000..91c403eae84
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/pagetemplates/model/Source.java
@@ -0,0 +1,137 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.pagetemplates.model;
+
+import com.yahoo.component.provider.FreezableClass;
+import com.yahoo.protect.Validator;
+
+import java.util.*;
+
+/**
+ * A source mentioned in a page template.
+ * <p>
+ * Two sources are equal if they have the same name and parameters.
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+public class Source extends FreezableClass implements PageElement {
+
+ /** The "any" source - used to mark that any source is acceptable here */
+ public static final Source any=new Source("*",true);
+
+ /** The obligatory name of a source */
+ private String name;
+
+ private List<PageElement> renderers =new ArrayList<>();
+
+ private Map<String,String> parameters =new LinkedHashMap<>();
+
+ private String url;
+
+ /** The precalculated hashCode of this object, or 0 if this is not frozen */
+ private int hashCode=0;
+
+ public Source(String name) {
+ this(name,false);
+ }
+
+ /** Creates a source and optionally immediately freezes it */
+ private Source(String name,boolean freeze) {
+ setName(name);
+ if (freeze)
+ freeze();
+ }
+
+ /** Returns the name of this source (never null) */
+ public String getName() { return name; }
+
+ public final void setName(String name) {
+ ensureNotFrozen();
+ Validator.ensureNotNull("Source name",name);
+ this.name=name;
+ }
+
+ /** Returns the url of this source or null if none */
+ public String getUrl() { return url; }
+
+ /**
+ * Sets the url of this source. If a source has an url (i.e this returns non-null), the content of
+ * the url is <i>not</i> fetched - fetching is left to the frontend by exposing this url in the result.
+ */
+ public void setUrl(String url) {
+ ensureNotFrozen();
+ this.url=url;
+ }
+
+ /**
+ * Returns the renderers or choices of renderers to apply on individual items of this source
+ * <p>
+ * If this contains multiple renderers/choices, they are to be used on different types of hits returned by this source.
+ */
+ public List<PageElement> renderers() { return renderers; }
+
+ /**
+ * Returns the parameters of this source as a live reference (never null).
+ * The parameters will be passed to the provider getting source data.
+ */
+ public Map<String,String> parameters() { return parameters; }
+
+ public @Override void freeze() {
+ if (isFrozen()) return;
+ for (PageElement element : renderers) {
+ if (element instanceof Renderer) {
+ assignRendererForIfNotSet((Renderer)element);
+ }
+ else if (element instanceof Choice) {
+ for (List<PageElement> renderersAlternative : ((Choice)element).alternatives()) {
+ for (PageElement rendererElement : renderersAlternative) {
+ Renderer renderer=(Renderer)rendererElement;
+ if (renderer.getRendererFor()==null)
+ renderer.setRendererFor(name);
+ }
+ }
+ }
+ element.freeze();
+ }
+ parameters = Collections.unmodifiableMap(parameters);
+ hashCode=hashCode();
+ super.freeze();
+ }
+
+ private void assignRendererForIfNotSet(Renderer renderer) {
+ if (renderer.getRendererFor()==null)
+ renderer.setRendererFor(name);
+ }
+
+ /** Accepts a visitor to this structure */
+ public @Override void accept(PageTemplateVisitor visitor) {
+ visitor.visit(this);
+ for (PageElement renderer : renderers)
+ renderer.accept(visitor);
+ }
+
+ public @Override int hashCode() {
+ if (isFrozen()) return hashCode;
+ int hashCode=name.hashCode();
+ int i=0;
+ for (Map.Entry<String,String> parameter : parameters.entrySet())
+ hashCode+=i*17*parameter.getKey().hashCode()+i*31*parameter.getValue().hashCode();
+ return hashCode;
+ }
+
+ public @Override boolean equals(Object other) {
+ if (other==this) return true;
+ if (! (other instanceof Source)) return false;
+ Source otherSource=(Source)other;
+ if (! this.name.equals(otherSource.name)) return false;
+ if (this.parameters.size() != otherSource.parameters.size()) return false;
+ for (Map.Entry<String,String> thisParameter : this.parameters.entrySet())
+ if ( ! thisParameter.getValue().equals(otherSource.parameters.get(thisParameter.getKey())))
+ return false;
+ return true;
+ }
+
+ public @Override String toString() {
+ return "source '" + name + "'";
+ }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/pagetemplates/model/package-info.java b/container-search/src/main/java/com/yahoo/search/pagetemplates/model/package-info.java
new file mode 100644
index 00000000000..22a004d7555
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/pagetemplates/model/package-info.java
@@ -0,0 +1,7 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+@PublicApi
+package com.yahoo.search.pagetemplates.model;
+
+import com.yahoo.api.annotations.PublicApi;
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/container-search/src/main/java/com/yahoo/search/pagetemplates/package-info.java b/container-search/src/main/java/com/yahoo/search/pagetemplates/package-info.java
new file mode 100644
index 00000000000..0368351a6dc
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/pagetemplates/package-info.java
@@ -0,0 +1,7 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+@PublicApi
+package com.yahoo.search.pagetemplates;
+
+import com.yahoo.api.annotations.PublicApi;
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/container-search/src/main/java/com/yahoo/search/pagetemplates/result/SectionHitGroup.java b/container-search/src/main/java/com/yahoo/search/pagetemplates/result/SectionHitGroup.java
new file mode 100644
index 00000000000..00f6c6350fc
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/pagetemplates/result/SectionHitGroup.java
@@ -0,0 +1,52 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.search.pagetemplates.result;
+
+import com.yahoo.search.pagetemplates.model.Renderer;
+import com.yahoo.search.pagetemplates.model.Source;
+import com.yahoo.search.result.HitGroup;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A hit group corresponding to a section - contains some additional information
+ * in proper getters and setters which is used during rendering.
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+public class SectionHitGroup extends HitGroup {
+
+ private static final long serialVersionUID = -9048845836777953538L;
+ private List<Source> sources=new ArrayList<>(0);
+ private List<Renderer> renderers=new ArrayList<>(0);
+ private final String displayId;
+
+ private boolean leaf=false;
+
+ public SectionHitGroup(String id) {
+ super(id);
+ if (id.startsWith("section:section_"))
+ displayId=null; // Don't display section ids when not named explicitly
+ else
+ displayId=id;
+ types().add("section");
+ }
+
+ @Override
+ public String getDisplayId() { return displayId; }
+
+ /**
+ * Returns the live, modifiable list of sources which are not fetched by the framework but should
+ * instead be included in the result
+ */
+ public List<Source> sources() { return sources; }
+
+ /** Returns the live, modifiable list of renderers in this section */
+ public List<Renderer> renderers() { return renderers; }
+
+ /** Returns whether this is a leaf section containing no subsections */
+ public boolean isLeaf() { return leaf; }
+
+ public void setLeaf(boolean leaf) { this.leaf=leaf; }
+
+}
diff --git a/container-search/src/main/java/com/yahoo/search/pagetemplates/result/package-info.java b/container-search/src/main/java/com/yahoo/search/pagetemplates/result/package-info.java
new file mode 100644
index 00000000000..7d006aad551
--- /dev/null
+++ b/container-search/src/main/java/com/yahoo/search/pagetemplates/result/package-info.java
@@ -0,0 +1,7 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+@PublicApi
+package com.yahoo.search.pagetemplates.result;
+
+import com.yahoo.api.annotations.PublicApi;
+import com.yahoo.osgi.annotation.ExportPackage;