summaryrefslogtreecommitdiffstats
path: root/config-model/src/main/java/com/yahoo/searchdefinition/ApplicationBuilder.java
diff options
context:
space:
mode:
Diffstat (limited to 'config-model/src/main/java/com/yahoo/searchdefinition/ApplicationBuilder.java')
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/ApplicationBuilder.java536
1 files changed, 536 insertions, 0 deletions
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/ApplicationBuilder.java b/config-model/src/main/java/com/yahoo/searchdefinition/ApplicationBuilder.java
new file mode 100644
index 00000000000..f766e8fcf79
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/ApplicationBuilder.java
@@ -0,0 +1,536 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition;
+
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.config.application.api.FileRegistry;
+import com.yahoo.config.model.api.ModelContext;
+import com.yahoo.config.model.application.provider.BaseDeployLogger;
+import com.yahoo.config.model.application.provider.MockFileRegistry;
+import com.yahoo.config.model.deploy.TestProperties;
+import com.yahoo.config.model.test.MockApplicationPackage;
+import com.yahoo.document.DocumentTypeManager;
+import com.yahoo.io.IOUtils;
+import com.yahoo.io.reader.NamedReader;
+import com.yahoo.search.query.profile.QueryProfileRegistry;
+import com.yahoo.search.query.profile.config.QueryProfileXMLReader;
+import com.yahoo.searchdefinition.parser.ParseException;
+import com.yahoo.searchdefinition.parser.SDParser;
+import com.yahoo.searchdefinition.parser.SimpleCharStream;
+import com.yahoo.searchdefinition.parser.TokenMgrException;
+import com.yahoo.searchdefinition.processing.Processor;
+import com.yahoo.vespa.documentmodel.DocumentModel;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+import com.yahoo.yolean.Exceptions;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.Reader;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Application builder. Usage:
+ * 1) Add all schemas, using the addXXX() methods,
+ * 2) provide the available rank types and rank expressions, using the setRankXXX() methods,
+ * 3) invoke the {@link #build()} method
+ */
+public class ApplicationBuilder {
+
+ private final ApplicationPackage applicationPackage;
+ private final List<Schema> schemas = new ArrayList<>();
+ private final DocumentTypeManager documentTypeManager = new DocumentTypeManager();
+ private final RankProfileRegistry rankProfileRegistry;
+ private final QueryProfileRegistry queryProfileRegistry;
+ private final FileRegistry fileRegistry;
+ private final DeployLogger deployLogger;
+ private final ModelContext.Properties properties;
+ /** True to build the document aspect only, skipping instantiation of rank profiles */
+ private final boolean documentsOnly;
+
+ private Application application;
+
+ private final Set<Class<? extends Processor>> processorsToSkip = new HashSet<>();
+
+ /** For testing only */
+ public ApplicationBuilder() {
+ this(new RankProfileRegistry(), new QueryProfileRegistry());
+ }
+
+ /** For testing only */
+ public ApplicationBuilder(DeployLogger deployLogger) {
+ this(MockApplicationPackage.createEmpty(), deployLogger);
+ }
+
+ /** For testing only */
+ public ApplicationBuilder(DeployLogger deployLogger, RankProfileRegistry rankProfileRegistry) {
+ this(MockApplicationPackage.createEmpty(), deployLogger, rankProfileRegistry);
+ }
+
+ /** Used for generating documents for typed access to document fields in Java */
+ public ApplicationBuilder(boolean documentsOnly) {
+ this(MockApplicationPackage.createEmpty(), new MockFileRegistry(), new BaseDeployLogger(), new TestProperties(), new RankProfileRegistry(), new QueryProfileRegistry(), documentsOnly);
+ }
+
+ /** For testing only */
+ public ApplicationBuilder(ApplicationPackage app, DeployLogger deployLogger) {
+ this(app, new MockFileRegistry(), deployLogger, new TestProperties(), new RankProfileRegistry(), new QueryProfileRegistry());
+ }
+
+ /** For testing only */
+ public ApplicationBuilder(ApplicationPackage app, DeployLogger deployLogger, RankProfileRegistry rankProfileRegistry) {
+ this(app, new MockFileRegistry(), deployLogger, new TestProperties(), rankProfileRegistry, new QueryProfileRegistry());
+ }
+
+ /** For testing only */
+ public ApplicationBuilder(RankProfileRegistry rankProfileRegistry) {
+ this(rankProfileRegistry, new QueryProfileRegistry());
+ }
+
+ /** For testing only */
+ public ApplicationBuilder(RankProfileRegistry rankProfileRegistry, QueryProfileRegistry queryProfileRegistry) {
+ this(rankProfileRegistry, queryProfileRegistry, new TestProperties());
+ }
+
+ public ApplicationBuilder(RankProfileRegistry rankProfileRegistry, QueryProfileRegistry queryProfileRegistry, ModelContext.Properties properties) {
+ this(MockApplicationPackage.createEmpty(), new MockFileRegistry(), new BaseDeployLogger(), properties, rankProfileRegistry, queryProfileRegistry);
+ }
+
+ public ApplicationBuilder(ApplicationPackage app,
+ FileRegistry fileRegistry,
+ DeployLogger deployLogger,
+ ModelContext.Properties properties,
+ RankProfileRegistry rankProfileRegistry,
+ QueryProfileRegistry queryProfileRegistry) {
+ this(app, fileRegistry, deployLogger, properties, rankProfileRegistry, queryProfileRegistry, false);
+ }
+
+ private ApplicationBuilder(ApplicationPackage applicationPackage,
+ FileRegistry fileRegistry,
+ DeployLogger deployLogger,
+ ModelContext.Properties properties,
+ RankProfileRegistry rankProfileRegistry,
+ QueryProfileRegistry queryProfileRegistry,
+ boolean documentsOnly) {
+ this.applicationPackage = applicationPackage;
+ this.rankProfileRegistry = rankProfileRegistry;
+ this.queryProfileRegistry = queryProfileRegistry;
+ this.fileRegistry = fileRegistry;
+ this.deployLogger = deployLogger;
+ this.properties = properties;
+ this.documentsOnly = documentsOnly;
+ }
+
+ /**
+ * Import search definition.
+ *
+ * @param fileName the name of the file to import
+ * @return the name of the imported object
+ * @throws IOException thrown if the file can not be read for some reason
+ * @throws ParseException thrown if the file does not contain a valid search definition
+ */
+ public Schema addSchemaFile(String fileName) throws IOException, ParseException {
+ File file = new File(fileName);
+ return addSchema(IOUtils.readFile(file), file.getAbsoluteFile().getParent());
+ }
+
+ private Schema addSchemaFile(Path file) throws IOException, ParseException {
+ return addSchemaFile(file.toString());
+ }
+
+ public void importFromApplicationPackage() {
+ for (NamedReader reader : applicationPackage.getSchemas()) {
+ importFrom(reader);
+ }
+ }
+
+ /**
+ * Reads and parses the schema string provided by the given reader. Once all schemas have been
+ * imported, call {@link #build()}.
+ *
+ * @param reader the reader whose content to import
+ */
+ private void importFrom(NamedReader reader) {
+ try {
+ String schemaName = addSchema(IOUtils.readAll(reader), reader.getName()).getName();
+ String schemaFileName = stripSuffix(reader.getName(), ApplicationPackage.SD_NAME_SUFFIX);
+ if ( ! schemaFileName.equals(schemaName)) {
+ throw new IllegalArgumentException("The file containing schema '" + schemaName + "' must be named '" +
+ schemaName + ApplicationPackage.SD_NAME_SUFFIX + "', not " + reader.getName());
+ }
+ } catch (ParseException e) {
+ throw new IllegalArgumentException("Could not parse schema file '" + reader.getName() + "'", e);
+ } catch (IOException e) {
+ throw new IllegalArgumentException("Could not read schema file '" + reader.getName() + "'", e);
+ } finally {
+ closeIgnoreException(reader.getReader());
+ }
+ }
+
+ private static String stripSuffix(String readerName, String suffix) {
+ if ( ! readerName.endsWith(suffix))
+ throw new IllegalArgumentException("Schema '" + readerName + "' does not end with " + suffix);
+ return readerName.substring(0, readerName.length() - suffix.length());
+ }
+
+ /**
+ * Adds a schema to this.
+ *
+ * @param string the string to parse
+ * @return the schema
+ * @throws ParseException thrown if the file does not contain a valid search definition
+ */
+ public Schema addSchema(String string) throws ParseException {
+ return addSchema(string, null);
+ }
+
+ private Schema addSchema(String str, String schemaDir) throws ParseException {
+ SimpleCharStream stream = new SimpleCharStream(str);
+ try {
+ Schema schema = new SDParser(stream, applicationPackage, fileRegistry, deployLogger, properties,
+ rankProfileRegistry, documentsOnly)
+ .schema(documentTypeManager, schemaDir);
+ addSchemaFile(schema);
+ return schema;
+ } catch (TokenMgrException e) {
+ throw new ParseException("Unknown symbol: " + e.getMessage());
+ } catch (ParseException pe) {
+ throw new ParseException(stream.formatException(Exceptions.toMessageString(pe)));
+ }
+ }
+
+ /**
+ * Registers the given schema to the application to be built during {@link #build()}. A
+ * {@link Schema} object is considered to be "raw" if it has not already been processed. This is the case for most
+ * programmatically constructed schemas used in unit tests.
+ *
+ * @param schema the object to import
+ * @throws IllegalArgumentException if the given search object has already been processed
+ */
+ public void addSchemaFile(Schema schema) {
+ if (schema.getName() == null)
+ throw new IllegalArgumentException("Schema has no name");
+ schemas.add(schema);
+ }
+
+ /**
+ * Processes and finalizes the schemas of this.
+ * Only for testing.
+ *
+ * @throws IllegalStateException Thrown if this method has already been called.
+ */
+ public void build() {
+ build(true);
+ }
+
+ /**
+ * Processes and finalizes the schemas of this.
+ *
+ * @throws IllegalStateException thrown if this method has already been called
+ */
+ public void build(boolean validate) {
+ if (application != null) throw new IllegalStateException("Application already built");
+
+ application = new Application(applicationPackage,
+ schemas,
+ rankProfileRegistry,
+ new QueryProfiles(queryProfileRegistry, deployLogger),
+ properties,
+ documentsOnly,
+ validate,
+ processorsToSkip,
+ deployLogger);
+ }
+
+ /** Returns a modifiable set of processors we should skip for these schemas. Useful for testing. */
+ public Set<Class<? extends Processor>> processorsToSkip() { return processorsToSkip; }
+
+ /**
+ * Convenience method to call {@link #getSchema(String)} when there is only a single {@link Schema} object
+ * built. This method will never return null.
+ *
+ * @return the built object
+ * @throws IllegalStateException if there is not exactly one search.
+ */
+ public Schema getSchema() {
+ if (application == null) throw new IllegalStateException("Application not built");
+ if (application.schemas().size() != 1)
+ throw new IllegalStateException("This call only works if we have 1 schema. Schemas: " +
+ application.schemas().values());
+
+ return application.schemas().values().stream().findAny().get();
+ }
+
+ public DocumentModel getModel() { return application.documentModel(); }
+
+ /**
+ * Returns the built {@link Schema} object that has the given name. If the name is unknown, this method will simply
+ * return null.
+ *
+ * @param name the name of the schema to return,
+ * or null to return the only one or throw an exception if there are multiple to choose from
+ * @return the built object, or null if none with this name
+ * @throws IllegalStateException if {@link #build()} has not been called.
+ */
+ public Schema getSchema(String name) {
+ if (application == null) throw new IllegalStateException("Application not built");
+ if (name == null) return getSchema();
+ return application.schemas().get(name);
+ }
+
+ public Application application() { return application; }
+
+ /**
+ * Convenience method to return a list of all built {@link Schema} objects.
+ *
+ * @return the list of built searches
+ */
+ public List<Schema> getSchemaList() {
+ return new ArrayList<>(application.schemas().values());
+ }
+
+ /**
+ * Convenience factory method to import and build a {@link Schema} object from a string.
+ *
+ * @param sd the string to build from
+ * @return the built {@link ApplicationBuilder} object
+ * @throws ParseException thrown if there is a problem parsing the string
+ */
+ public static ApplicationBuilder createFromString(String sd) throws ParseException {
+ return createFromString(sd, new BaseDeployLogger());
+ }
+
+ public static ApplicationBuilder createFromString(String sd, DeployLogger logger) throws ParseException {
+ ApplicationBuilder builder = new ApplicationBuilder(logger);
+ builder.addSchema(sd);
+ builder.build(true);
+ return builder;
+ }
+
+ public static ApplicationBuilder createFromStrings(DeployLogger logger, String ... schemas) throws ParseException {
+ ApplicationBuilder builder = new ApplicationBuilder(logger);
+ for (var schema : schemas)
+ builder.addSchema(schema);
+ builder.build(true);
+ return builder;
+ }
+
+ /**
+ * Convenience factory method to import and build a {@link Schema} object from a file. Only for testing.
+ *
+ * @param fileName the file to build from
+ * @return the built {@link ApplicationBuilder} object
+ * @throws IOException if there was a problem reading the file.
+ * @throws ParseException if there was a problem parsing the file content.
+ */
+ public static ApplicationBuilder createFromFile(String fileName) throws IOException, ParseException {
+ return createFromFile(fileName, new BaseDeployLogger());
+ }
+
+ /**
+ * Convenience factory methdd to create a SearchBuilder from multiple SD files. Only for testing.
+ */
+ public static ApplicationBuilder createFromFiles(Collection<String> fileNames) throws IOException, ParseException {
+ return createFromFiles(fileNames, new BaseDeployLogger());
+ }
+
+ public static ApplicationBuilder createFromFile(String fileName, DeployLogger logger) throws IOException, ParseException {
+ return createFromFile(fileName, logger, new RankProfileRegistry(), new QueryProfileRegistry());
+ }
+
+ private static ApplicationBuilder createFromFiles(Collection<String> fileNames, DeployLogger logger) throws IOException, ParseException {
+ return createFromFiles(fileNames, new MockFileRegistry(), logger, new TestProperties(), new RankProfileRegistry(), new QueryProfileRegistry());
+ }
+
+ /**
+ * Convenience factory method to import and build a {@link Schema} object from a file.
+ *
+ * @param fileName the file to build from.
+ * @param deployLogger logger for deploy messages.
+ * @param rankProfileRegistry registry for rank profiles.
+ * @return the built {@link ApplicationBuilder} object.
+ * @throws IOException if there was a problem reading the file.
+ * @throws ParseException if there was a problem parsing the file content.
+ */
+ private static ApplicationBuilder createFromFile(String fileName,
+ DeployLogger deployLogger,
+ RankProfileRegistry rankProfileRegistry,
+ QueryProfileRegistry queryprofileRegistry)
+ throws IOException, ParseException {
+ return createFromFiles(Collections.singletonList(fileName), new MockFileRegistry(), deployLogger, new TestProperties(),
+ rankProfileRegistry, queryprofileRegistry);
+ }
+
+ /**
+ * Convenience factory methdd to create a SearchBuilder from multiple SD files..
+ */
+ private static ApplicationBuilder createFromFiles(Collection<String> fileNames,
+ FileRegistry fileRegistry,
+ DeployLogger deployLogger,
+ ModelContext.Properties properties,
+ RankProfileRegistry rankProfileRegistry,
+ QueryProfileRegistry queryprofileRegistry)
+ throws IOException, ParseException {
+ ApplicationBuilder builder = new ApplicationBuilder(MockApplicationPackage.createEmpty(),
+ fileRegistry,
+ deployLogger,
+ properties,
+ rankProfileRegistry,
+ queryprofileRegistry);
+ for (String fileName : fileNames) {
+ builder.addSchemaFile(fileName);
+ }
+ builder.build(true);
+ return builder;
+ }
+
+
+ public static ApplicationBuilder createFromDirectory(String dir, FileRegistry fileRegistry, DeployLogger logger, ModelContext.Properties properties) throws IOException, ParseException {
+ return createFromDirectory(dir, fileRegistry, logger, properties, new RankProfileRegistry());
+ }
+ public static ApplicationBuilder createFromDirectory(String dir,
+ FileRegistry fileRegistry,
+ DeployLogger logger,
+ ModelContext.Properties properties,
+ RankProfileRegistry rankProfileRegistry) throws IOException, ParseException {
+ return createFromDirectory(dir, fileRegistry, logger, properties, rankProfileRegistry, createQueryProfileRegistryFromDirectory(dir));
+ }
+ private static ApplicationBuilder createFromDirectory(String dir,
+ FileRegistry fileRegistry,
+ DeployLogger logger,
+ ModelContext.Properties properties,
+ RankProfileRegistry rankProfileRegistry,
+ QueryProfileRegistry queryProfileRegistry) throws IOException, ParseException {
+ return createFromDirectory(dir, MockApplicationPackage.fromSearchDefinitionAndRootDirectory(dir), fileRegistry, logger, properties,
+ rankProfileRegistry, queryProfileRegistry);
+ }
+
+ private static ApplicationBuilder createFromDirectory(String dir,
+ ApplicationPackage applicationPackage,
+ FileRegistry fileRegistry,
+ DeployLogger deployLogger,
+ ModelContext.Properties properties,
+ RankProfileRegistry rankProfileRegistry,
+ QueryProfileRegistry queryProfileRegistry) throws IOException, ParseException {
+ ApplicationBuilder builder = new ApplicationBuilder(applicationPackage,
+ fileRegistry,
+ deployLogger,
+ properties,
+ rankProfileRegistry,
+ queryProfileRegistry);
+ for (Iterator<Path> i = Files.list(new File(dir).toPath()).filter(p -> p.getFileName().toString().endsWith(".sd")).iterator(); i.hasNext(); ) {
+ builder.addSchemaFile(i.next());
+ }
+ builder.build(true);
+ return builder;
+ }
+
+ private static QueryProfileRegistry createQueryProfileRegistryFromDirectory(String dir) {
+ File queryProfilesDir = new File(dir, "query-profiles");
+ if ( ! queryProfilesDir.exists()) return new QueryProfileRegistry();
+ return new QueryProfileXMLReader().read(queryProfilesDir.toString());
+ }
+
+ // TODO: The build methods below just call the create methods above - remove
+
+ /**
+ * Convenience factory method to import and build a {@link Schema} object from a file. Only for testing.
+ *
+ * @param fileName the file to build from
+ * @return the built {@link Schema} object
+ * @throws IOException thrown if there was a problem reading the file
+ * @throws ParseException thrown if there was a problem parsing the file content
+ */
+ public static Schema buildFromFile(String fileName) throws IOException, ParseException {
+ return buildFromFile(fileName, new BaseDeployLogger(), new RankProfileRegistry(), new QueryProfileRegistry());
+ }
+
+ /**
+ * Convenience factory method to import and build a {@link Schema} object from a file.
+ *
+ * @param fileName the file to build from
+ * @param rankProfileRegistry registry for rank profiles
+ * @return the built {@link Schema} object
+ * @throws IOException thrown if there was a problem reading the file
+ * @throws ParseException thrown if there was a problem parsing the file content
+ */
+ public static Schema buildFromFile(String fileName,
+ RankProfileRegistry rankProfileRegistry,
+ QueryProfileRegistry queryProfileRegistry)
+ throws IOException, ParseException {
+ return buildFromFile(fileName, new BaseDeployLogger(), rankProfileRegistry, queryProfileRegistry);
+ }
+
+ /**
+ * Convenience factory method to import and build a {@link Schema} from a file.
+ *
+ * @param fileName the file to build from
+ * @param deployLogger logger for deploy messages
+ * @param rankProfileRegistry registry for rank profiles
+ * @return the built {@link Schema} object
+ * @throws IOException thrown if there was a problem reading the file
+ * @throws ParseException thrown if there was a problem parsing the file content
+ */
+ public static Schema buildFromFile(String fileName,
+ DeployLogger deployLogger,
+ RankProfileRegistry rankProfileRegistry,
+ QueryProfileRegistry queryProfileRegistry)
+ throws IOException, ParseException {
+ return createFromFile(fileName, deployLogger, rankProfileRegistry, queryProfileRegistry).getSchema();
+ }
+
+ /**
+ * Convenience factory method to import and build a {@link Schema} object from a raw object.
+ *
+ * @param rawSchema the raw object to build from
+ * @return the built {@link ApplicationBuilder} object
+ * @see #addSchemaFile(Schema)
+ */
+ public static ApplicationBuilder createFromRawSchema(Schema rawSchema,
+ RankProfileRegistry rankProfileRegistry,
+ QueryProfileRegistry queryProfileRegistry) {
+ ApplicationBuilder builder = new ApplicationBuilder(rankProfileRegistry, queryProfileRegistry);
+ builder.addSchemaFile(rawSchema);
+ builder.build();
+ return builder;
+ }
+
+ /**
+ * Convenience factory method to import and build a {@link Schema} object from a raw object.
+ *
+ * @param rawSchema the raw object to build from
+ * @return the built {@link Schema} object
+ * @see #addSchemaFile(Schema)
+ */
+ public static Schema buildFromRawSchema(Schema rawSchema,
+ RankProfileRegistry rankProfileRegistry,
+ QueryProfileRegistry queryProfileRegistry) {
+ return createFromRawSchema(rawSchema, rankProfileRegistry, queryProfileRegistry).getSchema();
+ }
+
+ public RankProfileRegistry getRankProfileRegistry() {
+ return rankProfileRegistry;
+ }
+
+ public QueryProfileRegistry getQueryProfileRegistry() {
+ return queryProfileRegistry;
+ }
+
+ public ModelContext.Properties getProperties() { return properties; }
+
+ public DeployLogger getDeployLogger() { return deployLogger; }
+
+ @SuppressWarnings("EmptyCatchBlock")
+ private static void closeIgnoreException(Reader reader) {
+ try {
+ reader.close();
+ } catch(Exception e) {}
+ }
+}