diff options
author | Jon Bratseth <bratseth@yahoo-inc.com> | 2016-06-15 23:09:44 +0200 |
---|---|---|
committer | Jon Bratseth <bratseth@yahoo-inc.com> | 2016-06-15 23:09:44 +0200 |
commit | 72231250ed81e10d66bfe70701e64fa5fe50f712 (patch) | |
tree | 2728bba1131a6f6e5bdf95afec7d7ff9358dac50 /container-search/src/main/java/com/yahoo/search/pagetemplates |
Publish
Diffstat (limited to 'container-search/src/main/java/com/yahoo/search/pagetemplates')
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<String> 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<PageTemplate> + * 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; |