diff options
author | Kristian Aune <kraune@yahoo-inc.com> | 2017-06-13 09:38:59 +0200 |
---|---|---|
committer | Kristian Aune <kraune@yahoo-inc.com> | 2017-06-13 09:38:59 +0200 |
commit | 11b568c2dbec5f9fa35ab8a0aee98dd485db5230 (patch) | |
tree | 57c97f04fb900f0b5d44e6f1a7e12112eea7371b /sample-apps | |
parent | 344730820bb8e6f23eb0784a0dd4b469657ab49e (diff) |
add HTTP API sample app
Diffstat (limited to 'sample-apps')
10 files changed, 572 insertions, 0 deletions
diff --git a/sample-apps/http-api-using-request-handlers-and-processors/README.md b/sample-apps/http-api-using-request-handlers-and-processors/README.md new file mode 100644 index 00000000000..570ebf62015 --- /dev/null +++ b/sample-apps/http-api-using-request-handlers-and-processors/README.md @@ -0,0 +1,4 @@ +# Vespa sample applications - Building a HTTP API using request handlers and processors + +Refer to the [http-api-tutorial.html](https://git.corp.yahoo.com/pages/vespa/documentation/documentation/jdisc/http-api-tutorial.html) for documentation + diff --git a/sample-apps/http-api-using-request-handlers-and-processors/pom.xml b/sample-apps/http-api-using-request-handlers-and-processors/pom.xml new file mode 100644 index 00000000000..7ae8549c385 --- /dev/null +++ b/sample-apps/http-api-using-request-handlers-and-processors/pom.xml @@ -0,0 +1,85 @@ +<?xml version="1.0"?> +<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. --> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 + http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <groupId>com.yahoo.demo</groupId> + <artifactId>sample-app</artifactId> + <version>1.0.1</version> + <packaging>container-plugin</packaging> <!-- Use Vespa packaging --> + + <properties> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + <test.hide>true</test.hide> + <vespa_version>6-SNAPSHOT</vespa_version> + </properties> + + <dependencies> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <version>4.11</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>com.yahoo.vespa</groupId> + <artifactId>application</artifactId> <!-- Is this needed? --> + <version>${vespa_version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>com.yahoo.vespa</groupId> + <artifactId>container-dev</artifactId> <!-- not container-dev --> + <version>${vespa_version}</version> + <scope>provided</scope> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-compiler-plugin</artifactId> + <version>3.6.1</version> + <configuration> + <optimize>true</optimize> + <showDeprecation>true</showDeprecation> + <showWarnings>true</showWarnings> + <source>1.8</source> + <target>1.8</target> + </configuration> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-surefire-plugin</artifactId> + <version>2.19.1</version> + <configuration> + <systemPropertyVariables> + <isMavenSurefirePlugin>true</isMavenSurefirePlugin> + </systemPropertyVariables> + <redirectTestOutputToFile>${test.hide}</redirectTestOutputToFile> + </configuration> + </plugin> + <plugin> + <groupId>com.yahoo.vespa</groupId> + <artifactId>vespa-application-maven-plugin</artifactId> <!-- Zip the application package --> + <version>${vespa_version}</version> + <executions> + <execution> + <goals> + <goal>packageApplication</goal> + </goals> + </execution> + </executions> + </plugin> + <plugin> + <groupId>com.yahoo.vespa</groupId> + <artifactId>bundle-plugin</artifactId> + <version>${vespa_version}</version> + <extensions>true</extensions> + </plugin> + </plugins> + </build> +</project> diff --git a/sample-apps/http-api-using-request-handlers-and-processors/src/main/application/services.xml b/sample-apps/http-api-using-request-handlers-and-processors/src/main/application/services.xml new file mode 100644 index 00000000000..add155c2279 --- /dev/null +++ b/sample-apps/http-api-using-request-handlers-and-processors/src/main/application/services.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8" ?> +<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. --> +<services version="1.0"> + <container id="default" version="1.0"> + <processing> + <chain id="default"> + <processor id="com.yahoo.demo.AnnotatingProcessor" bundle="demo"> + <config name="demo.demo"> + <demo> + <item> + <term>smurf</term> + </item> + </demo> + </config> + </processor> + <processor id="com.yahoo.demo.DataProcessor" bundle="demo" /> + </chain> + <renderer id="demo" class="com.yahoo.demo.DemoRenderer" bundle="demo" /> + </processing> + <handler id="com.yahoo.demo.DemoHandler" bundle="demo"> + <binding>http://*/demo</binding> + </handler> + <component id="com.yahoo.demo.DemoComponent" bundle="demo"/> + </container> +</services> diff --git a/sample-apps/http-api-using-request-handlers-and-processors/src/main/java/com/yahoo/demo/AnnotatingProcessor.java b/sample-apps/http-api-using-request-handlers-and-processors/src/main/java/com/yahoo/demo/AnnotatingProcessor.java new file mode 100644 index 00000000000..e241d59f5d7 --- /dev/null +++ b/sample-apps/http-api-using-request-handlers-and-processors/src/main/java/com/yahoo/demo/AnnotatingProcessor.java @@ -0,0 +1,97 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.demo; + +import com.google.common.base.Splitter; +import com.yahoo.processing.Processor; +import com.yahoo.processing.Request; +import com.yahoo.processing.Response; +import com.yahoo.processing.execution.Execution; +import com.yahoo.processing.request.CompoundName; +import com.yahoo.processing.request.ErrorMessage; +import com.yahoo.processing.response.DataList; +import com.yahoo.yolean.chain.Before; +import com.yahoo.yolean.chain.Provides; + +import java.util.ArrayList; +import java.util.List; + +/** + * A processor which processes the incoming query property "terms", then checks + * whether there are any smurfs, i.e. DemoData instances containing the string + * {@link DemoComponent#SMURF}, and adds errors if that is the case. + */ +@Provides(AnnotatingProcessor.DemoProperty.NAME) +@Before(DataProcessor.DemoData.NAME) +public class AnnotatingProcessor extends Processor { + + public static class DemoProperty { + + public static final String NAME = "demo.property"; + public static final CompoundName NAME_AS_COMPOUND = new CompoundName(NAME); + + private final List<String> terms = new ArrayList<>(); + + public void add(String term) { + terms.add(term); + } + + public List<String> terms() { + return terms; + } + } + + private final DemoConfig defaultTermSet; + + public final CompoundName TERMS = new CompoundName("terms"); + + private static final Splitter splitter = Splitter.on(' ').omitEmptyStrings(); + + public AnnotatingProcessor(DemoConfig defaultTermSet) { + this.defaultTermSet = defaultTermSet; + } + + @Override + public Response process(Request request, Execution execution) { + Response response; + List<?> d; + DemoProperty p = new DemoProperty(); + String terms = request.properties().getString(TERMS); + + if (terms != null) { + for (String s : splitter.split(terms)) { + p.add(s); + } + } else { + for (DemoConfig.Demo demo : defaultTermSet.demo()) { + p.add(demo.term()); + } + } + request.properties().set(DemoProperty.NAME_AS_COMPOUND, p); + response = execution.process(request); + d = response.data().asList(); + traverse(d, response.data().request().errors()); + return response; + } + + private boolean traverse(List<?> list, List<ErrorMessage> topLevelErrors) { + boolean smurfFound = false; + // traverse the tree in the response, and react to the known types + for (Object data : list) { + if (data instanceof DataList) { + smurfFound = traverse(((DataList<?>) data).asList(), + topLevelErrors); + } else if (data instanceof DataProcessor.DemoData) { + DataProcessor.DemoData content = (DataProcessor.DemoData) data; + if (DemoComponent.SMURF.equals(content.content())) { + topLevelErrors.add(new ErrorMessage("There's a smurf!")); + smurfFound = true; + } + } + if (smurfFound) { + break; + } + } + return smurfFound; + } + +}
\ No newline at end of file diff --git a/sample-apps/http-api-using-request-handlers-and-processors/src/main/java/com/yahoo/demo/DataProcessor.java b/sample-apps/http-api-using-request-handlers-and-processors/src/main/java/com/yahoo/demo/DataProcessor.java new file mode 100644 index 00000000000..915beb38f5e --- /dev/null +++ b/sample-apps/http-api-using-request-handlers-and-processors/src/main/java/com/yahoo/demo/DataProcessor.java @@ -0,0 +1,87 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.demo; + +import com.yahoo.component.provider.ListenableFreezableClass; +import com.yahoo.processing.Processor; +import com.yahoo.processing.Request; +import com.yahoo.processing.Response; +import com.yahoo.processing.execution.Execution; +import com.yahoo.processing.response.ArrayDataList; +import com.yahoo.processing.response.Data; +import com.yahoo.processing.response.DataList; +import com.yahoo.yolean.chain.After; +import com.yahoo.yolean.chain.Provides; + +/** + * A processor making a nested result sets of "normalized" strings from the + * request property {@link AnnotatingProcessor.DemoProperty#NAME}. + */ +@Provides(DataProcessor.DemoData.NAME) +@After(AnnotatingProcessor.DemoProperty.NAME) +public class DataProcessor extends Processor { + public static class DemoData extends ListenableFreezableClass implements Data { + public static final String NAME = "DemoData"; + + private final Request request; + private final String content; + + DemoData(Request request, String content) { + this.request = request; + this.content = content; + } + + @Override + public Request request() { + return request; + } + + public String content() { + return content; + } + + public String toString() { + return NAME + "(\"" + content + "\")"; + } + } + + private final DemoComponent termChecker; + + public DataProcessor(DemoComponent termChecker) { + this.termChecker = termChecker; + } + + @Override + public Response process(Request request, Execution execution) { + Response r = new Response(request); + @SuppressWarnings("unchecked") + DataList<Data> current = r.data(); + DataList<Data> previous = null; + String exampleProperty = request.properties().getString(DemoHandler.REQUEST_URI); + Object o = request.properties().get(AnnotatingProcessor.DemoProperty.NAME_AS_COMPOUND); + + + if (exampleProperty != null) { + current.add(new DemoData(request, exampleProperty)); + } + + if (o instanceof AnnotatingProcessor.DemoProperty) { + // create a nested result set with a level for each term + for (String s : ((AnnotatingProcessor.DemoProperty) o).terms()) { + String normalized = termChecker.normalize(s); + DemoData data = new DemoData(request, normalized); + + if (current == null) { + current = ArrayDataList.create(request); + } + current.add(data); + if (previous != null) { + previous.add(current); + } + previous = current; + current = null; + } + } + return r; + } + +}
\ No newline at end of file diff --git a/sample-apps/http-api-using-request-handlers-and-processors/src/main/java/com/yahoo/demo/DemoComponent.java b/sample-apps/http-api-using-request-handlers-and-processors/src/main/java/com/yahoo/demo/DemoComponent.java new file mode 100644 index 00000000000..455784525e1 --- /dev/null +++ b/sample-apps/http-api-using-request-handlers-and-processors/src/main/java/com/yahoo/demo/DemoComponent.java @@ -0,0 +1,47 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.demo; + +import com.yahoo.component.AbstractComponent; + +import java.text.Normalizer; +import java.util.HashSet; +import java.util.Random; +import java.util.Set; + +/** + * A shared component with an "expensive" constructor exposing a shared, + * thread-safe service. + */ +public class DemoComponent extends AbstractComponent { + public static final String SMURF = "smurf"; + + private final Set<Integer> illegalHashes; + + public DemoComponent() { + illegalHashes = new HashSet<Integer>(); + Random r = new Random(); + // generate up to 1e6 unique hashes + for (int i = 0; i < 1000 * 1000; ++i) { + illegalHashes.add(r.nextInt()); + } + } + + /** + * NFKC-normalize term, or replace it with "smurf" with a low probability. + * Will change choice for each run, but will be constant in a single run of + * the container. + * + * @param term + * term to normalize or replace with "smurf" + * @return NFKC-normalized term or "smurf" + */ + public String normalize(String term) { + String normalized = Normalizer.normalize(term, Normalizer.Form.NFKC); + if (illegalHashes.contains(normalized.hashCode())) { + return SMURF; + } else { + return normalized; + } + } + +} diff --git a/sample-apps/http-api-using-request-handlers-and-processors/src/main/java/com/yahoo/demo/DemoHandler.java b/sample-apps/http-api-using-request-handlers-and-processors/src/main/java/com/yahoo/demo/DemoHandler.java new file mode 100644 index 00000000000..b7ceab55609 --- /dev/null +++ b/sample-apps/http-api-using-request-handlers-and-processors/src/main/java/com/yahoo/demo/DemoHandler.java @@ -0,0 +1,76 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.demo; + +import com.google.inject.Inject; +import com.yahoo.container.jdisc.HttpRequest; +import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.container.jdisc.ThreadedHttpRequestHandler; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.processing.handler.ProcessingHandler; + +import java.util.concurrent.Executor; + +/** + * Annotate an incoming request with the URI string used to query from the + * network, and pass the request on to the processing handler. + */ +public class DemoHandler extends ThreadedHttpRequestHandler { + + /** The name used by the processing handler to choose output renderer. */ + private static final String FORMAT = "format"; + + /** The property name for the incoming URI as a string. */ + public static final String REQUEST_URI = "request.uri"; + + private final ProcessingHandler processingHandler; + + /** + * Constructor for use in injection. The requested objects are subclasses of + * component or have dedicated providers, so the container will know how to + * create this handler. + * + * @param executor + * threadpool, provided by the container + * @param processingHandler + * the processing handler, also automatically injected + */ + @Inject + public DemoHandler(Executor executor, ProcessingHandler processingHandler) { + super(executor, null, true); + this.processingHandler = processingHandler; + } + + @Override + public HttpResponse handle(HttpRequest request) { + // We have implemented #handle(HttpRequest, ContentChannel) to be + // able to use the ProcessingHandler, so this will never be called. + // An implementation is needed, though, as the method is abstract. + throw new UnsupportedOperationException("See #handle(HttpRequest, ContentChannel)"); + } + + @Override + public HttpResponse handle(HttpRequest request, ContentChannel channel) { + HttpRequest.Builder builder = + new HttpRequest.Builder(request).put(REQUEST_URI, request.getUri().toString()); + setFormat(builder, request); + return processingHandler.handle(builder.createDirectRequest(), + channel); + } + + /** + * Set the output format to the renderer with id = "demo" in services.xml if + * no explicit format parameter is present. This allows using e.g. the + * default processing renderer by adding <code>&format=default</code> to + * the HTTP request. + * + * @param builder + * the mutable builder instance used for creating the forwarding request + * @param request + * the incoming HTTP request + */ + private void setFormat(HttpRequest.Builder builder, HttpRequest request) { + if ( ! request.hasProperty(FORMAT)) { + builder.put(FORMAT, "demo"); + } + } +} diff --git a/sample-apps/http-api-using-request-handlers-and-processors/src/main/java/com/yahoo/demo/DemoRenderer.java b/sample-apps/http-api-using-request-handlers-and-processors/src/main/java/com/yahoo/demo/DemoRenderer.java new file mode 100644 index 00000000000..c30adbcf7aa --- /dev/null +++ b/sample-apps/http-api-using-request-handlers-and-processors/src/main/java/com/yahoo/demo/DemoRenderer.java @@ -0,0 +1,119 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.demo; + +import com.yahoo.processing.Response; +import com.yahoo.processing.rendering.AsynchronousSectionedRenderer; +import com.yahoo.processing.response.Data; +import com.yahoo.processing.response.DataList; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.nio.charset.StandardCharsets; + +/** + * Render a response as plain text. First line is whether an error occurred, + * second rendering initialization time stamp, then each line is response data + * (indented according to its place in the hierarchic response), and the last + * line is time stamp for when the renderer was finished. + */ +public class DemoRenderer extends AsynchronousSectionedRenderer<Response> { + + /** + * Indent size for rendering hierarchic response data. + */ + public static final int INDENT_SIZE = 4; + + // Response heading + private String heading; + + // Just a utility to write strings to output stream + private Writer writer; + + // current indent in the rendered tree + String indent; + + /** + * No global, shared state to set. + */ + public DemoRenderer() { + } + + @Override + public void beginResponse(OutputStream stream) throws IOException { + writer = new OutputStreamWriter(stream, StandardCharsets.UTF_8.newEncoder()); + if (getResponse().data().request().errors().size() == 0) { + writer.write("OK\n"); + } else { + writer.write("Oops!\n"); + } + writer.write(heading); + writer.write('\n'); + } + + /** + * Indent {@link #INDENT_SIZE} spaces for each level in the tree. + */ + @Override + public void beginList(DataList<?> list) throws IOException { + indent = spaces((getRecursionLevel() - 1) * INDENT_SIZE); + } + + @Override + public void data(Data data) throws IOException { + if (!(data instanceof DataProcessor.DemoData)) { + return; + } + writer.write(indent); + writer.write(((DataProcessor.DemoData) data).content()); + writer.write('\n'); + } + + private static String spaces(int len) { + StringBuilder s = new StringBuilder(len); + for (int i = 0; i < len; ++i) { + s.append(' '); + } + return s.toString(); + } + + /** + * Out-dent one level if not at outermost level. + */ + @Override + public void endList(DataList<?> list) throws IOException { + if (indent.length() == 0) { + return; + } + indent = spaces(indent.length() - INDENT_SIZE); + } + + @Override + public void endResponse() throws IOException { + writer.write("Rendering finished work: " + System.currentTimeMillis()); + writer.write('\n'); + writer.close(); + } + + @Override + public String getEncoding() { + return StandardCharsets.UTF_8.name(); + } + + @Override + public String getMimeType() { + return "text/plain"; + } + + /** + * Initialize mutable, per-result set state here. + */ + @Override + public void init() { + long time = System.currentTimeMillis(); + + super.init(); // Important! The base class needs to initialize itself. + heading = "Renderer initialized: " + time; + } +} diff --git a/sample-apps/http-api-using-request-handlers-and-processors/src/main/resources/configdefinitions/demo.def b/sample-apps/http-api-using-request-handlers-and-processors/src/main/resources/configdefinitions/demo.def new file mode 100644 index 00000000000..9a804089433 --- /dev/null +++ b/sample-apps/http-api-using-request-handlers-and-processors/src/main/resources/configdefinitions/demo.def @@ -0,0 +1,5 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +namespace=demo + +demo[].term string + diff --git a/sample-apps/http-api-using-request-handlers-and-processors/src/test/java/com/yahoo/example/ApplicationMain.java b/sample-apps/http-api-using-request-handlers-and-processors/src/test/java/com/yahoo/example/ApplicationMain.java new file mode 100644 index 00000000000..da85e486602 --- /dev/null +++ b/sample-apps/http-api-using-request-handlers-and-processors/src/test/java/com/yahoo/example/ApplicationMain.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.example; + +import com.yahoo.application.Networking; +import org.junit.Test; + +import java.nio.file.FileSystems; + +import static org.junit.Assume.assumeTrue; + +public class ApplicationMain { + + @Test + public void runFromMaven() throws Exception { + assumeTrue(Boolean.valueOf(System.getProperty("isMavenSurefirePlugin"))); + main(null); + } + + public static void main(String[] args) throws Exception { + try (com.yahoo.application.Application app = com.yahoo.application.Application.fromApplicationPackage( + FileSystems.getDefault().getPath("src/main/application"), + Networking.enable)) { + app.getClass(); // throws NullPointerException + Thread.sleep(Long.MAX_VALUE); + } + } +}
\ No newline at end of file |