diff --git a/.gitignore b/.gitignore index e9b1bbce..3cb06f73 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,7 @@ Thumbs.db #.idea/workspace.xml - remove # and delete .idea if it better suit your needs. .gradle build/ +out/ #NDK obj/ diff --git a/.travis.yml b/.travis.yml index c0cf170b..a6f5916f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,12 @@ +dist: bionic language: java jdk: - - openjdk7 + - openjdk8 +before_install: + - sudo apt-get -y install python2.7 python-pip + - python2 -m pip install --user google-apis-client-generator install: - - JAVA_HOME=$(jdk_switcher home openjdk8) ./gradlew classes testClasses + - ./gradlew classes testClasses after_success: - ./gradlew jacocoTestReport - bash <(curl -s https://codecov.io/bash) diff --git a/README.md b/README.md index 79b5d969..07678ee6 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,18 @@ -[![Build Status](https://travis-ci.org/cloudendpoints/endpoints-java.svg?branch=master)](https://travis-ci.org/cloudendpoints/endpoints-java) -[![codecov](https://codecov.io/gh/cloudendpoints/endpoints-java/branch/master/graph/badge.svg)](https://codecov.io/gh/cloudendpoints/endpoints-java) +[![Build Status](https://api.travis-ci.org/AODocs/endpoints-java.svg?branch=master)](https://travis-ci.org/AODocs/endpoints-java) +[![codecov](https://codecov.io/gh/AODocs/endpoints-java/branch/master/graph/badge.svg)](https://codecov.io/gh/AODocs/endpoints-java) # Endpoints Java Framework The Endpoints Java Framework aims to be a simple solution to assist in creation of RESTful web APIs in Java. This repository provides several artifacts, all -in the `com.google.endpoints` group: +in the `com.aodocs.endpoints` group: 1. `endpoints-framework`: The core framework, required for all applications building Endpoints apps. -2. `endpoints-framework-guice`: An extension for configuring Endpoints using +2. `endpoints-framework-all`: Same as above, but with repackaged dependencies. +3. `endpoints-framework-guice`: An extension for configuring Endpoints using Guice. -3. `endpoints-framework-tools`: Tools for generating discovery documents, +4. `endpoints-framework-tools`: Tools for generating discovery documents, Swagger documents, and client libraries. The main documents for consuming Endpoints can be found at @@ -22,54 +23,39 @@ https://cloud.google.com/endpoints/docs/frameworks/java To install test versions to Maven for easier dependency management, simply run: gradle install + +## Additions to the original project + +These are the most notable additions to +[the original project by Google](https://github.com/cloudendpoints/endpoints-java), currently +inactive: +- Runtime + - Allow [adding arbitrary data](https://github.com/AODocs/endpoints-java/pull/20) to generic errors + - [Improve returned errors](https://github.com/AODocs/endpoints-java/pull/30) on malformed JSON + - Validate request parameters/body through [Java bean validation](https://beanvalidation.org/) provided by [Hibernate validator](https://hibernate.org/validator/) + - Validate request content-type (must be enable through enableContentTypeValidation servlet parameter) +- Discovery and Swagger + - [Add description on resources and resource usage as request body](https://github.com/AODocs/endpoints-java/commit/bbb1eff2bb9e7d28fc2ec17599257d0ef610531d) + - [Support declaring resource properties as required](https://github.com/AODocs/endpoints-java/pull/41) + - Support pattern, minimum/maximum attributes +- Swagger + - Generated spec is [fully compatible](https://github.com/AODocs/endpoints-java/pull/34) with +[Cloud Endpoints Portal](https://cloud.google.com/endpoints/docs/frameworks/dev-portal-overview) (and is 100% valid Swagger spec) + - Support [multi-API service](https://github.com/AODocs/endpoints-java/pull/40/commits/1f18d2f64f1538e63a7836a5cd52ff639fc624fd) in Endpoints Management + - [New options](https://github.com/AODocs/endpoints-java/pull/37) to combine common parameters in same path, extract parameter refs at spec level, add error model description, customize spec title and description + - [Add description support](https://github.com/AODocs/endpoints-java/pull/40/commits/bbb1eff2bb9e7d28fc2ec17599257d0ef610531d) for resource and resource usage + - [Configurable naming templates](https://github.com/AODocs/endpoints-java/pull/42) for operationIds and tag names with better defaults + - Support exclusiveMinimum/exclusiveMaximum, minLength/maxLength and minItems/maxItems attributes + +Check +[closed PRs](https://github.com/AODocs/endpoints-java/pulls?q=is%3Apr+sort%3Aupdated-desc+is%3Aclosed) +for all additions. + +## Troubleshooting + +* When using validation with java bean validation annotations : in order to have well named parameters instead of _arg0/arg1_ in error messages, your code must be compiled with the `-parameters` options. -## Migrating from the legacy Endpoints framework - -This release replaces the old `appengine-endpoints` artifact. You should replace -the dependency with the `endpoints-framework` artifact from the -`com.google.endpoints` group. In Maven, the new dependency looks like this: - - - com.google.endpoints - endpoints-framework - 2.0.14 - - -In Gradle, the new dependency looks like this: - - compile group: 'com.google.endpoints', name: 'endpoints-framework', version: '2.0.14' - -You also need to update your `web.xml`. Simply replace all instances of -`SystemServiceServlet` with `EndpointsServlet` and replace `/_ah/spi/*` with -`/_ah/api/*`. The new Endpoints configuration should look something like this: - - - EndpointsServlet - com.google.api.server.spi.EndpointsServlet - - services - com.example.Endpoint1,com.example.Endpoint2 - - - restricted - false - - - - EndpointsServlet - /_ah/api/* - - -## Repackaging dependencies - -The new version of the Endpoints framework does not repackage its dependencies -to hide them. If you run into dependency conflicts and need to do so, we -recommend using the Maven Shade plugin or Gradle Shadow plugin. Full -instructions on doing so are on the [wiki][1]. ## Contributing -Your contributions are welcome. Please follow the [contributor -guidelines](/CONTRIBUTING.md). - -[1]: https://github.com/cloudendpoints/endpoints-java/wiki/Vendoring-dependencies +Your contributions are welcome. Please follow the [contributor guidelines](/CONTRIBUTING.md). diff --git a/build.gradle b/build.gradle index 4c285e58..a35c9cd8 100644 --- a/build.gradle +++ b/build.gradle @@ -35,6 +35,10 @@ subprojects { sourceCompatibility = project.sourceCompatibility targetCompatibility = project.targetCompatibility options.encoding = 'UTF-8' + //required for com.google.api.server.spi.ObjectMapperUtilTest + //this need to be configured manually for the module in IntelliJ at: + //File | Settings | Build, Execution, Deployment | Compiler | Java Compiler + options.compilerArgs << '-parameters' } test { @@ -45,7 +49,7 @@ subprojects { jacocoTestReport { reports { - xml.enabled true + xml.required = true } } } @@ -55,70 +59,64 @@ def configureMaven(project, projectName, projectDescription) { def ossrhUsername = project.findProperty('ossrhUsername') def ossrhPassword = project.findProperty('ossrhPassword') configure(project) { - apply plugin: 'maven' + apply plugin: 'java-library' + apply plugin: 'maven-publish' apply plugin: 'signing' - task sourceJar(type: Jar, dependsOn: classes) { - classifier = 'sources' - from sourceSets.main.allSource + java { + withJavadocJar() + withSourcesJar() } - task javadocJar(type: Jar) { - classifier = 'javadoc' - from javadoc - } - - artifacts { - archives sourceJar, javadocJar - } - - signing { - required { gradle.taskGraph.hasTask('uploadArchives') } - sign configurations.archives - } - - uploadArchives { + publishing { repositories { - mavenDeployer { - beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } - - repository(url: "https://oss.sonatype.org/service/local/staging/deploy/maven2/") { - authentication(userName: ossrhUsername, password: ossrhPassword) + maven { + def releasesRepoUrl = "https://aodocs.jfrog.io/artifactory/aodocs-java-release/" + def snapshotsRepoUrl = "https://aodocs.jfrog.io/artifactory/aodocs-java-snapshot/" + url = version.endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl + credentials { + username = ossrhUsername + password = ossrhPassword } - - snapshotRepository(url: "https://oss.sonatype.org/content/repositories/snapshots/") { - authentication(userName: ossrhUsername, password: ossrhPassword) - } - - pom.project { - name projectName - description projectDescription - packaging 'jar' - url 'https://cloud.google.com/endpoints/docs/frameworks/java' + } + } + publications { + mavenJava(MavenPublication) { + from components.java + pom { + name = projectName + description = projectDescription + packaging = 'jar' + url = 'https://cloud.google.com/endpoints/docs/frameworks/java' scm { - connection 'scm:git:https://github.com/cloudendpoints/endpoints-java' - developerConnection 'scm:git:https://github.com/cloudendpoints/endpoints-java' - url 'scm:git:https://github.com/cloudendpoints/endpoints-java' + connection = 'scm:git:https://github.com/AODocs/endpoints-java.git' + developerConnection = 'scm:git:https://github.com/AODocs/endpoints-java.git' + url = 'scm:git:https://github.com/AODocs/endpoints-java.git' } licenses { license { - name 'The Apache License, Version 2.0' - url 'http://www.apache.org/licenses/LICENSE-2.0.txt' + name = 'The Apache License, Version 2.0' + url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' } } developers { developer { - id 'tangd' - name 'Daniel Tang' - email 'tangd@google.com' + id = 'tangd' + name = 'Daniel Tang' + email = 'tangd@google.com' } } } } } } + + signing { + required { gradle.taskGraph.hasTask('publish') } + sign publishing.publications.mavenJava + } } } diff --git a/discovery-client/build.gradle b/discovery-client/build.gradle index 194bacce..0e372587 100644 --- a/discovery-client/build.gradle +++ b/discovery-client/build.gradle @@ -14,25 +14,25 @@ * limitations under the License. */ apply plugin: 'java' -apply plugin: 'maven' +apply plugin: 'maven-publish' group = 'com.google.apis' archivesBaseName = 'google-api-services-discovery' -version = 'v1-rev20151119-1.20.0' +//the sources are included in the main artifact, the version does not matter +version = 'doesnotmatter' -sourceCompatibility = 1.6 -targetCompatibility = 1.6 +sourceCompatibility = 1.8 +targetCompatibility = 1.8 jar.manifest.attributes("Built-By": "Google") -jar.manifest.attributes("Build-Jdk": "1.6.x") +jar.manifest.attributes("Build-Jdk": "1.8.x") -task sourceJar(type: Jar) { - classifier = 'sources' - from sourceSets.main.allJava +java { + withSourcesJar() } artifacts { - archives sourceJar + archives sourcesJar } repositories { @@ -40,11 +40,11 @@ repositories { } dependencies { - compile module(group: 'com.google.api-client', name: 'google-api-client', version: '1.21.0') { - module(group: 'com.google.http-client', name: 'google-http-client-jackson2', version: '1.21.0') { - dependency('com.fasterxml.jackson.core:jackson-core:2.6.4') - dependency('com.google.http-client:google-http-client:1.21.0') + implementation module(group: 'com.google.api-client', name: 'google-api-client', version: "1.32.1") { + module(group: 'com.google.http-client', name: 'google-http-client-jackson2', version: "1.40.0") { + dependency("com.fasterxml.jackson.core:jackson-core:2.12.5") + dependency("com.google.http-client:google-http-client:1.40.0") } - dependency('com.google.oauth-client:google-oauth-client:1.21.0') + dependency("com.google.oauth-client:google-oauth-client:1.32.1") } } diff --git a/discovery-client/src/main/java/com/google/api/services/discovery/Discovery.java b/discovery-client/src/main/java/com/google/api/services/discovery/Discovery.java index 1a4e8d19..fca627d4 100644 --- a/discovery-client/src/main/java/com/google/api/services/discovery/Discovery.java +++ b/discovery-client/src/main/java/com/google/api/services/discovery/Discovery.java @@ -295,85 +295,7 @@ public GenerateRest set(String parameterName, Object value) { return (GenerateRest) super.set(parameterName, value); } } - /** - * Generates the Discovery Document of an API given its configuration. - * - * Create a request for the method "apis.generateRpc". - * - * This request holds the parameters needed by the discovery server. After setting any optional - * parameters, call the {@link GenerateRpc#execute()} method to invoke the remote operation. - * - * @param content the {@link com.google.api.services.discovery.model.ApiConfig} - * @return the request - */ - public GenerateRpc generateRpc(com.google.api.services.discovery.model.ApiConfig content) throws java.io.IOException { - GenerateRpc result = new GenerateRpc(content); - initialize(result); - return result; - } - - public class GenerateRpc extends DiscoveryRequest { - - private static final String REST_PATH = "apis/generate/rpc"; - - /** - * Generates the Discovery Document of an API given its configuration. - * - * Create a request for the method "apis.generateRpc". - * - * This request holds the parameters needed by the the discovery server. After setting any - * optional parameters, call the {@link GenerateRpc#execute()} method to invoke the remote - * operation.

{@link - * GenerateRpc#initialize(com.google.api.client.googleapis.services.AbstractGoogleClientRequest)} - * must be called to initialize this instance immediately after invoking the constructor.

- * - * @param content the {@link com.google.api.services.discovery.model.ApiConfig} - * @since 1.13 - */ - protected GenerateRpc(com.google.api.services.discovery.model.ApiConfig content) { - super(Discovery.this, "POST", REST_PATH, content, com.google.api.services.discovery.model.RpcDescription.class); - } - - @Override - public GenerateRpc setAlt(java.lang.String alt) { - return (GenerateRpc) super.setAlt(alt); - } - - @Override - public GenerateRpc setFields(java.lang.String fields) { - return (GenerateRpc) super.setFields(fields); - } - - @Override - public GenerateRpc setKey(java.lang.String key) { - return (GenerateRpc) super.setKey(key); - } - - @Override - public GenerateRpc setOauthToken(java.lang.String oauthToken) { - return (GenerateRpc) super.setOauthToken(oauthToken); - } - - @Override - public GenerateRpc setPrettyPrint(java.lang.Boolean prettyPrint) { - return (GenerateRpc) super.setPrettyPrint(prettyPrint); - } - - @Override - public GenerateRpc setQuotaUser(java.lang.String quotaUser) { - return (GenerateRpc) super.setQuotaUser(quotaUser); - } - - @Override - public GenerateRpc setUserIp(java.lang.String userIp) { - return (GenerateRpc) super.setUserIp(userIp); - } - - @Override - public GenerateRpc set(String parameterName, Object value) { - return (GenerateRpc) super.set(parameterName, value); - } - } + /** * Retrieve the description of a particular version of an api. * @@ -499,131 +421,7 @@ public GetRest set(String parameterName, Object value) { return (GetRest) super.set(parameterName, value); } } - /** - * Retrieve the description of a particular version of an api. - * - * Create a request for the method "apis.getRpc". - * - * This request holds the parameters needed by the discovery server. After setting any optional - * parameters, call the {@link GetRpc#execute()} method to invoke the remote operation. - * - * @param api The name of the API. - * @param version The version of the API. - * @return the request - */ - public GetRpc getRpc(java.lang.String api, java.lang.String version) throws java.io.IOException { - GetRpc result = new GetRpc(api, version); - initialize(result); - return result; - } - - public class GetRpc extends DiscoveryRequest { - - private static final String REST_PATH = "apis/{api}/{version}/rpc"; - - /** - * Retrieve the description of a particular version of an api. - * - * Create a request for the method "apis.getRpc". - * - * This request holds the parameters needed by the the discovery server. After setting any - * optional parameters, call the {@link GetRpc#execute()} method to invoke the remote operation. - *

{@link - * GetRpc#initialize(com.google.api.client.googleapis.services.AbstractGoogleClientRequest)} must - * be called to initialize this instance immediately after invoking the constructor.

- * - * @param api The name of the API. - * @param version The version of the API. - * @since 1.13 - */ - protected GetRpc(java.lang.String api, java.lang.String version) { - super(Discovery.this, "GET", REST_PATH, null, com.google.api.services.discovery.model.RpcDescription.class); - this.api = com.google.api.client.util.Preconditions.checkNotNull(api, "Required parameter api must be specified."); - this.version = com.google.api.client.util.Preconditions.checkNotNull(version, "Required parameter version must be specified."); - } - - @Override - public com.google.api.client.http.HttpResponse executeUsingHead() throws java.io.IOException { - return super.executeUsingHead(); - } - - @Override - public com.google.api.client.http.HttpRequest buildHttpRequestUsingHead() throws java.io.IOException { - return super.buildHttpRequestUsingHead(); - } - - @Override - public GetRpc setAlt(java.lang.String alt) { - return (GetRpc) super.setAlt(alt); - } - - @Override - public GetRpc setFields(java.lang.String fields) { - return (GetRpc) super.setFields(fields); - } - - @Override - public GetRpc setKey(java.lang.String key) { - return (GetRpc) super.setKey(key); - } - - @Override - public GetRpc setOauthToken(java.lang.String oauthToken) { - return (GetRpc) super.setOauthToken(oauthToken); - } - - @Override - public GetRpc setPrettyPrint(java.lang.Boolean prettyPrint) { - return (GetRpc) super.setPrettyPrint(prettyPrint); - } - - @Override - public GetRpc setQuotaUser(java.lang.String quotaUser) { - return (GetRpc) super.setQuotaUser(quotaUser); - } - - @Override - public GetRpc setUserIp(java.lang.String userIp) { - return (GetRpc) super.setUserIp(userIp); - } - - /** The name of the API. */ - @com.google.api.client.util.Key - private java.lang.String api; - - /** The name of the API. - */ - public java.lang.String getApi() { - return api; - } - - /** The name of the API. */ - public GetRpc setApi(java.lang.String api) { - this.api = api; - return this; - } - - /** The version of the API. */ - @com.google.api.client.util.Key - private java.lang.String version; - - /** The version of the API. - */ - public java.lang.String getVersion() { - return version; - } - - /** The version of the API. */ - public GetRpc setVersion(java.lang.String version) { - this.version = version; - return this; - } - - @Override - public GetRpc set(String parameterName, Object value) { - return (GetRpc) super.set(parameterName, value); - } - } + /** * Retrieve the list of APIs supported at this endpoint. * diff --git a/discovery-client/src/main/java/com/google/api/services/discovery/model/RpcDescription.java b/discovery-client/src/main/java/com/google/api/services/discovery/model/RpcDescription.java deleted file mode 100644 index 74765187..00000000 --- a/discovery-client/src/main/java/com/google/api/services/discovery/model/RpcDescription.java +++ /dev/null @@ -1,869 +0,0 @@ -/* - * Copyright 2010 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License - * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express - * or implied. See the License for the specific language governing permissions and limitations under - * the License. - */ -/* - * This code was generated by https://code.google.com/p/google-apis-client-generator/ - * (build: 2015-11-16 19:10:01 UTC) - * on 2015-11-19 at 17:34:48 UTC - * Modify at your own risk. - */ - -package com.google.api.services.discovery.model; - -/** - * Model definition for RpcDescription. - * - *

This is the Java data model class that specifies how to parse/serialize into the JSON that is - * transmitted over HTTP when working with the APIs Discovery Service. For a detailed explanation - * see: - * http://code.google.com/p/google-http-java-client/wiki/JSON - *

- * - * @author Google, Inc. - */ -@SuppressWarnings("javadoc") -public final class RpcDescription extends com.google.api.client.json.GenericJson { - - /** - * Authentication information. - * The value may be {@code null}. - */ - @com.google.api.client.util.Key - private Auth auth; - - /** - * Indicates how the API name should be capitalized and split into various parts. Useful for - * generating pretty class names. - * The value may be {@code null}. - */ - @com.google.api.client.util.Key - private java.lang.String canonicalName; - - /** - * The description of the API. - * The value may be {@code null}. - */ - @com.google.api.client.util.Key - private java.lang.String description; - - /** - * Indicate the version of the Discovery API used to generate this doc. - * The value may be {@code null}. - */ - @com.google.api.client.util.Key - private java.lang.String discoveryVersion; - - /** - * A link to human readable documentation for the API. - * The value may be {@code null}. - */ - @com.google.api.client.util.Key - private java.lang.String documentationLink; - - /** - * The ETag for this response. - * The value may be {@code null}. - */ - @com.google.api.client.util.Key - private java.lang.String etag; - - /** - * Enable exponential backoff for suitable methods in the generated clients. - * The value may be {@code null}. - */ - @com.google.api.client.util.Key - private java.lang.Boolean exponentialBackoffDefault; - - /** - * A list of supported features for this API. - * The value may be {@code null}. - */ - @com.google.api.client.util.Key - private java.util.List features; - - /** - * Links to 16x16 and 32x32 icons representing the API. - * The value may be {@code null}. - */ - @com.google.api.client.util.Key - private Icons icons; - - /** - * The ID of this API. - * The value may be {@code null}. - */ - @com.google.api.client.util.Key - private java.lang.String id; - - /** - * The kind for this response. - * The value may be {@code null}. - */ - @com.google.api.client.util.Key - private java.lang.String kind; - - /** - * Labels for the status of this API, such as labs or deprecated. - * The value may be {@code null}. - */ - @com.google.api.client.util.Key - private java.util.List labels; - - /** - * API-level methods for this API. - * The value may be {@code null}. - */ - @com.google.api.client.util.Key - private java.util.Map methods; - - /** - * The name of this API. - * The value may be {@code null}. - */ - @com.google.api.client.util.Key - private java.lang.String name; - - /** - * The domain of the owner of the API. Together with the ownerName and a packagePath values, this - * can be used to generate a library for the API which would have a unique fully qualified name. - * The value may be {@code null}. - */ - @com.google.api.client.util.Key - private java.lang.String ownerDomain; - - /** - * The name of the owner of the API. See ownerDomain - * The value may be {@code null}. - */ - @com.google.api.client.util.Key - private java.lang.String ownerName; - - /** - * The package of the owner of the API. See ownerDomain - * The value may be {@code null}. - */ - @com.google.api.client.util.Key - private java.lang.String packagePath; - - /** - * Common parameters that apply across all apis. - * The value may be {@code null}. - */ - @com.google.api.client.util.Key - private java.util.Map parameters; - - /** - * The protocol described by this document. - * The value may be {@code null}. - */ - @com.google.api.client.util.Key - private java.lang.String protocol; - - /** - * The version of the API. - * The value may be {@code null}. - */ - @com.google.api.client.util.Key - private java.lang.String revision; - - /** - * The root URL under which all API services live. - * The value may be {@code null}. - */ - @com.google.api.client.util.Key - private java.lang.String rootUrl; - - /** - * The path for JSON-RPC requests. - * The value may be {@code null}. - */ - @com.google.api.client.util.Key - private java.lang.String rpcPath; - - /** - * [DEPRECATED] The URL for JSON-RPC requests. - * The value may be {@code null}. - */ - @com.google.api.client.util.Key - private java.lang.String rpcUrl; - - /** - * The schemas for this API. - * The value may be {@code null}. - */ - @com.google.api.client.util.Key - private java.util.Map schemas; - - /** - * The title of the API. - * The value may be {@code null}. - */ - @com.google.api.client.util.Key - private java.lang.String title; - - /** - * The version of the API. - * The value may be {@code null}. - */ - @com.google.api.client.util.Key - private java.lang.String version; - - /** - * Authentication information. - * @return value or {@code null} for none - */ - public Auth getAuth() { - return auth; - } - - /** - * Authentication information. - * @param auth auth or {@code null} for none - */ - public RpcDescription setAuth(Auth auth) { - this.auth = auth; - return this; - } - - /** - * Indicates how the API name should be capitalized and split into various parts. Useful for - * generating pretty class names. - * @return value or {@code null} for none - */ - public java.lang.String getCanonicalName() { - return canonicalName; - } - - /** - * Indicates how the API name should be capitalized and split into various parts. Useful for - * generating pretty class names. - * @param canonicalName canonicalName or {@code null} for none - */ - public RpcDescription setCanonicalName(java.lang.String canonicalName) { - this.canonicalName = canonicalName; - return this; - } - - /** - * The description of the API. - * @return value or {@code null} for none - */ - public java.lang.String getDescription() { - return description; - } - - /** - * The description of the API. - * @param description description or {@code null} for none - */ - public RpcDescription setDescription(java.lang.String description) { - this.description = description; - return this; - } - - /** - * Indicate the version of the Discovery API used to generate this doc. - * @return value or {@code null} for none - */ - public java.lang.String getDiscoveryVersion() { - return discoveryVersion; - } - - /** - * Indicate the version of the Discovery API used to generate this doc. - * @param discoveryVersion discoveryVersion or {@code null} for none - */ - public RpcDescription setDiscoveryVersion(java.lang.String discoveryVersion) { - this.discoveryVersion = discoveryVersion; - return this; - } - - /** - * A link to human readable documentation for the API. - * @return value or {@code null} for none - */ - public java.lang.String getDocumentationLink() { - return documentationLink; - } - - /** - * A link to human readable documentation for the API. - * @param documentationLink documentationLink or {@code null} for none - */ - public RpcDescription setDocumentationLink(java.lang.String documentationLink) { - this.documentationLink = documentationLink; - return this; - } - - /** - * The ETag for this response. - * @return value or {@code null} for none - */ - public java.lang.String getEtag() { - return etag; - } - - /** - * The ETag for this response. - * @param etag etag or {@code null} for none - */ - public RpcDescription setEtag(java.lang.String etag) { - this.etag = etag; - return this; - } - - /** - * Enable exponential backoff for suitable methods in the generated clients. - * @return value or {@code null} for none - */ - public java.lang.Boolean getExponentialBackoffDefault() { - return exponentialBackoffDefault; - } - - /** - * Enable exponential backoff for suitable methods in the generated clients. - * @param exponentialBackoffDefault exponentialBackoffDefault or {@code null} for none - */ - public RpcDescription setExponentialBackoffDefault(java.lang.Boolean exponentialBackoffDefault) { - this.exponentialBackoffDefault = exponentialBackoffDefault; - return this; - } - - /** - * A list of supported features for this API. - * @return value or {@code null} for none - */ - public java.util.List getFeatures() { - return features; - } - - /** - * A list of supported features for this API. - * @param features features or {@code null} for none - */ - public RpcDescription setFeatures(java.util.List features) { - this.features = features; - return this; - } - - /** - * Links to 16x16 and 32x32 icons representing the API. - * @return value or {@code null} for none - */ - public Icons getIcons() { - return icons; - } - - /** - * Links to 16x16 and 32x32 icons representing the API. - * @param icons icons or {@code null} for none - */ - public RpcDescription setIcons(Icons icons) { - this.icons = icons; - return this; - } - - /** - * The ID of this API. - * @return value or {@code null} for none - */ - public java.lang.String getId() { - return id; - } - - /** - * The ID of this API. - * @param id id or {@code null} for none - */ - public RpcDescription setId(java.lang.String id) { - this.id = id; - return this; - } - - /** - * The kind for this response. - * @return value or {@code null} for none - */ - public java.lang.String getKind() { - return kind; - } - - /** - * The kind for this response. - * @param kind kind or {@code null} for none - */ - public RpcDescription setKind(java.lang.String kind) { - this.kind = kind; - return this; - } - - /** - * Labels for the status of this API, such as labs or deprecated. - * @return value or {@code null} for none - */ - public java.util.List getLabels() { - return labels; - } - - /** - * Labels for the status of this API, such as labs or deprecated. - * @param labels labels or {@code null} for none - */ - public RpcDescription setLabels(java.util.List labels) { - this.labels = labels; - return this; - } - - /** - * API-level methods for this API. - * @return value or {@code null} for none - */ - public java.util.Map getMethods() { - return methods; - } - - /** - * API-level methods for this API. - * @param methods methods or {@code null} for none - */ - public RpcDescription setMethods(java.util.Map methods) { - this.methods = methods; - return this; - } - - /** - * The name of this API. - * @return value or {@code null} for none - */ - public java.lang.String getName() { - return name; - } - - /** - * The name of this API. - * @param name name or {@code null} for none - */ - public RpcDescription setName(java.lang.String name) { - this.name = name; - return this; - } - - /** - * The domain of the owner of the API. Together with the ownerName and a packagePath values, this - * can be used to generate a library for the API which would have a unique fully qualified name. - * @return value or {@code null} for none - */ - public java.lang.String getOwnerDomain() { - return ownerDomain; - } - - /** - * The domain of the owner of the API. Together with the ownerName and a packagePath values, this - * can be used to generate a library for the API which would have a unique fully qualified name. - * @param ownerDomain ownerDomain or {@code null} for none - */ - public RpcDescription setOwnerDomain(java.lang.String ownerDomain) { - this.ownerDomain = ownerDomain; - return this; - } - - /** - * The name of the owner of the API. See ownerDomain - * @return value or {@code null} for none - */ - public java.lang.String getOwnerName() { - return ownerName; - } - - /** - * The name of the owner of the API. See ownerDomain - * @param ownerName ownerName or {@code null} for none - */ - public RpcDescription setOwnerName(java.lang.String ownerName) { - this.ownerName = ownerName; - return this; - } - - /** - * The package of the owner of the API. See ownerDomain - * @return value or {@code null} for none - */ - public java.lang.String getPackagePath() { - return packagePath; - } - - /** - * The package of the owner of the API. See ownerDomain - * @param packagePath packagePath or {@code null} for none - */ - public RpcDescription setPackagePath(java.lang.String packagePath) { - this.packagePath = packagePath; - return this; - } - - /** - * Common parameters that apply across all apis. - * @return value or {@code null} for none - */ - public java.util.Map getParameters() { - return parameters; - } - - /** - * Common parameters that apply across all apis. - * @param parameters parameters or {@code null} for none - */ - public RpcDescription setParameters(java.util.Map parameters) { - this.parameters = parameters; - return this; - } - - /** - * The protocol described by this document. - * @return value or {@code null} for none - */ - public java.lang.String getProtocol() { - return protocol; - } - - /** - * The protocol described by this document. - * @param protocol protocol or {@code null} for none - */ - public RpcDescription setProtocol(java.lang.String protocol) { - this.protocol = protocol; - return this; - } - - /** - * The version of the API. - * @return value or {@code null} for none - */ - public java.lang.String getRevision() { - return revision; - } - - /** - * The version of the API. - * @param revision revision or {@code null} for none - */ - public RpcDescription setRevision(java.lang.String revision) { - this.revision = revision; - return this; - } - - /** - * The root URL under which all API services live. - * @return value or {@code null} for none - */ - public java.lang.String getRootUrl() { - return rootUrl; - } - - /** - * The root URL under which all API services live. - * @param rootUrl rootUrl or {@code null} for none - */ - public RpcDescription setRootUrl(java.lang.String rootUrl) { - this.rootUrl = rootUrl; - return this; - } - - /** - * The path for JSON-RPC requests. - * @return value or {@code null} for none - */ - public java.lang.String getRpcPath() { - return rpcPath; - } - - /** - * The path for JSON-RPC requests. - * @param rpcPath rpcPath or {@code null} for none - */ - public RpcDescription setRpcPath(java.lang.String rpcPath) { - this.rpcPath = rpcPath; - return this; - } - - /** - * [DEPRECATED] The URL for JSON-RPC requests. - * @return value or {@code null} for none - */ - public java.lang.String getRpcUrl() { - return rpcUrl; - } - - /** - * [DEPRECATED] The URL for JSON-RPC requests. - * @param rpcUrl rpcUrl or {@code null} for none - */ - public RpcDescription setRpcUrl(java.lang.String rpcUrl) { - this.rpcUrl = rpcUrl; - return this; - } - - /** - * The schemas for this API. - * @return value or {@code null} for none - */ - public java.util.Map getSchemas() { - return schemas; - } - - /** - * The schemas for this API. - * @param schemas schemas or {@code null} for none - */ - public RpcDescription setSchemas(java.util.Map schemas) { - this.schemas = schemas; - return this; - } - - /** - * The title of the API. - * @return value or {@code null} for none - */ - public java.lang.String getTitle() { - return title; - } - - /** - * The title of the API. - * @param title title or {@code null} for none - */ - public RpcDescription setTitle(java.lang.String title) { - this.title = title; - return this; - } - - /** - * The version of the API. - * @return value or {@code null} for none - */ - public java.lang.String getVersion() { - return version; - } - - /** - * The version of the API. - * @param version version or {@code null} for none - */ - public RpcDescription setVersion(java.lang.String version) { - this.version = version; - return this; - } - - @Override - public RpcDescription set(String fieldName, Object value) { - return (RpcDescription) super.set(fieldName, value); - } - - @Override - public RpcDescription clone() { - return (RpcDescription) super.clone(); - } - - /** - * Authentication information. - */ - public static final class Auth extends com.google.api.client.json.GenericJson { - - /** - * OAuth 2.0 authentication information. - * The value may be {@code null}. - */ - @com.google.api.client.util.Key - private Oauth2 oauth2; - - /** - * OAuth 2.0 authentication information. - * @return value or {@code null} for none - */ - public Oauth2 getOauth2() { - return oauth2; - } - - /** - * OAuth 2.0 authentication information. - * @param oauth2 oauth2 or {@code null} for none - */ - public Auth setOauth2(Oauth2 oauth2) { - this.oauth2 = oauth2; - return this; - } - - @Override - public Auth set(String fieldName, Object value) { - return (Auth) super.set(fieldName, value); - } - - @Override - public Auth clone() { - return (Auth) super.clone(); - } - - /** - * OAuth 2.0 authentication information. - */ - public static final class Oauth2 extends com.google.api.client.json.GenericJson { - - /** - * Available OAuth 2.0 scopes. - * The value may be {@code null}. - */ - @com.google.api.client.util.Key - private java.util.Map scopes; - - static { - // hack to force ProGuard to consider ScopesElement used, since otherwise it would be stripped out - // see http://code.google.com/p/google-api-java-client/issues/detail?id=528 - com.google.api.client.util.Data.nullOf(ScopesElement.class); - } - - /** - * Available OAuth 2.0 scopes. - * @return value or {@code null} for none - */ - public java.util.Map getScopes() { - return scopes; - } - - /** - * Available OAuth 2.0 scopes. - * @param scopes scopes or {@code null} for none - */ - public Oauth2 setScopes(java.util.Map scopes) { - this.scopes = scopes; - return this; - } - - @Override - public Oauth2 set(String fieldName, Object value) { - return (Oauth2) super.set(fieldName, value); - } - - @Override - public Oauth2 clone() { - return (Oauth2) super.clone(); - } - - /** - * The scope value. - */ - public static final class ScopesElement extends com.google.api.client.json.GenericJson { - - /** - * Description of scope. - * The value may be {@code null}. - */ - @com.google.api.client.util.Key - private java.lang.String description; - - /** - * Description of scope. - * @return value or {@code null} for none - */ - public java.lang.String getDescription() { - return description; - } - - /** - * Description of scope. - * @param description description or {@code null} for none - */ - public ScopesElement setDescription(java.lang.String description) { - this.description = description; - return this; - } - - @Override - public ScopesElement set(String fieldName, Object value) { - return (ScopesElement) super.set(fieldName, value); - } - - @Override - public ScopesElement clone() { - return (ScopesElement) super.clone(); - } - - } - } - } - - /** - * Links to 16x16 and 32x32 icons representing the API. - */ - public static final class Icons extends com.google.api.client.json.GenericJson { - - /** - * The URL of the 16x16 icon. - * The value may be {@code null}. - */ - @com.google.api.client.util.Key - private java.lang.String x16; - - /** - * The URL of the 32x32 icon. - * The value may be {@code null}. - */ - @com.google.api.client.util.Key - private java.lang.String x32; - - /** - * The URL of the 16x16 icon. - * @return value or {@code null} for none - */ - public java.lang.String getX16() { - return x16; - } - - /** - * The URL of the 16x16 icon. - * @param x16 x16 or {@code null} for none - */ - public Icons setX16(java.lang.String x16) { - this.x16 = x16; - return this; - } - - /** - * The URL of the 32x32 icon. - * @return value or {@code null} for none - */ - public java.lang.String getX32() { - return x32; - } - - /** - * The URL of the 32x32 icon. - * @param x32 x32 or {@code null} for none - */ - public Icons setX32(java.lang.String x32) { - this.x32 = x32; - return this; - } - - @Override - public Icons set(String fieldName, Object value) { - return (Icons) super.set(fieldName, value); - } - - @Override - public Icons clone() { - return (Icons) super.clone(); - } - - } - -} diff --git a/discovery-client/src/main/java/com/google/api/services/discovery/model/RpcMethod.java b/discovery-client/src/main/java/com/google/api/services/discovery/model/RpcMethod.java deleted file mode 100644 index f8259217..00000000 --- a/discovery-client/src/main/java/com/google/api/services/discovery/model/RpcMethod.java +++ /dev/null @@ -1,507 +0,0 @@ -/* - * Copyright 2010 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License - * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express - * or implied. See the License for the specific language governing permissions and limitations under - * the License. - */ -/* - * This code was generated by https://code.google.com/p/google-apis-client-generator/ - * (build: 2015-11-16 19:10:01 UTC) - * on 2015-11-19 at 17:34:48 UTC - * Modify at your own risk. - */ - -package com.google.api.services.discovery.model; - -/** - * Model definition for RpcMethod. - * - *

This is the Java data model class that specifies how to parse/serialize into the JSON that is - * transmitted over HTTP when working with the APIs Discovery Service. For a detailed explanation - * see: - * http://code.google.com/p/google-http-java-client/wiki/JSON - *

- * - * @author Google, Inc. - */ -@SuppressWarnings("javadoc") -public final class RpcMethod extends com.google.api.client.json.GenericJson { - - /** - * Whether the method can be made using an HTTP GET JSON-RPC request. - * The value may be {@code null}. - */ - @com.google.api.client.util.Key - private java.lang.Boolean allowGet; - - /** - * Description of this method. - * The value may be {@code null}. - */ - @com.google.api.client.util.Key - private java.lang.String description; - - /** - * Does this method require sending the ETag along with the request. - * The value may be {@code null}. - */ - @com.google.api.client.util.Key - private java.lang.Boolean etagRequired; - - /** - * A unique ID for this method. This property can be used to match methods between different - * versions of Discovery. - * The value may be {@code null}. - */ - @com.google.api.client.util.Key - private java.lang.String id; - - /** - * Media upload parameters. - * The value may be {@code null}. - */ - @com.google.api.client.util.Key - private MediaUpload mediaUpload; - - /** - * Ordered list of required parameters, serves as a hint to clients on how to structure their - * method signatures. The array is ordered such that the "most-significant" parameter appears - * first. - * The value may be {@code null}. - */ - @com.google.api.client.util.Key - private java.util.List parameterOrder; - - /** - * Description for all parameters in this method. - * The value may be {@code null}. - */ - @com.google.api.client.util.Key - private java.util.Map parameters; - - static { - // hack to force ProGuard to consider JsonSchema used, since otherwise it would be stripped out - // see http://code.google.com/p/google-api-java-client/issues/detail?id=528 - com.google.api.client.util.Data.nullOf(JsonSchema.class); - } - - /** - * The schema for the response. - * The value may be {@code null}. - */ - @com.google.api.client.util.Key - private Returns returns; - - /** - * OAuth 2.0 scopes applicable to this method. - * The value may be {@code null}. - */ - @com.google.api.client.util.Key - private java.util.List scopes; - - /** - * Whether this method supports media download. - * The value may be {@code null}. - */ - @com.google.api.client.util.Key - private java.lang.Boolean supportsMediaDownload; - - /** - * Whether this method supports media upload. - * The value may be {@code null}. - */ - @com.google.api.client.util.Key - private java.lang.Boolean supportsMediaUpload; - - /** - * Whether this method supports patch semantics. - * The value may be {@code null}. - */ - @com.google.api.client.util.Key - private java.lang.Boolean supportsPatch; - - /** - * Whether this method supports subscriptions. - * The value may be {@code null}. - */ - @com.google.api.client.util.Key - private java.lang.Boolean supportsSubscription; - - /** - * Indicates that downloads from this method should use the download service URL (i.e. - * "/download"). Only applies if the method supports media download. - * The value may be {@code null}. - */ - @com.google.api.client.util.Key - private java.lang.Boolean useMediaDownloadService; - - /** - * Whether the method can be made using an HTTP GET JSON-RPC request. - * @return value or {@code null} for none - */ - public java.lang.Boolean getAllowGet() { - return allowGet; - } - - /** - * Whether the method can be made using an HTTP GET JSON-RPC request. - * @param allowGet allowGet or {@code null} for none - */ - public RpcMethod setAllowGet(java.lang.Boolean allowGet) { - this.allowGet = allowGet; - return this; - } - - /** - * Description of this method. - * @return value or {@code null} for none - */ - public java.lang.String getDescription() { - return description; - } - - /** - * Description of this method. - * @param description description or {@code null} for none - */ - public RpcMethod setDescription(java.lang.String description) { - this.description = description; - return this; - } - - /** - * Does this method require sending the ETag along with the request. - * @return value or {@code null} for none - */ - public java.lang.Boolean getEtagRequired() { - return etagRequired; - } - - /** - * Does this method require sending the ETag along with the request. - * @param etagRequired etagRequired or {@code null} for none - */ - public RpcMethod setEtagRequired(java.lang.Boolean etagRequired) { - this.etagRequired = etagRequired; - return this; - } - - /** - * A unique ID for this method. This property can be used to match methods between different - * versions of Discovery. - * @return value or {@code null} for none - */ - public java.lang.String getId() { - return id; - } - - /** - * A unique ID for this method. This property can be used to match methods between different - * versions of Discovery. - * @param id id or {@code null} for none - */ - public RpcMethod setId(java.lang.String id) { - this.id = id; - return this; - } - - /** - * Media upload parameters. - * @return value or {@code null} for none - */ - public MediaUpload getMediaUpload() { - return mediaUpload; - } - - /** - * Media upload parameters. - * @param mediaUpload mediaUpload or {@code null} for none - */ - public RpcMethod setMediaUpload(MediaUpload mediaUpload) { - this.mediaUpload = mediaUpload; - return this; - } - - /** - * Ordered list of required parameters, serves as a hint to clients on how to structure their - * method signatures. The array is ordered such that the "most-significant" parameter appears - * first. - * @return value or {@code null} for none - */ - public java.util.List getParameterOrder() { - return parameterOrder; - } - - /** - * Ordered list of required parameters, serves as a hint to clients on how to structure their - * method signatures. The array is ordered such that the "most-significant" parameter appears - * first. - * @param parameterOrder parameterOrder or {@code null} for none - */ - public RpcMethod setParameterOrder(java.util.List parameterOrder) { - this.parameterOrder = parameterOrder; - return this; - } - - /** - * Description for all parameters in this method. - * @return value or {@code null} for none - */ - public java.util.Map getParameters() { - return parameters; - } - - /** - * Description for all parameters in this method. - * @param parameters parameters or {@code null} for none - */ - public RpcMethod setParameters(java.util.Map parameters) { - this.parameters = parameters; - return this; - } - - /** - * The schema for the response. - * @return value or {@code null} for none - */ - public Returns getReturns() { - return returns; - } - - /** - * The schema for the response. - * @param returns returns or {@code null} for none - */ - public RpcMethod setReturns(Returns returns) { - this.returns = returns; - return this; - } - - /** - * OAuth 2.0 scopes applicable to this method. - * @return value or {@code null} for none - */ - public java.util.List getScopes() { - return scopes; - } - - /** - * OAuth 2.0 scopes applicable to this method. - * @param scopes scopes or {@code null} for none - */ - public RpcMethod setScopes(java.util.List scopes) { - this.scopes = scopes; - return this; - } - - /** - * Whether this method supports media download. - * @return value or {@code null} for none - */ - public java.lang.Boolean getSupportsMediaDownload() { - return supportsMediaDownload; - } - - /** - * Whether this method supports media download. - * @param supportsMediaDownload supportsMediaDownload or {@code null} for none - */ - public RpcMethod setSupportsMediaDownload(java.lang.Boolean supportsMediaDownload) { - this.supportsMediaDownload = supportsMediaDownload; - return this; - } - - /** - * Whether this method supports media upload. - * @return value or {@code null} for none - */ - public java.lang.Boolean getSupportsMediaUpload() { - return supportsMediaUpload; - } - - /** - * Whether this method supports media upload. - * @param supportsMediaUpload supportsMediaUpload or {@code null} for none - */ - public RpcMethod setSupportsMediaUpload(java.lang.Boolean supportsMediaUpload) { - this.supportsMediaUpload = supportsMediaUpload; - return this; - } - - /** - * Whether this method supports patch semantics. - * @return value or {@code null} for none - */ - public java.lang.Boolean getSupportsPatch() { - return supportsPatch; - } - - /** - * Whether this method supports patch semantics. - * @param supportsPatch supportsPatch or {@code null} for none - */ - public RpcMethod setSupportsPatch(java.lang.Boolean supportsPatch) { - this.supportsPatch = supportsPatch; - return this; - } - - /** - * Whether this method supports subscriptions. - * @return value or {@code null} for none - */ - public java.lang.Boolean getSupportsSubscription() { - return supportsSubscription; - } - - /** - * Whether this method supports subscriptions. - * @param supportsSubscription supportsSubscription or {@code null} for none - */ - public RpcMethod setSupportsSubscription(java.lang.Boolean supportsSubscription) { - this.supportsSubscription = supportsSubscription; - return this; - } - - /** - * Indicates that downloads from this method should use the download service URL (i.e. - * "/download"). Only applies if the method supports media download. - * @return value or {@code null} for none - */ - public java.lang.Boolean getUseMediaDownloadService() { - return useMediaDownloadService; - } - - /** - * Indicates that downloads from this method should use the download service URL (i.e. - * "/download"). Only applies if the method supports media download. - * @param useMediaDownloadService useMediaDownloadService or {@code null} for none - */ - public RpcMethod setUseMediaDownloadService(java.lang.Boolean useMediaDownloadService) { - this.useMediaDownloadService = useMediaDownloadService; - return this; - } - - @Override - public RpcMethod set(String fieldName, Object value) { - return (RpcMethod) super.set(fieldName, value); - } - - @Override - public RpcMethod clone() { - return (RpcMethod) super.clone(); - } - - /** - * Media upload parameters. - */ - public static final class MediaUpload extends com.google.api.client.json.GenericJson { - - /** - * MIME Media Ranges for acceptable media uploads to this method. - * The value may be {@code null}. - */ - @com.google.api.client.util.Key - private java.util.List accept; - - /** - * Maximum size of a media upload, such as "1MB", "2GB" or "3TB". - * The value may be {@code null}. - */ - @com.google.api.client.util.Key - private java.lang.String maxSize; - - /** - * MIME Media Ranges for acceptable media uploads to this method. - * @return value or {@code null} for none - */ - public java.util.List getAccept() { - return accept; - } - - /** - * MIME Media Ranges for acceptable media uploads to this method. - * @param accept accept or {@code null} for none - */ - public MediaUpload setAccept(java.util.List accept) { - this.accept = accept; - return this; - } - - /** - * Maximum size of a media upload, such as "1MB", "2GB" or "3TB". - * @return value or {@code null} for none - */ - public java.lang.String getMaxSize() { - return maxSize; - } - - /** - * Maximum size of a media upload, such as "1MB", "2GB" or "3TB". - * @param maxSize maxSize or {@code null} for none - */ - public MediaUpload setMaxSize(java.lang.String maxSize) { - this.maxSize = maxSize; - return this; - } - - @Override - public MediaUpload set(String fieldName, Object value) { - return (MediaUpload) super.set(fieldName, value); - } - - @Override - public MediaUpload clone() { - return (MediaUpload) super.clone(); - } - - } - - /** - * The schema for the response. - */ - public static final class Returns extends com.google.api.client.json.GenericJson { - - /** - * Schema ID for the response schema. - * The value may be {@code null}. - */ - @com.google.api.client.util.Key - private java.lang.String $ref; - - /** - * Schema ID for the response schema. - * @return value or {@code null} for none - */ - public java.lang.String get$ref() { - return $ref; - } - - /** - * Schema ID for the response schema. - * @param $ref $ref or {@code null} for none - */ - public Returns set$ref(java.lang.String $ref) { - this.$ref = $ref; - return this; - } - - @Override - public Returns set(String fieldName, Object value) { - return (Returns) super.set(fieldName, value); - } - - @Override - public Returns clone() { - return (Returns) super.clone(); - } - - } - -} diff --git a/endpoints-framework-all/build.gradle b/endpoints-framework-all/build.gradle index 73394894..22aace28 100644 --- a/endpoints-framework-all/build.gradle +++ b/endpoints-framework-all/build.gradle @@ -1,5 +1,6 @@ plugins { - id 'com.github.johnrengelman.shadow' version '1.2.3' + id 'com.github.johnrengelman.shadow' version '5.1.0' + id 'java-library' } configurations { @@ -16,7 +17,7 @@ jar { def repackagedDir = 'endpoints.repackaged' shadowJar { - classifier = null + archiveClassifier = null relocate 'org.apache', "${repackagedDir}.org.apache" relocate 'org.yaml', "${repackagedDir}.org.yaml" relocate 'org.joda', "${repackagedDir}.org.joda" @@ -25,6 +26,7 @@ shadowJar { relocate 'com.google.common', "${repackagedDir}.com.google.common" relocate 'com.google.api.client', "${repackagedDir}.com.google.api.client" relocate 'org.slf4j', "${repackagedDir}.org.slf4j" + relocate 'com.google.thirdparty', "${repackagedDir}.com.google.thirdparty" dependencies { exclude(dependency('com.google.appengine:appengine-api-1.0-sdk:.*')) @@ -37,9 +39,9 @@ artifacts { } dependencies { - include project(':endpoints-framework') - compile group: 'com.google.appengine', name: 'appengine-api-1.0-sdk', version: appengineVersion - compile group: 'javax.servlet', name: 'servlet-api', version: servletVersion + compileOnly project(':endpoints-framework') + api group: 'com.google.appengine', name: 'appengine-api-1.0-sdk', version: appengineVersion + api group: 'javax.servlet', name: 'servlet-api', version: servletVersion } configureMaven(project, 'Endpoints Framework', 'A framework for building RESTful web APIs.') diff --git a/endpoints-framework-guice/build.gradle b/endpoints-framework-guice/build.gradle index 851fdda1..6ba0566f 100644 --- a/endpoints-framework-guice/build.gradle +++ b/endpoints-framework-guice/build.gradle @@ -14,6 +14,10 @@ * limitations under the License. */ +plugins { + id 'java-library' +} + configureMaven( project, 'Endpoints Framework Guice Extension', @@ -21,10 +25,11 @@ configureMaven( dependencies { compileOnly project(':endpoints-framework') - compile group: 'com.google.inject', name: 'guice', version: guiceVersion - compile group: 'com.google.inject.extensions', name: 'guice-servlet', version: guiceVersion + api group: 'com.google.inject', name: 'guice', version: guiceVersion + api group: 'com.google.inject.extensions', name: 'guice-servlet', version: guiceVersion - testCompile project(':test-utils') - testCompile group: 'junit', name: 'junit', version: junitVersion - testCompile group: 'org.mockito', name: 'mockito-core', version: mockitoVersion + testImplementation project(':test-utils') + testImplementation project(':endpoints-framework') + testImplementation group: 'junit', name: 'junit', version: junitVersion + testImplementation group: 'org.mockito', name: 'mockito-core', version: mockitoVersion } diff --git a/endpoints-framework-guice/src/main/java/com/google/api/server/spi/guice/EndpointsModule.java b/endpoints-framework-guice/src/main/java/com/google/api/server/spi/guice/EndpointsModule.java index ee7814eb..694c7832 100644 --- a/endpoints-framework-guice/src/main/java/com/google/api/server/spi/guice/EndpointsModule.java +++ b/endpoints-framework-guice/src/main/java/com/google/api/server/spi/guice/EndpointsModule.java @@ -25,7 +25,6 @@ * this class and call one of the helpers in {@link #configureServlets()}. */ public class EndpointsModule extends ServletModule { - private static final Logger logger = Logger.getLogger(EndpointsModule.class.getName()); /** * Configure Endpoints given a list of service classes using {@link GuiceEndpointsServlet}. * @@ -36,40 +35,10 @@ public class EndpointsModule extends ServletModule { */ protected void configureEndpoints( String urlPattern, Iterable> serviceClasses) { - configureEndpoints(urlPattern, serviceClasses, false); - } - - /** - * Configure Endpoints given a list of service classes. - * - * @deprecated the legacy servlet is no longer available. - * @param urlPattern the URL pattern to configure the servlet on. For the legacy servlet, use - * "/_ah/spi/*". For the new servlet, use "/_ah/api/*" if backwards compatibility is desired, or - * any other pattern if compatibility is not an issue. - * @param serviceClasses the list of backend classes to be included - * @param useLegacyServlet whether or not to use the old style servlet - */ - @Deprecated - protected void configureEndpoints( - String urlPattern, Iterable> serviceClasses, boolean useLegacyServlet) { ServletInitializationParameters initParameters = ServletInitializationParameters.builder() .addServiceClasses(serviceClasses) .build(); - configureEndpoints(urlPattern, initParameters, useLegacyServlet); - } - - /** - * Configure Endpoints given {@link ServletInitializationParameters} using - * {@link GuiceEndpointsServlet}. - * - * @param urlPattern the URL pattern to configure the servlet on. For the legacy servlet, use - * "/_ah/spi/*". For the new servlet, use "/_ah/api/*" if backwards compatibility is desired, or - * any other pattern if compatibility is not an issue - * @param initParameters the initialization parameters. Must include service classes to be useful - */ - protected void configureEndpoints( - String urlPattern, ServletInitializationParameters initParameters) { - configureEndpoints(urlPattern, initParameters, false); + configureEndpoints(urlPattern, initParameters); } /** @@ -79,15 +48,11 @@ protected void configureEndpoints( * "/_ah/spi/*". For the new servlet, use "/_ah/api/*" if backwards compatibility is desired, or * any other pattern if compatibility is not an issue * @param initParameters the initialization parameters. Must include service classes to be useful - * @param useLegacyServlet whether or not to use the old style servlet */ protected void configureEndpoints( - String urlPattern, ServletInitializationParameters initParameters, boolean useLegacyServlet) { + String urlPattern, ServletInitializationParameters initParameters) { bind(ServiceMap.class) .toInstance(ServiceMap.create(binder(), initParameters.getServiceClasses())); - if (useLegacyServlet) { - logger.severe("the legacy servlet is no longer available."); - } super.serve(urlPattern).with(GuiceEndpointsServlet.class, initParameters.asMap()); } } diff --git a/endpoints-framework-guice/src/test/java/com/google/api/server/spi/guice/EndpointsModuleTest.java b/endpoints-framework-guice/src/test/java/com/google/api/server/spi/guice/EndpointsModuleTest.java index 79f18087..ada9972a 100644 --- a/endpoints-framework-guice/src/test/java/com/google/api/server/spi/guice/EndpointsModuleTest.java +++ b/endpoints-framework-guice/src/test/java/com/google/api/server/spi/guice/EndpointsModuleTest.java @@ -54,7 +54,6 @@ public class EndpointsModuleTest { private static final ServletInitializationParameters INIT_PARAMETERS = ServletInitializationParameters.builder() .addServiceClasses(SERVICES) - .setRestricted(false) .build(); private EndpointsModule module; @@ -64,7 +63,7 @@ public void setUp() throws Exception { @Override protected void configureServlets() { super.configureServlets(); - configureEndpoints(URL_PATTERN, INIT_PARAMETERS, true); + configureEndpoints(URL_PATTERN, INIT_PARAMETERS); } }; Elements.getElements(module); @@ -82,8 +81,6 @@ public void testConfigureEndpoints_withInterceptor() { assertEquals("Servlet not bound.", 1, visitor.linkedServlets.size()); LinkedServletBinding servletBinding = visitor.linkedServlets.get(0); assertEquals("URL pattern does not match", URL_PATTERN, servletBinding.getPattern()); - assertEquals("Wrong initialization parameter provided", "false", - servletBinding.getInitParams().get("restricted")); assertNotNull("SystemService named provider not found.", visitor.systemServiceProvider); ServiceMap serviceMap = (ServiceMap) visitor.systemServiceProvider.getProvider().get(); @@ -105,8 +102,6 @@ public void testConfigureEndpoints_withoutInterceptor() { assertEquals("Servlet not bound.", 1, visitor.linkedServlets.size()); LinkedServletBinding servletBinding = visitor.linkedServlets.get(0); assertEquals("URL pattern does not match", URL_PATTERN, servletBinding.getPattern()); - assertEquals("Wrong initialization parameter provided", "false", - servletBinding.getInitParams().get("restricted")); assertNotNull("SystemService named provider not found.", visitor.systemServiceProvider); ServiceMap serviceMap = (ServiceMap) visitor.systemServiceProvider.getProvider().get(); @@ -116,64 +111,34 @@ public void testConfigureEndpoints_withoutInterceptor() { services.toArray()[0].getClass()); } - @Test - public void testConfigureEndpoints_legacyServletWithServices() { - testServletClassWithServices(true, GuiceEndpointsServlet.class); - } - - @Test - public void testConfigureEndpoints_newServletWithServices() { - testServletClassWithServices(false, GuiceEndpointsServlet.class); - } - @Test public void testConfigureEndpoints_defaultServletWithServices() { - testServletClassWithServices(null, GuiceEndpointsServlet.class); - } - - @Test - public void testConfigureEndpoints_legacyServletWithInitParams() { - testServletClassWithInitParams(true, GuiceEndpointsServlet.class); - } - - @Test - public void testConfigureEndpoints_newServletWithInitParams() { - testServletClassWithInitParams(false, GuiceEndpointsServlet.class); + testServletClassWithServices(GuiceEndpointsServlet.class); } @Test public void testConfigureEndpoints_defaultServletWithInitParams() { - testServletClassWithInitParams(null, GuiceEndpointsServlet.class); + testServletClassWithInitParams(GuiceEndpointsServlet.class); } - private void testServletClassWithServices(final Boolean servletFlag, Class expectedClass) { + private void testServletClassWithServices(Class expectedClass) { testServletClass(new EndpointsModule() { @Override protected void configureServlets() { super.configureServlets(); - if (servletFlag == null) { - configureEndpoints(URL_PATTERN, SERVICES); - } else { - configureEndpoints(URL_PATTERN, SERVICES, servletFlag); - } + configureEndpoints(URL_PATTERN, SERVICES); } }, expectedClass); } - private void testServletClassWithInitParams(final Boolean servletFlag, Class expectedClass) { + private void testServletClassWithInitParams(Class expectedClass) { testServletClass(new EndpointsModule() { @Override protected void configureServlets() { super.configureServlets(); - if (servletFlag == null) { - configureEndpoints(URL_PATTERN, ServletInitializationParameters.builder() - .addServiceClasses(SERVICES) - .build()); - } else { - configureEndpoints(URL_PATTERN, ServletInitializationParameters.builder() - .addServiceClasses(SERVICES) - .build(), servletFlag); - } + configureEndpoints(URL_PATTERN, ServletInitializationParameters.builder() + .addServiceClasses(SERVICES) + .build()); } }, expectedClass); } @@ -199,7 +164,7 @@ public void testConfigureEndpoints_defaultInitParameters() { @Override protected void configureServlets() { super.configureServlets(); - configureEndpoints(URL_PATTERN, SERVICES, true); + configureEndpoints(URL_PATTERN, SERVICES); } }; Injector injector = Guice.createInjector(module, new DummyModule()); @@ -212,8 +177,6 @@ protected void configureServlets() { assertEquals("Servlet not bound.", 1, visitor.linkedServlets.size()); LinkedServletBinding servletBinding = visitor.linkedServlets.get(0); assertEquals("URL pattern does not match", URL_PATTERN, servletBinding.getPattern()); - assertEquals("Wrong initialization parameter provided", "true", - servletBinding.getInitParams().get("restricted")); assertNotNull("SystemService named provider not found.", visitor.systemServiceProvider); ServiceMap serviceMap = (ServiceMap) visitor.systemServiceProvider.getProvider().get(); diff --git a/endpoints-framework-tools/build.gradle b/endpoints-framework-tools/build.gradle index ed9db8d9..9c739507 100644 --- a/endpoints-framework-tools/build.gradle +++ b/endpoints-framework-tools/build.gradle @@ -30,12 +30,13 @@ apply plugin: 'application' mainClassName = 'com.google.api.server.spi.tools.EndpointsTool' dependencies { - compile project(':endpoints-framework') - compile group: 'com.google.appengine', name: 'appengine-tools-sdk', version: appengineVersion + compileOnly project(':discovery-client') + api project(':endpoints-framework') + api group: 'com.google.appengine', name: 'appengine-tools-sdk', version: appengineVersion - testCompile project(':test-utils') - testCompile group: 'junit', name: 'junit', version: junitVersion - testCompile group: 'org.mockito', name: 'mockito-core', version: mockitoVersion - testCompile group: 'com.google.truth', name: 'truth', version: truthVersion - testCompile group: 'org.springframework', name: 'spring-test', version: springtestVersion + testImplementation project(':test-utils') + testImplementation group: 'junit', name: 'junit', version: junitVersion + testImplementation group: 'org.mockito', name: 'mockito-core', version: mockitoVersion + testImplementation group: 'com.google.truth', name: 'truth', version: truthVersion + testImplementation group: 'org.springframework', name: 'spring-test', version: springtestVersion } diff --git a/endpoints-framework-tools/src/main/java/com/google/api/server/spi/tools/EndpointsTool.java b/endpoints-framework-tools/src/main/java/com/google/api/server/spi/tools/EndpointsTool.java index ded5a58f..862be3b9 100644 --- a/endpoints-framework-tools/src/main/java/com/google/api/server/spi/tools/EndpointsTool.java +++ b/endpoints-framework-tools/src/main/java/com/google/api/server/spi/tools/EndpointsTool.java @@ -51,8 +51,10 @@ public EndpointsTool() { actions = new LinkedHashMap<>(); actions.put(GetDiscoveryDocAction.NAME, new GetDiscoveryDocAction()); actions.put(GetClientLibAction.NAME, new GetClientLibAction()); + actions.put(GetClientSrcAction.NAME, new GetClientSrcAction()); actions.put(GenApiConfigAction.NAME, new GenApiConfigAction()); actions.put(GenClientLibAction.NAME, new GenClientLibAction()); + actions.put(GenClientSrcAction.NAME, new GenClientSrcAction()); actions.put(GetOpenApiDocAction.NAME, new GetOpenApiDocAction()); actions.put(GetOpenApiDocAction.LEGACY_NAME, new GetOpenApiDocAction(GetOpenApiDocAction.LEGACY_NAME, false)); diff --git a/endpoints-framework-tools/src/main/java/com/google/api/server/spi/tools/EndpointsToolAction.java b/endpoints-framework-tools/src/main/java/com/google/api/server/spi/tools/EndpointsToolAction.java index 2380a822..49e775f4 100644 --- a/endpoints-framework-tools/src/main/java/com/google/api/server/spi/tools/EndpointsToolAction.java +++ b/endpoints-framework-tools/src/main/java/com/google/api/server/spi/tools/EndpointsToolAction.java @@ -214,10 +214,6 @@ protected String getBuildSystem(Option buildSystemOption) { return getOptionOrDefault(buildSystemOption, DEFAULT_BUILD_SYSTEM); } - protected String getFormat(Option formatOption) { - return getOptionOrDefault(formatOption, DEFAULT_FORMAT); - } - protected boolean getDebug(Option debugOption) { return debugOption.getValue() != null; } @@ -406,13 +402,6 @@ public void setHelpDisplayNeeded(boolean helpDisplayNeeded) { this.helpDisplayNeeded = helpDisplayNeeded; } - /** - * Return the example string which will be displayed in the usage. - */ - public String getExampleString() { - return exampleString; - } - /** * Set the example string which will be displayed in the usage. */ @@ -467,6 +456,17 @@ public static EndpointsOption makeVisibleNonFlagOption( return new EndpointsOption(shortName, longName, false, true, placeHolderValue, description); } + public static EndpointsOption makeVisibleFlagOption( + @Nullable String longName, + @Nullable String description) { + return new EndpointsOption(null, longName, true, true, null, description) { + @Override + public void apply() { + getValues().add("true"); + } + }; + } + public static EndpointsOption makeInvisibleFlagOption( @Nullable String shortName, @Nullable String longName) { diff --git a/endpoints-framework-tools/src/main/java/com/google/api/server/spi/tools/GenClientSrcAction.java b/endpoints-framework-tools/src/main/java/com/google/api/server/spi/tools/GenClientSrcAction.java new file mode 100644 index 00000000..bffdfefe --- /dev/null +++ b/endpoints-framework-tools/src/main/java/com/google/api/server/spi/tools/GenClientSrcAction.java @@ -0,0 +1,66 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.api.server.spi.tools; + +import java.io.File; +import java.io.IOException; +import java.util.Arrays; + +import com.google.appengine.tools.util.Option; + +/** + * Command to generate a client API source code from a Discovery document. + */ +public class GenClientSrcAction extends EndpointsToolAction { + + // name of this command + public static final String NAME = "gen-client-src"; + + private Option languageOption = makeLanguageOption(); + private Option outputOption = makeOutputOption(); + + public GenClientSrcAction() { + super(NAME); + setOptions(Arrays.asList(languageOption, outputOption)); + setShortDescription("Generates a client API source code"); + setHelpDisplayNeeded(false); + } + + @Override + public boolean execute() throws IOException { + if (getArgs().size() != 1) { + return false; + } + genClientSrcFromFile(getLanguage(languageOption), getOutputPath(outputOption), getArgs().get(0)); + return true; + } + + /** + * Generates a client library for an API. + * @param language Language of the client library. + * @param outputDirPath Directory to write generated client library into + * @param discoveryDoc Discovery doc file + */ + public Object genClientSrcFromFile(String language, String outputDirPath, String discoveryDoc) + throws IOException { + ClientLibGenerator generator = new LocalClientLibGenerator(); + generator.generateClientLib(discoveryDoc, language, "", "", new File(outputDirPath)); + return null; + } + + @Override + public String getUsageString() { + return NAME + " "; + } +} diff --git a/endpoints-framework-tools/src/main/java/com/google/api/server/spi/tools/GetClientSrcAction.java b/endpoints-framework-tools/src/main/java/com/google/api/server/spi/tools/GetClientSrcAction.java new file mode 100644 index 00000000..85221409 --- /dev/null +++ b/endpoints-framework-tools/src/main/java/com/google/api/server/spi/tools/GetClientSrcAction.java @@ -0,0 +1,92 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.api.server.spi.tools; + +import java.io.IOException; +import java.net.URL; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import com.google.api.server.spi.config.ApiConfigException; +import com.google.appengine.tools.util.Option; + +/** + * Command that combines 3 other ones and generates client API source code from service classes. + */ +public class GetClientSrcAction extends EndpointsToolAction { + + // name of this command + public static final String NAME = "get-client-src"; + + private Option classPathOption = makeClassPathOption(); + private Option languageOption = makeLanguageOption(); + private Option outputOption = makeOutputOption(); + private Option warOption = makeWarOption(); + private Option debugOption = makeDebugOption(); + private Option hostnameOption = makeHostnameOption(); + private Option basePathOption = makeBasePathOption(); + + public GetClientSrcAction() { + super(NAME); + setOptions(Arrays.asList(classPathOption, languageOption, outputOption, warOption, + debugOption, hostnameOption, basePathOption)); + setShortDescription("Generates client source code"); + setExampleString(" get-client-src --language=java " + + "com.google.devrel.samples.ttt.spi.BoardV1 com.google.devrel.samples.ttt.spi.ScoresV1"); + setHelpDisplayNeeded(true); + } + + @Override + public boolean execute() throws ClassNotFoundException, IOException, ApiConfigException { + String warPath = getWarPath(warOption); + List serviceClassNames = getServiceClassNames(warPath); + if (serviceClassNames.isEmpty()) { + return false; + } + getClientSrc(computeClassPath(warPath, getClassPath(classPathOption)), + getLanguage(languageOption), getOutputPath(outputOption), serviceClassNames, + getHostname(hostnameOption, warPath), getBasePath(basePathOption), + getDebug(debugOption) + ); + return true; + } + + /** + * Generates a Java client API source code for an API. Combines the steps of generating API + * configuration, generating Discovery doc and generating client source code into one. + * @param classPath Class path to load service classes and their dependencies + * @param language Language of the client library. Only "java" is supported right now + * @param outputDirPath Directory to write output files into + * @param serviceClassNames Array of service class names of the API + * @param hostname The hostname to use + * @param basePath The base path to use + * @param debug Whether or not to output intermediate output files + */ + public Object getClientSrc(URL[] classPath, String language, String outputDirPath, + List serviceClassNames, String hostname, String basePath, + boolean debug) throws ClassNotFoundException, IOException, ApiConfigException { + Map discoveryDocs = new GetDiscoveryDocAction().getDiscoveryDoc( + classPath, outputDirPath, serviceClassNames, hostname, basePath, debug /* outputToDisk */); + for (Map.Entry entry : discoveryDocs.entrySet()) { + new GenClientSrcAction().genClientSrcFromFile(language, outputDirPath, entry.getValue()); + } + return null; + } + + @Override + public String getUsageString() { + return NAME + " ..."; + } +} diff --git a/endpoints-framework-tools/src/main/java/com/google/api/server/spi/tools/GetDiscoveryDocAction.java b/endpoints-framework-tools/src/main/java/com/google/api/server/spi/tools/GetDiscoveryDocAction.java index 88bc0fe2..b5a26d3f 100644 --- a/endpoints-framework-tools/src/main/java/com/google/api/server/spi/tools/GetDiscoveryDocAction.java +++ b/endpoints-framework-tools/src/main/java/com/google/api/server/spi/tools/GetDiscoveryDocAction.java @@ -60,13 +60,12 @@ public class GetDiscoveryDocAction extends EndpointsToolAction { private Option classPathOption = makeClassPathOption(); private Option outputOption = makeOutputOption(); private Option warOption = makeWarOption(); - private Option debugOption = makeDebugOption(); private Option hostnameOption = makeHostnameOption(); private Option basePathOption = makeBasePathOption(); public GetDiscoveryDocAction() { super(NAME); - setOptions(Arrays.asList(classPathOption, outputOption, warOption, debugOption, hostnameOption, + setOptions(Arrays.asList(classPathOption, outputOption, warOption, hostnameOption, basePathOption)); setShortDescription("Generates discovery documents"); setExampleString(" get-discovery-doc " @@ -141,7 +140,7 @@ apiConfigs, new DiscoveryContext().setHostname(hostname).setBasePath(basePath), outputDir + "/" + key.getName() + "-" + key.getVersion() + "-rest.discovery"; String docString = writer.writeValueAsString(entry.getValue()); if (outputToDisk) { - Files.write(docString, new File(discoveryDocFilePath), UTF_8); + Files.asCharSink(new File(discoveryDocFilePath), UTF_8).write(docString); System.out.println("API Discovery Document written to " + discoveryDocFilePath); } builder.put(discoveryDocFilePath, docString); diff --git a/endpoints-framework-tools/src/main/java/com/google/api/server/spi/tools/GetOpenApiDocAction.java b/endpoints-framework-tools/src/main/java/com/google/api/server/spi/tools/GetOpenApiDocAction.java index dfcf47ac..819881b6 100644 --- a/endpoints-framework-tools/src/main/java/com/google/api/server/spi/tools/GetOpenApiDocAction.java +++ b/endpoints-framework-tools/src/main/java/com/google/api/server/spi/tools/GetOpenApiDocAction.java @@ -15,6 +15,7 @@ */ package com.google.api.server.spi.tools; +import static com.google.api.server.spi.tools.EndpointsToolAction.EndpointsOption.makeVisibleFlagOption; import static java.nio.charset.StandardCharsets.UTF_8; import com.google.api.server.spi.ServiceContext; @@ -52,6 +53,23 @@ public class GetOpenApiDocAction extends EndpointsToolAction { private Option warOption = makeWarOption(); private Option hostnameOption = makeHostnameOption(); private Option basePathOption = makeBasePathOption(); + private Option titleOption = makeTitleOption(); + private Option descriptionOption = makeDescriptionOption(); + private Option apiNameOption = makeApiNameOption(); + private Option tagTemplateOption = makeTagTemplateOption(); + private Option operationIdTemplateOption = makeOperationIdTemplateOption(); + private Option addGoogleJsonErrorAsDefaultResponseOption = makeVisibleFlagOption( + "addGoogleJsonErrorAsDefaultResponse", "Add GoogleJsonError as default response" + ); + private Option addErrorCodesForServiceExceptionsOption = makeVisibleFlagOption( + "addErrorCodesForServiceExceptions", "Add GoogleJsonError for codes in ServiceExceptions" + ); + private Option extractCommonParametersAsRefsOption = makeVisibleFlagOption( + "extractCommonParametersAsRefs", "Extract common parameters as refs at specification level" + ); + private Option combineCommonParametersInSamePathOption = makeVisibleFlagOption( + "combineCommonParametersInSamePath", "Combine common parameters in same path" + ); public GetOpenApiDocAction() { this(NAME, true); @@ -60,13 +78,61 @@ public GetOpenApiDocAction() { protected GetOpenApiDocAction(String alias, boolean displayHelp) { super(alias); setOptions( - Arrays.asList(classPathOption, outputOption, warOption, hostnameOption, basePathOption)); + Arrays.asList(classPathOption, outputOption, warOption, hostnameOption, basePathOption, + titleOption, descriptionOption, apiNameOption, + tagTemplateOption, operationIdTemplateOption, + addGoogleJsonErrorAsDefaultResponseOption, addErrorCodesForServiceExceptionsOption, + extractCommonParametersAsRefsOption, combineCommonParametersInSamePathOption)); setShortDescription("Generates an OpenAPI document"); setExampleString(" " + getNames()[0] + " com.google.devrel.samples.ttt.spi.BoardV1 com.google.devrel.samples.ttt.spi.ScoresV1"); setHelpDisplayNeeded(displayHelp); } + private static Option makeTitleOption() { + return EndpointsOption.makeVisibleNonFlagOption( + "t", + "title", + "TITLE", + "Sets the title for the generated document. Default is the app's host."); + } + + private static Option makeDescriptionOption() { + return EndpointsOption.makeVisibleNonFlagOption( + "d", + "description", + "DESCRIPTION", + "Sets the description for the generated document. Is empty by default."); + } + + private static Option makeApiNameOption() { + return EndpointsOption.makeVisibleNonFlagOption( + "a", + "apiName", + "API_NAME", + "Sets the api name. Endpoints Management will use hostname if not defined."); + } + + private static Option makeTagTemplateOption() { + return EndpointsOption.makeVisibleNonFlagOption( + "tt", + "tagTemplate", + "TAG_TEMPLATE", + "Sets the tag template name. Defaults to " + SwaggerContext.DEFAULT_TAG_TEMPLATE + "."); + } + + private static Option makeOperationIdTemplateOption() { + return EndpointsOption.makeVisibleNonFlagOption( + "oit", + "operationIdTemplate", + "OPERATION_ID_TEMPLATE", + "Sets the operation id template. Defaults to " + SwaggerContext.DEFAULT_OPERATION_ID_TEMPLATE + "."); + } + + private static boolean getBooleanOptionValue(Option option) { + return option.getValue() != null; + } + @Override public String getUsageString() { return getNames()[0] + " ..."; @@ -81,7 +147,17 @@ public boolean execute() throws ClassNotFoundException, IOException, ApiConfigEx } genOpenApiDoc(computeClassPath(warPath, getClassPath(classPathOption)), getOpenApiOutputPath(outputOption), getHostname(hostnameOption, warPath), - getBasePath(basePathOption), serviceClassNames, true); + getBasePath(basePathOption), + getOptionOrDefault(titleOption, null), + getOptionOrDefault(descriptionOption, null), + getOptionOrDefault(apiNameOption, null), + getOptionOrDefault(tagTemplateOption, SwaggerContext.DEFAULT_TAG_TEMPLATE), + getOptionOrDefault(operationIdTemplateOption, SwaggerContext.DEFAULT_OPERATION_ID_TEMPLATE), + getBooleanOptionValue(addGoogleJsonErrorAsDefaultResponseOption), + getBooleanOptionValue(addErrorCodesForServiceExceptionsOption), + getBooleanOptionValue(extractCommonParametersAsRefsOption), + getBooleanOptionValue(combineCommonParametersInSamePathOption), + serviceClassNames, true); return true; } @@ -98,6 +174,10 @@ public boolean execute() throws ClassNotFoundException, IOException, ApiConfigEx */ public String genOpenApiDoc( URL[] classPath, String outputFilePath, String hostname, String basePath, + String title, String description, String apiName, + String tagTemplate, String operationIdTemplate, + boolean addGoogleJsonErrorAsDefaultResponse, boolean addErrorCodesForServiceExceptionsOption, + boolean extractCommonParametersAsRefsOption, boolean combineCommonParametersInSamePathOption, List serviceClassNames, boolean outputToDisk) throws ClassNotFoundException, IOException, ApiConfigException { File outputFile = new File(outputFilePath); @@ -120,12 +200,21 @@ public String genOpenApiDoc( SwaggerGenerator generator = new SwaggerGenerator(); SwaggerContext swaggerContext = new SwaggerContext() .setHostname(hostname) - .setBasePath(basePath); - Swagger swagger = generator.writeSwagger(apiConfigs, true, swaggerContext); + .setBasePath(basePath) + .setTitle(title) + .setDescription(description) + .setApiName(apiName) + .setTagTemplate(tagTemplate) + .setOperationIdTemplate(operationIdTemplate) + .setAddGoogleJsonErrorAsDefaultResponse(addGoogleJsonErrorAsDefaultResponse) + .setAddErrorCodesForServiceExceptions(addErrorCodesForServiceExceptionsOption) + .setExtractCommonParametersAsRefs(extractCommonParametersAsRefsOption) + .setCombineCommonParametersInSamePath(combineCommonParametersInSamePathOption); + Swagger swagger = generator.writeSwagger(apiConfigs, swaggerContext); String swaggerStr = Json.mapper().writer(new EndpointsPrettyPrinter()) .writeValueAsString(swagger); if (outputToDisk) { - Files.write(swaggerStr, outputFile, UTF_8); + Files.asCharSink(outputFile, UTF_8).write(swaggerStr); System.out.println("OpenAPI document written to " + outputFilePath); } diff --git a/endpoints-framework-tools/src/main/java/com/google/api/server/spi/tools/JacksonUtil.java b/endpoints-framework-tools/src/main/java/com/google/api/server/spi/tools/JacksonUtil.java index 14788742..b15a46f4 100644 --- a/endpoints-framework-tools/src/main/java/com/google/api/server/spi/tools/JacksonUtil.java +++ b/endpoints-framework-tools/src/main/java/com/google/api/server/spi/tools/JacksonUtil.java @@ -47,7 +47,7 @@ public static ObjectNode mergeObject(ObjectNode object1, ObjectNode object2, JsonNode child2 = object2.get(fieldName); JsonNode child1 = object1.get(fieldName); JsonNode merged = (child1 == null) ? child2 : mergeNode(child1, child2, throwOnConflict); - object1.put(fieldName, merged); + object1.set(fieldName, merged); } return object1; } diff --git a/endpoints-framework-tools/src/main/java/com/google/api/server/spi/tools/LocalClientLibGenerator.java b/endpoints-framework-tools/src/main/java/com/google/api/server/spi/tools/LocalClientLibGenerator.java new file mode 100644 index 00000000..b09834ea --- /dev/null +++ b/endpoints-framework-tools/src/main/java/com/google/api/server/spi/tools/LocalClientLibGenerator.java @@ -0,0 +1,141 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.api.server.spi.tools; + +import static com.google.common.base.MoreObjects.firstNonNull; +import static java.lang.System.getProperty; +import static java.lang.System.getenv; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.logging.Logger; + +import org.apache.commons.io.input.ReaderInputStream; + +import com.google.api.server.spi.IoUtil; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.google.common.io.CharSource; + +/** + * Implementation of (@link ClientLibGenerator} using a local tool for generation.
+ * The Python package google-apis-client-generator must be installed: + *
+ *     python2 -m pip install --user --upgrade setuptools wheel
+ *     python2 -m pip install --user google-apis-client-generator
+ * 
+ */ +public class LocalClientLibGenerator implements ClientLibGenerator { + + private static final Logger log = Logger.getLogger(LocalClientLibGenerator.class.getName()); + + @VisibleForTesting + static final String GENERATOR_EXECUTABLE = "generate_library"; + private static final String GENERATOR_DISCOVERY_FILE_OPTION = "--input="; + private static final String GENERATOR_LANGUAGE_OPTION = "--language="; + private static final String GENERATOR_DESTINATION_DIRECTORY_OPTION = "--output_dir="; + /* Put API version in package paths. */ + private static final String GENERATOR_API_VERSION_PACKAGE_OPTION = "--version_package"; + + private static final List GENERATOR_SUPPORTED_LANGUAGES = ImmutableList.of("java"); + + /** + * Generate the source code. + * @param discoveryDoc Discovery document of the API + * @param language Only java is supported. + * @param languageVersion Ignored. + * @param layout Ignored. + * @param destinationDirectory Target directory to generate source code into. + * @throws IOException on failure + */ + @Override + public void generateClientLib(String discoveryDoc, String language, String languageVersion, + String layout, File destinationDirectory) throws IOException { + Preconditions.checkArgument(GENERATOR_SUPPORTED_LANGUAGES.contains(language), "Unsupported language: " + language); + + // Creates the destination directory + Files.createDirectories(destinationDirectory.toPath()); + + // Creates a temporary discovery file + File discoveryFile = File.createTempFile(GENERATOR_EXECUTABLE, "-discovery.tmp"); + try { + IoUtil.copy(new ReaderInputStream(CharSource.wrap(discoveryDoc).openStream()), discoveryFile); + + File generateLibOut = File.createTempFile(GENERATOR_EXECUTABLE, ".out"); + File generateLibErr = File.createTempFile(GENERATOR_EXECUTABLE, ".err"); + + List command = new ArrayList<>(getLibraryGeneratorCommand()); + command.add(GENERATOR_DISCOVERY_FILE_OPTION + discoveryFile); + command.add(GENERATOR_LANGUAGE_OPTION + language); + command.add(GENERATOR_DESTINATION_DIRECTORY_OPTION + destinationDirectory.getAbsolutePath()); + command.add(GENERATOR_API_VERSION_PACKAGE_OPTION); + + ProcessBuilder builder = new ProcessBuilder() + .command(command) + .redirectOutput(generateLibOut) + .redirectError(generateLibErr); + int status; + try { + status = builder.start().waitFor(); + } catch (InterruptedException e) { + throw new RuntimeException("Source code generation interrupted", e); + } + if (status == 0) { + // Success: get rid of output files + generateLibOut.delete(); + generateLibErr.delete(); + } else { + throw new IOException("Failed to generate source code. See " + generateLibErr.getAbsolutePath() + " for details"); + } + } finally { + discoveryFile.delete(); + } + } + + private List getLibraryGeneratorCommand() { + if (getProperty("os.name").toLowerCase().contains("win")) { + log.warning("You are using the LocalClientLibGenerator on Windows.\n" + + "You should specify the following environmental variables:\n" + + "- GOOGLE_GENERATE_LIBRARY_PYTHON to point at your Python 2.7 installation\n" + + "- GOOGLE_GENERATE_LIBRARY_SCRIPT_LOCATION to point at the location where your generate_library.py script was installed (it should be something like 'C:\\Users\\Armin\\AppData\\Roaming\\Python\\Python27\\site-packages\\googleapis\\codegen\\generate_library.py')"); + + // The generate_library.exe runs library generation asynchronously on Windows due to https://bugs.python.org/issue9148 + // so we call the script directly + + String python = firstNonNull( + getenv("GOOGLE_GENERATE_LIBRARY_PYTHON"), + "python" + ); + + String scriptLocation = firstNonNull( + getenv("GOOGLE_GENERATE_LIBRARY_SCRIPT_LOCATION"), + getProperty("user.home") + "\\AppData\\Roaming\\Python\\Python27\\site-packages\\googleapis\\codegen\\generate_library.py" + ); + + File scriptLocationFile = new File(scriptLocation); + Preconditions.checkArgument(scriptLocation.endsWith("generate_library.py"), "You should specify a generate_library.py in the GOOGLE_GENERATE_LIBRARY_SCRIPT_LOCATION env var"); + Preconditions.checkArgument(scriptLocationFile.isFile(), "Could not find script at '" + scriptLocationFile.getAbsolutePath() + "': make the GOOGLE_GENERATE_LIBRARY_SCRIPT_LOCATION env var point at the generate_library.py script you have installed"); + + return Arrays.asList(python, scriptLocationFile.getAbsolutePath()); + } + + return Collections.singletonList(GENERATOR_EXECUTABLE); + } +} diff --git a/endpoints-framework-tools/src/main/resources/com/google/api/server/spi/tools/testing/fake-discovery-doc-rest.json b/endpoints-framework-tools/src/main/resources/com/google/api/server/spi/tools/testing/fake-discovery-doc-rest.json index 3bd712c6..ce531cb3 100644 --- a/endpoints-framework-tools/src/main/resources/com/google/api/server/spi/tools/testing/fake-discovery-doc-rest.json +++ b/endpoints-framework-tools/src/main/resources/com/google/api/server/spi/tools/testing/fake-discovery-doc-rest.json @@ -3,6 +3,9 @@ "id": "guestbook:v1", "name": "guestbook", "version": "v1", + "ownerDomain": "google.com", + "ownerName": "Google", + "packagePath": "client", "description": "App Engine GuestBook API", "icons": { "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", diff --git a/endpoints-framework-tools/src/test/java/com/google/api/server/spi/tools/GenClientSrcActionTest.java b/endpoints-framework-tools/src/test/java/com/google/api/server/spi/tools/GenClientSrcActionTest.java new file mode 100644 index 00000000..8c416d62 --- /dev/null +++ b/endpoints-framework-tools/src/test/java/com/google/api/server/spi/tools/GenClientSrcActionTest.java @@ -0,0 +1,73 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.api.server.spi.tools; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * Tests for {@link GenClientSrcAction}. + */ +@RunWith(JUnit4.class) +public class GenClientSrcActionTest extends EndpointsToolTest { + + private String language; + private String outputDirPath; + private String discoveryDocPath; + + @Override + protected void addTestAction(Map commands) { + commands.put(GenClientSrcAction.NAME, new GenClientSrcAction() { + + @Override + public Object genClientSrcFromFile(String l, String o, String d) { + language = l; + outputDirPath = o; + discoveryDocPath = d; + return null; + } + }); + } + + @Before + public void setUp() throws Exception { + super.setUp(); + + usagePrinted = false; + language = null; + outputDirPath = null; + discoveryDocPath = null; + } + + @Test + public void testGenClientSrc() throws Exception { + tool.execute( + new String[]{GenClientSrcAction.NAME, option(EndpointsToolAction.OPTION_LANGUAGE_SHORT), + "java", option(EndpointsToolAction.OPTION_OUTPUT_DIR_SHORT), "outputDir", + "discoveryDocPath"}); + assertFalse(usagePrinted); + assertEquals("java", language); + assertEquals("outputDir", outputDirPath); + assertEquals("discoveryDocPath", discoveryDocPath); + } +} diff --git a/endpoints-framework-tools/src/test/java/com/google/api/server/spi/tools/GetClientSrcActionTest.java b/endpoints-framework-tools/src/test/java/com/google/api/server/spi/tools/GetClientSrcActionTest.java new file mode 100644 index 00000000..d6654855 --- /dev/null +++ b/endpoints-framework-tools/src/test/java/com/google/api/server/spi/tools/GetClientSrcActionTest.java @@ -0,0 +1,158 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.api.server.spi.tools; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import static com.google.common.truth.Truth.assertThat; +import static java.util.Collections.singletonList; + +import java.io.File; +import java.net.URL; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import com.google.common.collect.Lists; + +/** + * Tests for {@link GetClientSrcAction}. + */ +@RunWith(JUnit4.class) +public class GetClientSrcActionTest extends EndpointsToolTest { + + private URL[] classPath; + private String language; + private String outputDirPath; + private List serviceClassNames; + private boolean debugOutput; + private String hostname; + private String basePath; + private GetClientSrcAction testAction; + + @Override + protected void addTestAction(Map commands) { + commands.put(GetClientSrcAction.NAME, testAction); + } + + @Before + public void setUp() throws Exception { + super.setUp(); + + usagePrinted = false; + classPath = null; + language = null; + outputDirPath = null; + serviceClassNames = null; + hostname = null; + basePath = null; + testAction = new GetClientSrcAction() { + + @Override + public Object getClientSrc( + URL[] c, String l, String o, List s, String h, String bp, boolean d) { + classPath = c; + language = l; + outputDirPath = o; + serviceClassNames = s; + hostname = h; + basePath = bp; + debugOutput = d; + return null; + } + }; + } + + @Test + public void testMissingOption() throws Exception { + tool.execute(new String[]{GetClientSrcAction.NAME, + option(EndpointsToolAction.OPTION_CLASS_PATH_SHORT), "classPath", + option(EndpointsToolAction.OPTION_HOSTNAME_SHORT), "foo.com", + "MyService"}); + assertFalse(usagePrinted); + assertThat(Lists.newArrayList(classPath)) + .containsExactly(new File("classPath").toURI().toURL(), + new File(new File(EndpointsToolAction.DEFAULT_WAR_PATH).getAbsoluteFile(), + "/WEB-INF/classes") + .toURI() + .toURL()); + assertEquals(EndpointsToolAction.DEFAULT_LANGUAGE, language); + assertEquals(EndpointsToolAction.DEFAULT_OUTPUT_PATH, outputDirPath); + assertStringsEqual(singletonList("MyService"), serviceClassNames); + } + + @Test + public void testMissingArgument() throws Exception { + tool.execute(new String[]{ + GetClientSrcAction.NAME, option(EndpointsToolAction.OPTION_CLASS_PATH_SHORT), + "classPath", option(EndpointsToolAction.OPTION_OUTPUT_DIR_SHORT), "outputDir"}); + assertTrue(usagePrinted); + } + + @Test + public void testGetClientSrc() throws Exception { + tool.execute(new String[]{GetClientSrcAction.NAME, + option(EndpointsToolAction.OPTION_CLASS_PATH_SHORT), "classPath", + option(EndpointsToolAction.OPTION_LANGUAGE_SHORT), "java", + option(EndpointsToolAction.OPTION_OUTPUT_DIR_SHORT), "outputDir", + option(EndpointsToolAction.OPTION_HOSTNAME_SHORT), "foo.com", + "MyService", "MyService2"}); + assertFalse(usagePrinted); + assertThat(Lists.newArrayList(classPath)) + .containsExactly(new File("classPath").toURI().toURL(), + new File(new File(EndpointsToolAction.DEFAULT_WAR_PATH).getAbsoluteFile(), + "/WEB-INF/classes") + .toURI() + .toURL()); + assertEquals("java", language); + assertEquals("outputDir", outputDirPath); + assertStringsEqual(Arrays.asList("MyService", "MyService2"), serviceClassNames); + assertFalse(debugOutput); + assertThat(basePath).isEqualTo("/_ah/api"); + } + + @Test + public void testGetClientSrcWithDebugOutput() throws Exception { + tool.execute(new String[]{GetClientSrcAction.NAME, + option(EndpointsToolAction.OPTION_CLASS_PATH_SHORT), "classPath", + option(EndpointsToolAction.OPTION_LANGUAGE_SHORT), "java", + option(EndpointsToolAction.OPTION_OUTPUT_DIR_SHORT), "outputDir", + option(EndpointsToolAction.OPTION_DEBUG, false), + option(EndpointsToolAction.OPTION_HOSTNAME_SHORT), "foo.com", + option(EndpointsToolAction.OPTION_BASE_PATH_SHORT), "/api", + "MyService", "MyService2"}); + assertFalse(usagePrinted); + assertThat(Lists.newArrayList(classPath)) + .containsExactly(new File("classPath").toURI().toURL(), + new File(new File(EndpointsToolAction.DEFAULT_WAR_PATH).getAbsoluteFile(), + "/WEB-INF/classes") + .toURI() + .toURL()); + assertEquals("java", language); + assertEquals("outputDir", outputDirPath); + assertStringsEqual(Arrays.asList("MyService", "MyService2"), serviceClassNames); + assertTrue(debugOutput); + assertThat(hostname).isEqualTo("foo.com"); + assertThat(basePath).isEqualTo("/api"); + } +} diff --git a/endpoints-framework-tools/src/test/java/com/google/api/server/spi/tools/GetOpenApiDocActionTest.java b/endpoints-framework-tools/src/test/java/com/google/api/server/spi/tools/GetOpenApiDocActionTest.java index aa37e6b1..f4692c15 100644 --- a/endpoints-framework-tools/src/test/java/com/google/api/server/spi/tools/GetOpenApiDocActionTest.java +++ b/endpoints-framework-tools/src/test/java/com/google/api/server/spi/tools/GetOpenApiDocActionTest.java @@ -20,6 +20,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import com.google.api.server.spi.swagger.SwaggerGenerator.SwaggerContext; import com.google.appengine.tools.util.Option; import com.google.common.collect.Lists; @@ -44,7 +45,11 @@ public class GetOpenApiDocActionTest extends EndpointsToolTest { private String outputFilePath; private String basePath; private List serviceClassNames; + private String tagTemplate; + private String operationIdTemplate; private boolean outputToDisk; + private boolean addGoogleJsonErrorAsDefaultResponse; + private boolean addErrorCodesForServiceExceptionsOption; @Override protected void addTestAction(Map actions) { @@ -53,12 +58,24 @@ protected void addTestAction(Map actions) { @Override public String genOpenApiDoc( URL[] classPath, String outputFilePath, String hostname, String basePath, + String title, String description, String apiName, + String tagTemplate, String operationIdTemplate, + boolean addGoogleJsonErrorAsDefaultResponse, + boolean addErrorCodesForServiceExceptionsOption, + boolean extractCommonParametersAsRefsOption, + boolean combineCommonParametersInSamePathOption, List serviceClassNames, boolean outputToDisk) { GetOpenApiDocActionTest.this.classPath = classPath; GetOpenApiDocActionTest.this.outputFilePath = outputFilePath; GetOpenApiDocActionTest.this.basePath = basePath; GetOpenApiDocActionTest.this.serviceClassNames = serviceClassNames; + GetOpenApiDocActionTest.this.tagTemplate = tagTemplate; + GetOpenApiDocActionTest.this.operationIdTemplate = operationIdTemplate; GetOpenApiDocActionTest.this.outputToDisk = outputToDisk; + GetOpenApiDocActionTest.this.addGoogleJsonErrorAsDefaultResponse + = addGoogleJsonErrorAsDefaultResponse; + GetOpenApiDocActionTest.this.addErrorCodesForServiceExceptionsOption + = addErrorCodesForServiceExceptionsOption; return null; } @@ -84,7 +101,10 @@ public void setUp() throws Exception { public void testGetOpenApiDoc() throws Exception { tool.execute( new String[]{GetOpenApiDocAction.NAME, option(EndpointsToolAction.OPTION_CLASS_PATH_SHORT), - "classPath", option(EndpointsToolAction.OPTION_OUTPUT_DIR_SHORT), "outputDir", "MyService", + "classPath", option(EndpointsToolAction.OPTION_OUTPUT_DIR_SHORT), "outputDir", + option("addGoogleJsonErrorAsDefaultResponse", false), + option("tt"), "myCustomTemplate", + "MyService", "MyService2"}); assertFalse(usagePrinted); assertThat(Lists.newArrayList(classPath)) @@ -95,6 +115,10 @@ public void testGetOpenApiDoc() throws Exception { .toURL()); assertEquals("outputDir", outputFilePath); assertEquals(EndpointsToolAction.DEFAULT_BASE_PATH, basePath); + assertTrue(addGoogleJsonErrorAsDefaultResponse); + assertFalse(addErrorCodesForServiceExceptionsOption); + assertEquals("myCustomTemplate", tagTemplate); + assertEquals(SwaggerContext.DEFAULT_OPERATION_ID_TEMPLATE, operationIdTemplate); assertStringsEqual(Arrays.asList("MyService", "MyService2"), serviceClassNames); assertTrue(outputToDisk); } diff --git a/endpoints-framework-tools/src/test/java/com/google/api/server/spi/tools/LocalClientLibGeneratorTest.java b/endpoints-framework-tools/src/test/java/com/google/api/server/spi/tools/LocalClientLibGeneratorTest.java new file mode 100644 index 00000000..d59de336 --- /dev/null +++ b/endpoints-framework-tools/src/test/java/com/google/api/server/spi/tools/LocalClientLibGeneratorTest.java @@ -0,0 +1,130 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.api.server.spi.tools; + +import static org.junit.Assume.assumeTrue; + +import static com.google.api.server.spi.tools.LocalClientLibGenerator.GENERATOR_EXECUTABLE; + +import java.io.File; +import java.io.IOException; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import com.google.api.server.spi.IoUtil; +import com.google.api.server.spi.tools.testing.FakeClientLibGenerator; + +public class LocalClientLibGeneratorTest { + + @Rule + public TemporaryFolder tmpFolder = new TemporaryFolder(); + + private LocalClientLibGenerator generator; + private String discoveryDoc; + + @Before + public void init() throws Exception { + generator = new LocalClientLibGenerator(); + discoveryDoc = IoUtil.readStream(FakeClientLibGenerator.class.getResourceAsStream("fake-discovery-doc-rest.json")); + } + + @Test + public void testJavaCodeGeneration() throws Exception { + assumeTrue(isToolInstalled()); + + File destinationDir = tmpFolder.newFolder("destination"); + generator.generateClientLib(discoveryDoc, "java", null, null, destinationDir); + File com = new File(destinationDir, "com"); + Assert.assertTrue(com.isDirectory()); + File google = new File(com, "google"); + Assert.assertTrue(google.isDirectory()); + File client = new File(google, "client"); + Assert.assertTrue(client.isDirectory()); + File guestbook = new File(client, "guestbook"); + Assert.assertTrue(guestbook.isDirectory()); + File v1 = new File(guestbook, "v1"); + Assert.assertTrue(guestbook.isDirectory()); + File model = new File(v1, "model"); + Assert.assertTrue(model.isDirectory()); + File guestbookJava = new File(v1, "Guestbook.java"); + Assert.assertTrue(guestbookJava.isFile()); + File guestbookRequestJava = new File(v1, "GuestbookRequest.java"); + Assert.assertTrue(guestbookRequestJava.isFile()); + File guestbookRequestInitializerJava = new File(v1, "GuestbookRequestInitializer.java"); + Assert.assertTrue(guestbookRequestInitializerJava.isFile()); + File greetingJava = new File(model, "Greeting.java"); + Assert.assertTrue(greetingJava.isFile()); + } + + @Test + public void testDestinationDirectoryCreation() throws Exception { + assumeTrue(isToolInstalled()); + + File destinationParent = tmpFolder.newFolder("destination"); + File dir1 = new File(destinationParent, "d1"); + File destinationDir = new File(dir1, "d2"); + Assert.assertFalse(dir1.exists()); + Assert.assertFalse(destinationDir.exists()); + generator.generateClientLib(discoveryDoc, "java", null, null, destinationDir); + Assert.assertTrue(dir1.exists()); + Assert.assertTrue(destinationDir.exists()); + } + + @Test + public void testInvalidDiscoveryFile_fails() throws Exception { + assumeTrue(isToolInstalled()); + + discoveryDoc = IoUtil.readStream(FakeClientLibGenerator.class.getResourceAsStream("fake-api-config.json")); + File destinationDir = tmpFolder.newFolder("destination"); + Assert.assertThrows(IOException.class, () -> + generator.generateClientLib(discoveryDoc, "java", null, null, destinationDir) + ); + } + + @Test + public void testDestinationIsFile_fails() throws Exception { + assumeTrue(isToolInstalled()); + + File destinationFile = tmpFolder.newFile("destination"); + Assert.assertThrows(IOException.class, () -> + generator.generateClientLib(discoveryDoc, "java", null, null, destinationFile) + ); + } + + @Test + public void testLanguageUnsupported() throws Exception { + assumeTrue(isToolInstalled()); + + IllegalArgumentException e = Assert.assertThrows(IllegalArgumentException.class, () -> + generator.generateClientLib(discoveryDoc, "python", null, null, tmpFolder.getRoot()) + ); + Assert.assertEquals("Unsupported language: python", e.getMessage()); + } + + /** + * Checks if the generator is installed. + */ + private boolean isToolInstalled() { + try { + Runtime.getRuntime().exec(GENERATOR_EXECUTABLE); + return true; + } catch (IOException e) { + return false; + } + } +} diff --git a/endpoints-framework/build.gradle b/endpoints-framework/build.gradle index 1645cea6..932197f1 100644 --- a/endpoints-framework/build.gradle +++ b/endpoints-framework/build.gradle @@ -13,14 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - plugins { - id 'net.ltgt.apt' version '0.8' + id 'java-library' } configurations { include - compile.extendsFrom include + compileOnly.extendsFrom include } jar { @@ -47,7 +46,6 @@ def annotations = [ "ApiTransformer.java", "Authenticator.java", "DefaultValue.java", - "PeerAuthenticator.java", "AuthLevel.java", "Transformer.java", ] @@ -73,47 +71,55 @@ task copyTestResources(type: Copy) { processTestResources.dependsOn copyTestResources dependencies { - include(project(":discovery-client")) { + include(project(":discovery-client")) { // We already include all of the dependencies needed for the discovery // client, and they are often newer versions. Leaving this out can cause // Android Studio to confuse different versions of Jackson 2 and Guava, // leading to test failures. transitive = false } - compile group: 'com.google.guava', name: 'guava', version: guavaVersion - compile group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: jacksonVersion - compile group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: jacksonVersion - compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: jacksonVersion - compile group: 'com.google.appengine', name: 'appengine-api-1.0-sdk', version: appengineVersion - compile group: 'com.google.flogger', name: 'flogger', version: floggerVersion - runtime group: 'com.google.flogger', name: 'flogger-system-backend', version: floggerVersion - compile(group: 'com.google.http-client', name: 'google-http-client-jackson2', version: apiclientVersion) { - exclude group: 'com.google.guava', module: 'guava-jdk5' + api (group: 'com.google.guava', name: 'guava', version: guavaVersion) { force = true } + api (group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: jacksonVersion) + api (group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: jacksonVersion) + api (group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: jacksonVersion) + api group: 'com.fasterxml.jackson.module', name: 'jackson-module-parameter-names', version: jacksonVersion + api group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jdk8', version: jacksonVersion + api group: 'com.google.appengine', name: 'appengine-api-1.0-sdk', version: appengineVersion + api group: 'com.google.flogger', name: 'flogger', version: floggerVersion + implementation group: 'com.google.flogger', name: 'flogger-system-backend', version: floggerVersion + api(group: 'com.google.http-client', name: 'google-http-client-jackson2', version: httpClientVersion) { exclude group: 'com.fasterxml.jackson.core', module: 'jackson-core' } - compile(group: 'com.google.api-client', name: 'google-api-client', version: apiclientVersion) { - exclude group: 'com.google.guava', module: 'guava-jdk5' - } - compile(group: 'com.google.api-client', name: 'google-api-client-appengine', version: apiclientVersion) { - exclude group: 'com.google.guava', module: 'guava-jdk5' + api(group: 'com.google.api-client', name: 'google-api-client', version: apiclientVersion) + api(group: 'com.google.api-client', name: 'google-api-client-appengine', version: apiclientVersion) + compileOnly group: 'com.google.code.findbugs', name: 'jsr305', version: findbugsVersion + api group: 'commons-fileupload', name: 'commons-fileupload', version: fileUploadVersion + api group: 'io.swagger', name: 'swagger-models', version: swaggerVersion + api(group: 'io.swagger', name: 'swagger-core', version: swaggerVersion) { + exclude group: 'javax.validation', module: 'validation-api' } - compile group: 'com.google.code.findbugs', name: 'jsr305', version: findbugsVersion - compile group: 'commons-fileupload', name: 'commons-fileupload', version: fileUploadVersion - compile group: 'io.swagger', name: 'swagger-models', version: swaggerVersion - compile group: 'io.swagger', name: 'swagger-core', version: swaggerVersion - compile group: 'org.slf4j', name: 'slf4j-nop', version: slf4jVersion + compileOnly group: 'org.slf4j', name: 'slf4j-nop', version: slf4jVersion compileOnly group: 'javax.servlet', name: 'servlet-api', version: servletVersion - compileOnly "com.google.auto.value:auto-value:1.2" - apt "com.google.auto.value:auto-value:1.2" + compileOnly group: 'com.google.auto.value', name: 'auto-value-annotations', version: autoValueVersion + annotationProcessor group: 'com.google.auto.value', name: 'auto-value', version: autoValueVersion - testCompile project(':test-utils') - testCompile group: 'junit', name: 'junit', version: junitVersion - testCompile group: 'org.mockito', name: 'mockito-core', version: mockitoVersion - testCompile group: 'com.google.truth', name: 'truth', version: truthVersion - testCompile group: 'com.google.appengine', name: 'appengine-testing', version: appengineVersion - testCompile group: 'com.google.appengine', name: 'appengine-api-stubs', version: appengineVersion - testCompile group: 'org.springframework', name: 'spring-test', version: springtestVersion - testCompile group: 'com.google.guava', name: 'guava-testlib', version: guavaVersion -} + api group: 'org.hibernate.validator', name: 'hibernate-validator', version: hibernateValidatorVersion + api group: 'jakarta.validation', name: 'jakarta.validation-api', version: validationApiVersion + testImplementation project(':test-utils') + testImplementation project(':discovery-client') + testImplementation group: 'junit', name: 'junit', version: junitVersion + testImplementation group: 'org.mockito', name: 'mockito-core', version: mockitoVersion + testImplementation group: 'org.skyscreamer', name: 'jsonassert', version: jsonassertVersion + testImplementation group: 'com.google.truth', name: 'truth', version: truthVersion + testImplementation group: 'com.google.appengine', name: 'appengine-testing', version: appengineVersion + testImplementation group: 'com.google.appengine', name: 'appengine-api-stubs', version: appengineVersion + testImplementation group: 'org.springframework', name: 'spring-test', version: springtestVersion + testImplementation group: 'com.google.guava', name: 'guava-testlib', version: guavaVersion + testImplementation (group: 'io.swagger', name: 'swagger-validator', version: '1.0.7') { + exclude group: 'javax.servlet', module: 'javax.servlet-api' + exclude group: 'javax.validation', module: 'validation-api' + exclude group: 'ch.qos.logback', module: 'logback-classic' + } +} diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/ConfiguredObjectMapper.java b/endpoints-framework/src/main/java/com/google/api/server/spi/ConfiguredObjectMapper.java index 03db8af8..df3a1ec5 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/ConfiguredObjectMapper.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/ConfiguredObjectMapper.java @@ -20,21 +20,23 @@ import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Maps; +import com.google.common.flogger.FluentLogger; -import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonInclude.Value; import com.fasterxml.jackson.databind.Module; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectReader; import com.fasterxml.jackson.databind.ObjectWriter; -import com.fasterxml.jackson.databind.SerializationFeature; +import javax.annotation.Nullable; -import com.google.common.flogger.FluentLogger; import java.util.Map; import java.util.Objects; -import java.util.logging.Level; -import java.util.logging.Logger; - -import javax.annotation.Nullable; +import java.util.Optional; +import java.util.OptionalDouble; +import java.util.OptionalInt; +import java.util.OptionalLong; +import java.util.stream.Stream; /** * A wrapper around an {@link ObjectMapper} with a frozen configuration. This exposes a subset of @@ -137,6 +139,7 @@ public Builder addRegisteredModules(Iterable modules) { /** * Builds a {@link ConfiguredObjectMapper} using the configuration specified in this builder. + * Returns a cached instance unsing {@link ApiSerializationConfig} and modules as key. * * @return the constructed object */ @@ -146,8 +149,16 @@ public ConfiguredObjectMapper build() { if (instance == null) { ObjectMapper mapper = ObjectMapperUtil.createStandardObjectMapper(key.apiSerializationConfig); - mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); - mapper.disable(SerializationFeature.WRITE_EMPTY_JSON_ARRAYS); + mapper.setDefaultPropertyInclusion(Include.NON_EMPTY); + Stream.of( + //empty Strings must be serialized + String.class, + //Empty optionals should serialized by default + Optional.class, OptionalLong.class, OptionalDouble.class, OptionalInt.class) + .forEach(clazz -> mapper.configOverride(clazz) + .setIncludeAsProperty(Value.construct(Include.NON_NULL, Include.USE_DEFAULTS))); + mapper.configOverride(Map.class) + .setIncludeAsProperty(Value.construct(Include.USE_DEFAULTS, Include.NON_NULL)); for (Module module : key.modulesSet) { mapper.registerModule(module); } @@ -165,6 +176,20 @@ public ConfiguredObjectMapper build() { } return instance; } + + /** + * Builds a {@link ConfiguredObjectMapper} using the configuration specified in this builder, + * and a customized ObjectMapper instance. Returned instance is NOT cached. + * + * @param mapper low-level Jackson {@link ObjectMapper} + * @return the constructed object + */ + public ConfiguredObjectMapper buildWithCustomMapper(ObjectMapper mapper) { + for (Module module : modules.build()) { + mapper.registerModule(module); + } + return new ConfiguredObjectMapper(mapper); + } } // A key that uniquely identify a cached object mapper. diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/Constant.java b/endpoints-framework/src/main/java/com/google/api/server/spi/Constant.java index 75c9d076..3c77cbb9 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/Constant.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/Constant.java @@ -55,18 +55,23 @@ private Constant() {} public static final String SKIP_CLIENT_ID_CHECK = "*"; /** - * Root URL of discovery doc generation API. This is on a host that Endpoints project owns so - * that even if producer has not picked an App Engine app host, this call can still succeed. + * Friendly name to refer to Google ID token authentication with accounts.google.com issuer. */ - public static final String DISCOVERY_GEN_ROOT = "https://webapis-discovery.appspot.com/_ah/api"; + public static final String GOOGLE_ID_TOKEN_ALT = "google_id_token_legacy"; /** - * Friendly name to refer to Google ID token authentication with accounts.google.com issuer. + * Google ID token authentication variant with https://accounts.google.com issuer. */ public static final String GOOGLE_ID_TOKEN_NAME = "google_id_token"; /** - * Google ID token authentication variant with https://accounts.google.com issuer. + * Google JWKS URI + */ + public static final String GOOGLE_JWKS_URI = "https://www.googleapis.com/oauth2/v1/certs"; + + /** + * Google OAuth2 authentication URL */ - public static final String GOOGLE_ID_TOKEN_NAME_HTTPS = "google_id_token_https"; + public static final String GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth"; + } diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/EndpointsServlet.java b/endpoints-framework/src/main/java/com/google/api/server/spi/EndpointsServlet.java index 5f32bc6c..0c04d84e 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/EndpointsServlet.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/EndpointsServlet.java @@ -117,7 +117,8 @@ private PathDispatcher createDispatcher() { builder.add(handler.getRestMethod(), Strings.stripTrailingSlash(handler.getRestPath()), handler.getRestHandler()); } - ExplorerHandler explorerHandler = new ExplorerHandler(); + String apiExplorerUrlTemplate = initParameters.getApiExplorerUrlTemplate(); + ExplorerHandler explorerHandler = new ExplorerHandler(apiExplorerUrlTemplate); builder.add("GET", EXPLORER_PATH, explorerHandler); builder.add("GET", EXPLORER_PATH + "/", explorerHandler); builder.add("GET", "static/proxy.html", new ApiProxyHandler()); diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/EnvUtil.java b/endpoints-framework/src/main/java/com/google/api/server/spi/EnvUtil.java index 49b9c2cb..6548ffdf 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/EnvUtil.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/EnvUtil.java @@ -21,6 +21,7 @@ */ public class EnvUtil { public static final String ENV_APPENGINE_RUNTIME = "com.google.appengine.runtime.environment"; + public static final String FORCE_AUTHENTICATION_ENABLED = "com.aodocs.auth.force.enabled"; public static final String ENV_APPENGINE_PROD = "Production"; private static final String ORIGINAL_APPENGINE_RUNTIME_ENV = System.getProperty(EnvUtil.ENV_APPENGINE_RUNTIME); @@ -56,4 +57,11 @@ public static boolean isRunningOnAppEngineProd() { String property = System.getProperty(ENV_APPENGINE_RUNTIME); return property != null && property.equals(ENV_APPENGINE_PROD); } + + /** + * Returns whether force authentication is enabled. + */ + public static boolean hasForceAuthenticationEnabled() { + return Boolean.parseBoolean(System.getProperty(FORCE_AUTHENTICATION_ENABLED)); + } } diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/IoUtil.java b/endpoints-framework/src/main/java/com/google/api/server/spi/IoUtil.java index f8faa647..2862d24f 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/IoUtil.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/IoUtil.java @@ -30,6 +30,7 @@ import java.io.PushbackInputStream; import java.io.RandomAccessFile; import java.net.URL; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.zip.GZIPInputStream; @@ -55,7 +56,7 @@ private IoUtil() {} public static String readResourceFile(Class c, String fileName) throws IOException { URL url = c.getResource(fileName); StringBuilder sb = new StringBuilder(); - BufferedReader in = new BufferedReader(new InputStreamReader(url.openStream())); + BufferedReader in = new BufferedReader(new InputStreamReader(url.openStream(), StandardCharsets.UTF_8)); for (String line = in.readLine(); line != null; line = in.readLine()) { sb.append(line); } diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/MethodHierarchyReader.java b/endpoints-framework/src/main/java/com/google/api/server/spi/MethodHierarchyReader.java index 4f909e0d..1f551157 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/MethodHierarchyReader.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/MethodHierarchyReader.java @@ -15,16 +15,20 @@ */ package com.google.api.server.spi; +import com.google.api.server.spi.EndpointMethod.ResolvedSignature; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.ListMultimap; +import com.google.common.collect.Multimap; import com.google.common.reflect.TypeToken; import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.util.Collection; import java.util.List; import java.util.Map; @@ -42,7 +46,7 @@ public class MethodHierarchyReader { private final Class endpointClass; // Map from method signatures to a list of method overrides for that signature (ordered // subclass -> superclass). - private ListMultimap endpointMethods; + private Multimap endpointMethods; /** * Constructs a {@code MethodHierarchyReader} for the given class type. @@ -60,8 +64,8 @@ public MethodHierarchyReader(Class endpointClass) { private void readMethodHierarchyIfNecessary() { if (endpointMethods == null) { - ImmutableListMultimap.Builder builder = - ImmutableListMultimap.builder(); + ImmutableMultimap.Builder builder = + ImmutableMultimap.builder(); buildServiceMethods(builder, TypeToken.of(endpointClass)); endpointMethods = builder.build(); } @@ -72,24 +76,10 @@ private void readMethodHierarchyIfNecessary() { * * @param overrides A list of method overrides ordered subclass -> superclass. */ - private EndpointMethod getLeafMethod(List overrides) { + private EndpointMethod getLeafMethod(Collection overrides) { // Because the list is ordered subclass -> superclass, index 0 will always contain the leaf // subclass implementation. - return overrides.get(0); - } - - /** - * Returns {@link ListMultimap#asMap multimap.asMap()}, with its type - * corrected from {@code Map>} to {@code Map>}. - */ - // Copied from com.google.common.collect.Multimaps. We can't use the actual method from - // that class as appengine build magic gives us an older version of guava that doesn't yet have - // this method. - // TODO: Switch to Multimaps.asMap() once it becomes available in appengine. - @SuppressWarnings("unchecked") - // safe by specification of ListMultimap.asMap() - private static Map> asMap(ListMultimap multimap) { - return (Map>) (Map) multimap.asMap(); + return overrides.iterator().next(); } /** @@ -99,7 +89,7 @@ private static Map> asMap(ListMultimap multimap) { public Iterable getLeafMethods() { readMethodHierarchyIfNecessary(); ImmutableList.Builder builder = ImmutableList.builder(); - for (List overrides : asMap(endpointMethods).values()) { + for (Collection overrides : endpointMethods.asMap().values()) { builder.add(getLeafMethod(overrides).getMethod()); } return builder.build(); @@ -113,7 +103,7 @@ public Iterable getLeafMethods() { public Iterable getLeafEndpointMethods() { readMethodHierarchyIfNecessary(); ImmutableList.Builder builder = ImmutableList.builder(); - for (List overrides : asMap(endpointMethods).values()) { + for (Collection overrides : endpointMethods.asMap().values()) { builder.add(getLeafMethod(overrides)); } return builder.build(); @@ -127,7 +117,7 @@ public Iterable getLeafEndpointMethods() { public Iterable> getMethodOverrides() { readMethodHierarchyIfNecessary(); ImmutableList.Builder> builder = ImmutableList.builder(); - for (List overrides : asMap(endpointMethods).values()) { + for (Collection overrides : endpointMethods.asMap().values()) { ImmutableList.Builder methodBuilder = ImmutableList.builder(); for (EndpointMethod method : overrides) { methodBuilder.add(method.getMethod()); @@ -142,9 +132,9 @@ public Iterable> getMethodOverrides() { * Bridge methods are ignored. For each method, all valid method implementations are included, * ordered subclass to superclass. Methods are stored in the EndpointMethod container. */ - public Iterable> getEndpointOverrides() { + public Iterable> getEndpointOverrides() { readMethodHierarchyIfNecessary(); - return asMap(endpointMethods).values(); + return endpointMethods.asMap().values(); } /** @@ -155,7 +145,7 @@ public Iterable> getEndpointOverrides() { public Map getNameToLeafMethodMap() { readMethodHierarchyIfNecessary(); ImmutableMap.Builder builder = ImmutableMap.builder(); - for (List overrides : asMap(endpointMethods).values()) { + for (Collection overrides : endpointMethods.asMap().values()) { Method leafMethod = getLeafMethod(overrides).getMethod(); builder.put(leafMethod.getName(), leafMethod); } @@ -170,7 +160,7 @@ public Map getNameToLeafMethodMap() { public ListMultimap getNameToEndpointOverridesMap() { readMethodHierarchyIfNecessary(); ImmutableListMultimap.Builder builder = ImmutableListMultimap.builder(); - for (List overrides : asMap(endpointMethods).values()) { + for (Collection overrides : endpointMethods.asMap().values()) { builder.putAll(getLeafMethod(overrides).getMethod().getName(), overrides); } return builder.build(); @@ -183,13 +173,14 @@ public ListMultimap getNameToEndpointOverridesMap() { * @param serviceType is the class object being inspected for service methods */ private void buildServiceMethods( - ImmutableListMultimap.Builder builder, + ImmutableMultimap.Builder builder, TypeToken serviceType) { for (TypeToken typeToken : serviceType.getTypes().classes()) { Class serviceClass = typeToken.getRawType(); if (Object.class.equals(serviceClass)) { return; } + //getDeclaredMethods returns methods in random order, so must not assume any specific order for (Method method : serviceClass.getDeclaredMethods()) { if (!isServiceMethod(method)) { continue; diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/ObjectMapperUtil.java b/endpoints-framework/src/main/java/com/google/api/server/spi/ObjectMapperUtil.java index 331dcc86..7ff6990b 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/ObjectMapperUtil.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/ObjectMapperUtil.java @@ -17,6 +17,7 @@ import com.google.api.server.spi.config.annotationreader.ApiAnnotationIntrospector; import com.google.api.server.spi.config.model.ApiSerializationConfig; +import com.google.api.server.spi.config.model.EndpointsFlag; import com.fasterxml.jackson.core.Base64Variants; import com.fasterxml.jackson.core.JsonGenerator; @@ -38,8 +39,9 @@ import com.fasterxml.jackson.databind.type.ArrayType; import com.fasterxml.jackson.databind.type.CollectionType; import com.fasterxml.jackson.databind.type.MapType; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; -import com.google.api.server.spi.config.model.EndpointsFlag; import java.io.IOException; import java.lang.reflect.Array; import java.util.Collection; @@ -66,6 +68,14 @@ public static ObjectMapper createStandardObjectMapper() { /** * Creates an Endpoints standard object mapper that allows unquoted field names and unknown * properties. + * + * Some Jackson Java 8 modules (ParameterNamesModule and Jdk8Module) are enabled. + * They provide the following features: + * - Simpler support for immutable objects through automatic detection of multi-param constructors + * - Support for Optional, that differentiates between null value of a field (maps to + * Optional.empty()) and a missing fields (maps to null). + * + * Support for JSR310 has been left out for now, as it's quite complex to configure properly. * * Note on unknown properties: When Apiary FE supports a strict mode where properties * are checked against the schema, BE can just ignore unknown properties. This way, FE does @@ -87,7 +97,9 @@ public static ObjectMapper createStandardObjectMapper(ApiSerializationConfig con new JacksonAnnotationIntrospector()) : new ApiAnnotationIntrospector(config); objectMapper.setAnnotationIntrospector(pair); - return objectMapper; + return objectMapper + .registerModule(new ParameterNamesModule()) + .registerModule(new Jdk8Module()); } /** diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/PeerAuth.java b/endpoints-framework/src/main/java/com/google/api/server/spi/PeerAuth.java deleted file mode 100644 index 0cfb9cef..00000000 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/PeerAuth.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright 2016 Google Inc. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.api.server.spi; - -import com.google.api.server.spi.auth.EndpointsPeerAuthenticator; -import com.google.api.server.spi.config.PeerAuthenticator; -import com.google.api.server.spi.config.Singleton; -import com.google.api.server.spi.config.model.ApiMethodConfig; -import com.google.api.server.spi.request.Attribute; -import com.google.common.annotations.VisibleForTesting; - -import javax.servlet.http.HttpServletRequest; - -/** - * Utilities used to do peer authorization. - */ -public class PeerAuth { - private static final Singleton.Instantiator INSTANTIATOR - = new Singleton.Instantiator(new EndpointsPeerAuthenticator()); - - /** - * Must be used to instantiate new {@link PeerAuthenticator}s to honor - * {@link com.google.api.server.spi.config.Singleton} contract. - * - * @return a new instance of clazz, or an existing one if clazz is annotated with @{@link - * com.google.api.server.spi.config.Singleton} - */ - public static PeerAuthenticator instantiatePeerAuthenticator(Class clazz) { - return INSTANTIATOR.getInstanceOrDefault(clazz); - } - - private final HttpServletRequest request; - private final Attribute attr; - private final ApiMethodConfig config; - - @VisibleForTesting - PeerAuth(HttpServletRequest request) { - this.request = request; - attr = Attribute.from(request); - config = attr.get(Attribute.API_METHOD_CONFIG); - } - - static PeerAuth from(HttpServletRequest request) { - return new PeerAuth(request); - } - - @VisibleForTesting - Iterable getPeerAuthenticatorInstances() { - return INSTANTIATOR.getInstancesOrDefault(config.getPeerAuthenticators()); - } - - boolean authorizePeer() { - if (!attr.isEnabled(Attribute.RESTRICT_SERVLET)) { - return true; - } - Iterable peerAuthenticators = getPeerAuthenticatorInstances(); - if (peerAuthenticators != null) { - for (PeerAuthenticator peerAuthenticator : peerAuthenticators) { - if (!peerAuthenticator.authenticate(request)) { - return false; - } - } - } - return true; - } -} diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/RequestUtil.java b/endpoints-framework/src/main/java/com/google/api/server/spi/RequestUtil.java new file mode 100644 index 00000000..e7617e6c --- /dev/null +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/RequestUtil.java @@ -0,0 +1,27 @@ +package com.google.api.server.spi; + +import javax.servlet.http.HttpServletRequest; + +public class RequestUtil { + + public static String getOriginalRequestUrl(HttpServletRequest req) { + String protocolHeader = req.getHeader("X-Forwarded-Proto"); + String requestUrl = stripRedundantPorts(req.getRequestURL().toString()); + if (protocolHeader != null && protocolHeader.equalsIgnoreCase("https")) { + requestUrl = requestUrl.replaceFirst("^http:", "https:"); + } + return requestUrl; + } + + private static String stripRedundantPorts(String url) { + if (url == null) { + return null; + } else if (url.startsWith("http:") && url.contains(":80/")) { + return url.replace(":80/", "/"); + } else if (url.startsWith("https:") && url.contains(":443/")) { + return url.replace(":443/", "/"); + } + return url; + } + +} diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/ServiceException.java b/endpoints-framework/src/main/java/com/google/api/server/spi/ServiceException.java index d90f97b8..b010f3f3 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/ServiceException.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/ServiceException.java @@ -15,6 +15,12 @@ */ package com.google.api.server.spi; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.google.common.flogger.FluentLogger; + +import java.util.Collections; +import java.util.HashMap; import java.util.Map; import java.util.logging.Level; @@ -24,10 +30,16 @@ */ public class ServiceException extends Exception { + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + + /** Reserved keywords, cannot be set as an extra field name. */ + public static final ImmutableList EXTRA_FIELDS_RESERVED_NAMES = ImmutableList.of("domain", "message", "reason"); + protected final int statusCode; protected final String reason; protected final String domain; protected Level logLevel; + private final Map extraFields = new HashMap<>(); public ServiceException(int statusCode, String statusMessage) { super(statusMessage); @@ -102,6 +114,84 @@ public Map getHeaders() { return null; } + /** + * Associates to this exception an extra field as a field name/value pair. If a field + * with the same name was previously set, the old value is replaced by the specified + * value. + * @return this + * @throws NullPointerException if {@code fieldName} is {@code null}. + * @throws IllegalArgumentException if {@code fieldName} is one of the reserved field + * names {@link #EXTRA_FIELDS_RESERVED_NAMES}. + */ + public ServiceException putExtraField(String fieldName, String value) { + return putExtraFieldInternal(fieldName, value); + } + + /** + * Associates to this exception an extra field as a field name/value pair. If a field + * with the same name was previously set, the old value is replaced by the specified + * value. + * @return this + * @throws NullPointerException if {@code fieldName} is {@code null}. + * @throws IllegalArgumentException if {@code fieldName} is one of the reserved field + * names {@link #EXTRA_FIELDS_RESERVED_NAMES}. + */ + public ServiceException putExtraField(String fieldName, Boolean value) { + return putExtraFieldInternal(fieldName, value); + } + + /** + * Associates to this exception an extra field as a field name/value pair. If a field + * with the same name was previously set, the old value is replaced by the specified + * value. + * @return this + * @throws NullPointerException if {@code fieldName} is {@code null}. + * @throws IllegalArgumentException if {@code fieldName} is one of the reserved field + * names {@link #EXTRA_FIELDS_RESERVED_NAMES}. + */ + public ServiceException putExtraField(String fieldName, Number value) { + return putExtraFieldInternal(fieldName, value); + } + + /** + * Associates to this exception an extra field as a field name/value pair. If a field + * with the same name was previously set, the old value is replaced by the specified + * value.
+ * This unsafe version accepts any POJO as is: + *
    + *
  • the object should be serializable
  • + *
  • no defensive copy nor conversion are made. So {@code value} should not be modified + * or reused after the call of this method.
  • + *
+ * These constraints must be taken into consideration when overriding this method. + * @return this + * @throws NullPointerException if {@code fieldName} is {@code null}. + * @throws IllegalArgumentException if {@code fieldName} is one of the reserved field + * names {@link #EXTRA_FIELDS_RESERVED_NAMES}. + */ + protected ServiceException putExtraFieldUnsafe(String fieldName, Object value) { + return putExtraFieldInternal(fieldName, value); + } + + private ServiceException putExtraFieldInternal(String fieldName, Object value) { + Preconditions.checkNotNull(fieldName); + Preconditions.checkArgument(!EXTRA_FIELDS_RESERVED_NAMES.contains(fieldName), "The field name '%s' is reserved", fieldName); + final Object previousValue = extraFields.put(fieldName, value); + if (previousValue != null) { + logger.atFine().log("Replaced extra field %s: %s => %s", fieldName, previousValue, value); + } + return this; + } + + /** + * Gets the extra fields. The extra fields are returned in an unmodifiable map, + * each field name/value pair is a map entry. The map is empty if no extra field + * has been added. + */ + public final Map getExtraFields() { + return Collections.unmodifiableMap(extraFields); + } + public Level getLogLevel() { return logLevel == null ? getDefaultLoggingLevel(statusCode) : logLevel; } diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/ServletInitializationParameters.java b/endpoints-framework/src/main/java/com/google/api/server/spi/ServletInitializationParameters.java index 9c6a5b7e..877d16ef 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/ServletInitializationParameters.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/ServletInitializationParameters.java @@ -19,10 +19,12 @@ import com.google.common.base.Function; import com.google.common.base.Joiner; import com.google.common.base.Splitter; -import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; +import java.util.HashMap; +import java.util.Map; +import javax.annotation.Nullable; import javax.servlet.ServletConfig; import javax.servlet.ServletException; @@ -39,6 +41,9 @@ public abstract class ServletInitializationParameters { private static final String EXCEPTION_COMPATIBILITY = "enableExceptionCompatibility"; private static final String PRETTY_PRINT = "prettyPrint"; private static final String ADD_CONTENT_LENGTH = "addContentLength"; + private static final String API_EXPLORER_URL_TEMPLATE = "apiExplorerUrlTemplate"; + private static final String PARAMETER_VALIDATION = "enableValidation"; + private static final String CONTENT_TYPE_VALIDATION = "enableContentTypeValidation"; private static final Splitter CSV_SPLITTER = Splitter.on(',').omitEmptyStrings().trimResults(); private static final Joiner CSV_JOINER = Joiner.on(',').skipNulls(); @@ -54,14 +59,6 @@ public String apply(Class clazz) { */ public abstract ImmutableSet> getServiceClasses(); - /** - * Returns if the SPI servlet is restricted. - * - * @deprecated No longer serves any purpose and will be removed in a future release - */ - @Deprecated - public abstract boolean isServletRestricted(); - /** * Returns if client ID whitelisting is enabled. */ @@ -92,14 +89,29 @@ public String apply(Class clazz) { */ public abstract boolean isAddContentLength(); + /** + * Returns whether the request parameter validation is enabled. + */ + public abstract boolean isParameterValidationEnabled(); + + /** + * Returns whether the request content type validation is enabled. + */ + public abstract boolean isContentTypeValidationEnabled(); + + @Nullable + public abstract String getApiExplorerUrlTemplate(); + public static Builder builder() { return new AutoValue_ServletInitializationParameters.Builder() - .setServletRestricted(true) .setClientIdWhitelistEnabled(true) .setIllegalArgumentBackendError(false) .setExceptionCompatibilityEnabled(true) .setPrettyPrintEnabled(true) - .setAddContentLength(false); + .setAddContentLength(false) + .setParameterValidationEnabled(true) + .setContentTypeValidationEnabled(false) + .setApiExplorerUrlTemplate(null); } /** @@ -130,24 +142,6 @@ public Builder addServiceClasses(Iterable> serviceClasses) { */ public abstract Builder setServiceClasses(ImmutableSet> clazzes); - /** - * Sets if the servlet is restricted. Defaults to {@code true}. - * - * @deprecated No longer serves any purpose and will be removed in a future release - */ - @Deprecated - public abstract Builder setServletRestricted(boolean servletRestricted); - - /** - * Sets if the servlet is restricted. Retained for API compatibility. - * - * @deprecated Retained for API compatibility - */ - @Deprecated - public Builder setRestricted(boolean servletRestricted) { - return setServletRestricted(servletRestricted); - } - /** * Sets if the client ID whitelist is enabled, defaulting to {@code true}. */ @@ -160,10 +154,14 @@ public Builder setRestricted(boolean servletRestricted) { public abstract Builder setIllegalArgumentBackendError(boolean illegalArgumentBackendError); /** - * Sets if v1.0 style exceptions should be returned to users. In v1.0, certain codes are not - * permissible, and other codes are translated to other status codes. Defaults to {@code true}. + * Sets if request parameter validation should be enabled. Defaults to {@code true}. */ - public abstract Builder setExceptionCompatibilityEnabled(boolean exceptionCompatibility); + public abstract Builder setParameterValidationEnabled(boolean enabledParameterValidation); + + /** + * Sets if request content type validation should be enabled. Defaults to {@code false}. + */ + public abstract Builder setContentTypeValidationEnabled(boolean enabledContentTypeValidation); /** * Sets if pretty printing should be enabled for responses by default. Defaults to {@code true}. @@ -175,6 +173,20 @@ public Builder setRestricted(boolean servletRestricted) { */ public abstract Builder setAddContentLength(boolean addContentLength); + /** + * Sets the url template for the API explorer. + * The only supported variable is ${apiBase}. + * Given https://myapp.com/_ah/api/explorer, ${apiBase} is set to https://myapp.com/_ah/api. + * Defaults to http://apis-explorer.appspot.com/apis-explorer/?base=${apiBase} if not set. + */ + public abstract Builder setApiExplorerUrlTemplate(String urlTemplate); + + /** + * Sets if v1.0 style exceptions should be returned to users. In v1.0, certain codes are not + * permissible, and other codes are translated to other status codes. Defaults to {@code true}. + */ + public abstract Builder setExceptionCompatibilityEnabled(boolean exceptionCompatibility); + abstract ServletInitializationParameters autoBuild(); public ServletInitializationParameters build() { @@ -195,10 +207,6 @@ public static ServletInitializationParameters fromServletConfig( builder.addServiceClass(getClassForName(serviceClassName, classLoader)); } } - String servletRestricted = config.getInitParameter(RESTRICTED); - if (servletRestricted != null) { - builder.setServletRestricted(parseBoolean(servletRestricted, RESTRICTED)); - } String clientIdWhitelist = config.getInitParameter(CLIENT_ID_WHITELIST_ENABLED); if (clientIdWhitelist != null) { builder.setClientIdWhitelistEnabled( @@ -222,6 +230,17 @@ public static ServletInitializationParameters fromServletConfig( if (addContentLength != null) { builder.setAddContentLength(parseBoolean(addContentLength, ADD_CONTENT_LENGTH)); } + String enabledParameterValidation = config.getInitParameter(PARAMETER_VALIDATION); + if (enabledParameterValidation != null) { + builder.setParameterValidationEnabled( + parseBoolean(enabledParameterValidation, PARAMETER_VALIDATION)); + } + String enabledContentTypeValidation = config.getInitParameter(CONTENT_TYPE_VALIDATION); + if (enabledContentTypeValidation != null) { + builder.setContentTypeValidationEnabled( + parseBoolean(enabledContentTypeValidation, CONTENT_TYPE_VALIDATION)); + } + builder.setApiExplorerUrlTemplate(config.getInitParameter(API_EXPLORER_URL_TEMPLATE)); } return builder.build(); } @@ -249,15 +268,17 @@ private static Class getClassForName(String className, ClassLoader classLoade /** * Returns the parameters as a {@link java.util.Map} of parameter name to {@link String} value. */ - public ImmutableMap asMap() { - return ImmutableMap.builder() - .put(SERVICES, CSV_JOINER.join(Iterables.transform(getServiceClasses(), CLASS_TO_NAME))) - .put(RESTRICTED, Boolean.toString(isServletRestricted())) - .put(CLIENT_ID_WHITELIST_ENABLED, Boolean.toString(isClientIdWhitelistEnabled())) - .put(ILLEGAL_ARGUMENT_BACKEND_ERROR, Boolean.toString(isIllegalArgumentBackendError())) - .put(EXCEPTION_COMPATIBILITY, Boolean.toString(isExceptionCompatibilityEnabled())) - .put(PRETTY_PRINT, Boolean.toString(isPrettyPrintEnabled())) - .put(ADD_CONTENT_LENGTH, Boolean.toString(isAddContentLength())) - .build(); + public Map asMap() { + return new HashMap() {{ + put(SERVICES, CSV_JOINER.join(Iterables.transform(getServiceClasses(), CLASS_TO_NAME))); + put(CLIENT_ID_WHITELIST_ENABLED, Boolean.toString(isClientIdWhitelistEnabled())); + put(ILLEGAL_ARGUMENT_BACKEND_ERROR, Boolean.toString(isIllegalArgumentBackendError())); + put(EXCEPTION_COMPATIBILITY, Boolean.toString(isExceptionCompatibilityEnabled())); + put(PRETTY_PRINT, Boolean.toString(isPrettyPrintEnabled())); + put(ADD_CONTENT_LENGTH, Boolean.toString(isAddContentLength())); + put(PARAMETER_VALIDATION, Boolean.toString(isParameterValidationEnabled())); + put(CONTENT_TYPE_VALIDATION, Boolean.toString(isContentTypeValidationEnabled())); + put(API_EXPLORER_URL_TEMPLATE, getApiExplorerUrlTemplate()); + }}; } } diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/SystemService.java b/endpoints-framework/src/main/java/com/google/api/server/spi/SystemService.java index 36873da8..fa8c4d83 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/SystemService.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/SystemService.java @@ -34,6 +34,7 @@ import com.google.api.server.spi.request.ParamReader; import com.google.api.server.spi.response.BadRequestException; import com.google.api.server.spi.response.InternalServerErrorException; +import com.google.api.server.spi.response.RedirectException; import com.google.api.server.spi.response.ResultWriter; import com.google.api.server.spi.response.UnauthorizedException; import com.google.common.base.Function; @@ -340,21 +341,23 @@ public Method findServiceMethod(Object service, String methodName) throws Servic * Invokes a {@code method} on a {@code service} given a {@code paramReader} to read parameters * and a {@code resultWriter} to write result. */ - public void invokeServiceMethod(Object service, Method method, ParamReader paramReader, - ResultWriter resultWriter) throws IOException { + public void invokeServiceMethod(Object service, Method method, int status, ParamReader paramReader, + ResultWriter resultWriter) throws IOException, RedirectException { try { Object[] params = paramReader.read(); logger.atFine().log("params=%s (String)", Arrays.toString(params)); Object response = method.invoke(service, params); - resultWriter.write(response); + resultWriter.write(response, status); } catch (IllegalArgumentException | IllegalAccessException e) { logger.atSevere().withCause(e).log("exception occurred while calling backend method"); resultWriter.writeError(new BadRequestException(e)); } catch (InvocationTargetException e) { Throwable cause = e.getCause(); Level level = Level.INFO; - if (cause instanceof ServiceException) { + if (cause instanceof RedirectException) { + throw (RedirectException) cause; + } else if (cause instanceof ServiceException) { resultWriter.writeError((ServiceException) cause); } else if (cause instanceof IllegalArgumentException) { resultWriter.writeError( diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/TypeLoader.java b/endpoints-framework/src/main/java/com/google/api/server/spi/TypeLoader.java index c93afe27..dbafdc95 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/TypeLoader.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/TypeLoader.java @@ -191,6 +191,18 @@ private static Map> createAnnotationTypes( loadAnnotation(classLoader, "com.google.api.server.spi.config.Named")); annotationTypes.put("Nullable", loadAnnotation(classLoader, "com.google.api.server.spi.config.Nullable")); + annotationTypes.put("Pattern", + loadAnnotation(classLoader, "jakarta.validation.constraints.Pattern")); + annotationTypes.put("Min", + loadAnnotation(classLoader, "jakarta.validation.constraints.Min")); + annotationTypes.put("Max", + loadAnnotation(classLoader, "jakarta.validation.constraints.Max")); + annotationTypes.put("DecimalMin", + loadAnnotation(classLoader, "jakarta.validation.constraints.DecimalMin")); + annotationTypes.put("DecimalMax", + loadAnnotation(classLoader, "jakarta.validation.constraints.DecimalMax")); + annotationTypes.put("Size", + loadAnnotation(classLoader, "jakarta.validation.constraints.Size")); return Collections.unmodifiableMap(annotationTypes); } diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/auth/EndpointsAuthenticator.java b/endpoints-framework/src/main/java/com/google/api/server/spi/auth/EndpointsAuthenticator.java index ecdb7f7e..b5c56b48 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/auth/EndpointsAuthenticator.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/auth/EndpointsAuthenticator.java @@ -38,7 +38,7 @@ public class EndpointsAuthenticator implements Authenticator { public EndpointsAuthenticator() { this.jwtAuthenticator = new GoogleJwtAuthenticator(); - this.appEngineAuthenticator = new GoogleAppEngineAuthenticator(); + this.appEngineAuthenticator = EnvUtil.isRunningOnAppEngine() ? new GoogleAppEngineAuthenticator() : null; this.oauth2Authenticator = new GoogleOAuth2Authenticator(); } diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/auth/EndpointsPeerAuthenticator.java b/endpoints-framework/src/main/java/com/google/api/server/spi/auth/EndpointsPeerAuthenticator.java deleted file mode 100644 index b803cc76..00000000 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/auth/EndpointsPeerAuthenticator.java +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright 2016 Google Inc. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.api.server.spi.auth; - -import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken; -import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier; -import com.google.api.client.googleapis.auth.oauth2.GooglePublicKeysManager; -import com.google.api.server.spi.Client; -import com.google.api.server.spi.EnvUtil; -import com.google.api.server.spi.config.PeerAuthenticator; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.ImmutableSet; - -import com.google.common.flogger.FluentLogger; -import java.io.IOException; -import java.net.InetAddress; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.logging.Logger; - -import javax.servlet.http.HttpServletRequest; - -/** - * The default peer authenticator. It verify the request is from Google Cloud Endpoints frontend. It - * is different from EndpointsAuthenticator, which authenticates the end user. - */ -public class EndpointsPeerAuthenticator implements PeerAuthenticator { - @VisibleForTesting - static final String ISSUER = "https://www.cloudendpointsapis.com"; - @VisibleForTesting - static final String SIGNER = "cloud-endpoints-signer@system.gserviceaccount.com"; - @VisibleForTesting - static final String HEADER_APPENGINE_PEER = "X-Appengine-Peer"; - @VisibleForTesting - static final String APPENGINE_PEER = "apiserving"; - @VisibleForTesting - static final String HEADER_PEER_AUTHORIZATION = "Peer-Authorization"; - - private static final String PUBLIC_CERT_URL = - "https://www.googleapis.com/service_accounts/v1/metadata/x509/" + SIGNER; - private static final FluentLogger logger = FluentLogger.forEnclosingClass(); - private static final ImmutableSet localHostAddresses = getLocalHostAddresses(); - - private final GoogleJwtAuthenticator jwtAuthenticator; - - private static ImmutableSet getLocalHostAddresses() { - ImmutableSet.Builder builder = new ImmutableSet.Builder<>(); - try { - builder.add(InetAddress.getLocalHost().getHostAddress()); - } catch (IOException e) { - // try next. - } - try { - builder.add(InetAddress.getByName(null).getHostAddress()); - } catch (IOException e) { - // try next. - } - try { - for (InetAddress inetAddress : InetAddress.getAllByName("localhost")) { - builder.add(inetAddress.getHostAddress()); - } - } catch (IOException e) { - // check at the end. - } - ImmutableSet localHostSet = builder.build(); - if (localHostSet.isEmpty()) { - logger.atWarning().log("Unable to lookup local addresses."); - } - return localHostSet; - } - - public EndpointsPeerAuthenticator() { - Client client = Client.getInstance(); - GooglePublicKeysManager keyManager = new GooglePublicKeysManager.Builder( - client.getHttpTransport(), client.getJsonFactory()).setPublicCertsEncodedUrl( - PUBLIC_CERT_URL).build(); - GoogleIdTokenVerifier verifier = - new GoogleIdTokenVerifier.Builder(keyManager).setIssuer(ISSUER).build(); - jwtAuthenticator = new GoogleJwtAuthenticator(verifier); - } - - @VisibleForTesting - public EndpointsPeerAuthenticator(GoogleJwtAuthenticator jwtAuthenticator) { - this.jwtAuthenticator = jwtAuthenticator; - } - - @Override - public boolean authenticate(HttpServletRequest request) { - // Preserve current check for App Engine Env. - if (EnvUtil.isRunningOnAppEngine()) { - return APPENGINE_PEER.equals(request.getHeader(HEADER_APPENGINE_PEER)); - } - - // Skip peer verification for localhost request. - if (localHostAddresses.contains(request.getRemoteAddr())) { - logger.atFine().log("Skip endpoints peer verication from localhost."); - return true; - } - // Verify peer token, signer and audience. - GoogleIdToken idToken = - jwtAuthenticator.verifyToken(request.getHeader(HEADER_PEER_AUTHORIZATION)); - if (idToken == null || !SIGNER.equals(idToken.getPayload().getEmail()) - || !matchHostAndPort(idToken, request)) { - return false; - } - return true; - } - - private boolean matchHostAndPort(GoogleIdToken idToken, HttpServletRequest request) { - URL urlFromIdToken; - URL urlFromRequest; - try { - urlFromIdToken = new URL((String) idToken.getPayload().getAudience()); - urlFromRequest = new URL(request.getRequestURL().toString()); - return urlFromIdToken.getHost().equals(urlFromRequest.getHost()) - && getPort(urlFromIdToken) == getPort(urlFromRequest); - } catch (MalformedURLException e) { - logger.atWarning().log("Invalid URL from request"); - return false; - } - } - - private int getPort(URL url) { - int port = url.getPort(); - return port == -1 ? url.getDefaultPort() : port; - } -} diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/auth/GoogleCustomIdToken.java b/endpoints-framework/src/main/java/com/google/api/server/spi/auth/GoogleCustomIdToken.java new file mode 100644 index 00000000..7f67fc49 --- /dev/null +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/auth/GoogleCustomIdToken.java @@ -0,0 +1,40 @@ +package com.google.api.server.spi.auth; + +import java.io.IOException; + +import com.google.api.client.auth.openidconnect.IdToken; +import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.json.webtoken.JsonWebSignature; +import com.google.api.client.util.Key; + +public class GoogleCustomIdToken extends GoogleIdToken { + + public GoogleCustomIdToken(Header header, Payload payload, byte[] signatureBytes, byte[] signedContentBytes) { + super(header, payload, signatureBytes, signedContentBytes); + } + + public static GoogleCustomIdToken parse(JsonFactory jsonFactory, String idTokenString) throws IOException { + JsonWebSignature jws = JsonWebSignature.parser(jsonFactory).setPayloadClass(Payload.class).parse(idTokenString); + return new GoogleCustomIdToken(jws.getHeader(), (Payload) jws.getPayload(), jws.getSignatureBytes(), jws.getSignedContentBytes()); + } + + public GoogleCustomIdToken.Payload getPayload() { + return (GoogleCustomIdToken.Payload)super.getPayload(); + } + + public static class Payload extends GoogleIdToken.Payload { + + @Key("primaryEmail") + private String primaryEmail; + + public String getPrimaryEmail() { + return primaryEmail; + } + + public void setPrimaryEmail(String primaryEmail) { + this.primaryEmail = primaryEmail; + } + + } +} diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/auth/GoogleCustomIdTokenVerifier.java b/endpoints-framework/src/main/java/com/google/api/server/spi/auth/GoogleCustomIdTokenVerifier.java new file mode 100644 index 00000000..6a46d9c5 --- /dev/null +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/auth/GoogleCustomIdTokenVerifier.java @@ -0,0 +1,69 @@ +package com.google.api.server.spi.auth; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.util.Collection; + +import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken; +import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier; +import com.google.api.client.googleapis.auth.oauth2.GooglePublicKeysManager; +import com.google.api.client.http.HttpTransport; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.util.Clock; + +public class GoogleCustomIdTokenVerifier extends GoogleIdTokenVerifier { + + public GoogleCustomIdTokenVerifier(HttpTransport transport, JsonFactory jsonFactory) { + super(transport, jsonFactory); + } + protected GoogleCustomIdTokenVerifier(GoogleCustomIdTokenVerifier.Builder builder) { + super(builder); + } + public GoogleCustomIdTokenVerifier(GooglePublicKeysManager publicKeys) { + this(new GoogleCustomIdTokenVerifier.Builder(publicKeys)); + } + + + + public static class Builder extends GoogleIdTokenVerifier.Builder { + + public Builder(HttpTransport transport, JsonFactory jsonFactory) { + super(transport, jsonFactory); + } + public Builder(GooglePublicKeysManager publicKeys) { + super(publicKeys); + } + + @Override + public GoogleCustomIdTokenVerifier.Builder setIssuers(Collection issuers) { + return (Builder) super.setIssuers(issuers); + } + + @Override + public GoogleCustomIdTokenVerifier.Builder setAudience(Collection audience) { + return (Builder) super.setAudience(audience); + } + + @Override + public GoogleCustomIdTokenVerifier.Builder setClock(Clock clock) { + return (Builder) super.setClock(clock); + } + + @Override + public GoogleCustomIdTokenVerifier.Builder setIssuer(String issuer) { + return (Builder) super.setIssuer(issuer); + } + + public GoogleCustomIdTokenVerifier build() { + return new GoogleCustomIdTokenVerifier(this); + } + + } + + @Override + public GoogleCustomIdToken verify(String idTokenString) throws GeneralSecurityException, IOException { + GoogleCustomIdToken idToken = GoogleCustomIdToken.parse(this.getJsonFactory(), idTokenString); + return this.verify(idToken) ? idToken : null; + } + +} diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/auth/GoogleJwtAuthenticator.java b/endpoints-framework/src/main/java/com/google/api/server/spi/auth/GoogleJwtAuthenticator.java index f24fdea5..bf5dba79 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/auth/GoogleJwtAuthenticator.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/auth/GoogleJwtAuthenticator.java @@ -15,8 +15,6 @@ */ package com.google.api.server.spi.auth; -import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken; -import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier; import com.google.api.server.spi.Client; import com.google.api.server.spi.auth.common.User; import com.google.api.server.spi.config.Authenticator; @@ -35,19 +33,19 @@ @Singleton public class GoogleJwtAuthenticator implements Authenticator { private static final FluentLogger logger = FluentLogger.forEnclosingClass(); - private final GoogleIdTokenVerifier verifier; + private final GoogleCustomIdTokenVerifier verifier; public GoogleJwtAuthenticator() { - this(new GoogleIdTokenVerifier.Builder(Client.getInstance().getHttpTransport(), + this(new GoogleCustomIdTokenVerifier.Builder(Client.getInstance().getHttpTransport(), Client.getInstance().getJsonFactory()).build()); } - public GoogleJwtAuthenticator(GoogleIdTokenVerifier verifier) { + public GoogleJwtAuthenticator(GoogleCustomIdTokenVerifier verifier) { this.verifier = verifier; } @VisibleForTesting - GoogleIdToken verifyToken(String token) { + GoogleCustomIdToken verifyToken(String token) { if (token == null) { return null; } @@ -71,7 +69,7 @@ public User authenticate(HttpServletRequest request) { return null; } - GoogleIdToken idToken = verifyToken(token); + GoogleCustomIdToken idToken = verifyToken(token); if (idToken == null) { return null; } @@ -94,7 +92,7 @@ public User authenticate(HttpServletRequest request) { logger.atWarning().log("Audience is not allowed: %s", audience); return null; } - + String userId = idToken.getPayload().getSubject(); String email = idToken.getPayload().getEmail(); User user = (userId == null && email == null) ? null : new User(userId, email); @@ -102,9 +100,9 @@ public User authenticate(HttpServletRequest request) { com.google.appengine.api.users.User appEngineUser = (email == null) ? null : new com.google.appengine.api.users.User(email, ""); attr.set(Attribute.AUTHENTICATED_APPENGINE_USER, appEngineUser); - logger.atFine().log("appEngineUser = %s", appEngineUser); + logger.atInfo().log("appEngineUser = %s", appEngineUser); } else { - logger.atFine().log("user = %s", user); + logger.atInfo().log("user = %s", user); } return user; } diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/config/Api.java b/endpoints-framework/src/main/java/com/google/api/server/spi/config/Api.java index 3edbf46f..20bd2d06 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/config/Api.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/config/Api.java @@ -26,16 +26,8 @@ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface Api { - public static final int UNSPECIFIED_INT = Integer.MIN_VALUE; - public static final String UNSPECIFIED_STRING_FOR_LIST = "_UNSPECIFIED_LIST_STRING_VALUE"; - - /** - * Frontend root URL, e.g. "https://example.appspot.com/_ah/api". All api - * methods will be exposed below this path. This will default to - * "https://yourapp.appspot.com/_ah/api". - */ - @Deprecated - String root() default ""; + int UNSPECIFIED_INT = Integer.MIN_VALUE; + String UNSPECIFIED_STRING_FOR_LIST = "_UNSPECIFIED_LIST_STRING_VALUE"; /** * Name of the API, e.g. "guestbook". This is used as the prefix for all api @@ -73,14 +65,6 @@ */ String documentationLink() default ""; - /** - * Backend root URL, e.g. "https://example.appspot.com/_ah/spi". This is the root of all backend - * method calls. This will default to "https://yourapp.appspot.com/_ah/spi". Non-secure http URLs - * will be automatically converted to use https. - */ - @Deprecated - String backendRoot() default ""; - /** * Configures authentication information. See {@link ApiAuth} for details. */ @@ -146,13 +130,6 @@ ApiIssuerAudience[] issuerAudiences() default { */ Class[] authenticators() default {Authenticator.class}; - /** - * Custom peer authenticators. Applies to all methods of the API unless overridden by - * {@code @ApiClass#peerAuthenticators} or {@code @ApiMethod#peerAuthenticators}. See - * {@link PeerAuthenticator}. - */ - Class[] peerAuthenticators() default {PeerAuthenticator.class}; - /** * {@code true} if this API configuration is used as the base for another. Should be {@code false} * for most situations. diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/config/ApiClass.java b/endpoints-framework/src/main/java/com/google/api/server/spi/config/ApiClass.java index 351821cc..cff9e023 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/config/ApiClass.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/config/ApiClass.java @@ -77,12 +77,6 @@ ApiIssuerAudience[] issuerAudiences() default { */ Class[] authenticators() default {Authenticator.class}; - /** - * Custom peer authenticators, applicable to all methods of the API class unless overridden by - * {@code @ApiMethod#peerAuthenticators}. - */ - Class[] peerAuthenticators() default {PeerAuthenticator.class}; - /** * {@code AnnotationBoolean.TRUE} to request that overriding configuration be loaded from the * appengine datastore. diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/config/ApiIssuer.java b/endpoints-framework/src/main/java/com/google/api/server/spi/config/ApiIssuer.java index bc42ba91..c14899bf 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/config/ApiIssuer.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/config/ApiIssuer.java @@ -33,4 +33,15 @@ * The location of the JSON web key set used to verify tokens generated by this issuer. */ String jwksUri() default ""; + + /** + * The authorization URL to use for the authorization flow. + */ + String authorizationUrl() default ""; + + /** + * When true, scopes will be used in authorization flow. + */ + boolean useScopesInAuthFlow() default false; + } diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/config/ApiMethod.java b/endpoints-framework/src/main/java/com/google/api/server/spi/config/ApiMethod.java index 3065fa77..e8a18f2e 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/config/ApiMethod.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/config/ApiMethod.java @@ -15,6 +15,8 @@ */ package com.google.api.server.spi.config; +import static com.google.api.server.spi.config.model.ApiMethodConfig.RESPONSE_STATUS_UNSPECIFIED; + import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -68,14 +70,9 @@ public static class HttpMethod { String httpMethod() default ""; /** - * Cache-Control header settings for this method. See - * {@link ApiMethodCacheControl} for details. - * - * @deprecated ApiMethodCacheControl is deprecated and will be removed in a future version of - * Cloud Endpoints. + * The response status on success. If not set, the value is 200 or 204 if there is no content returned. */ - @Deprecated - ApiMethodCacheControl cacheControl() default @ApiMethodCacheControl; + int responseStatus() default RESPONSE_STATUS_UNSPECIFIED; /** * Set frontend auth level. @@ -113,11 +110,6 @@ ApiIssuerAudience[] issuerAudiences() default { */ Class[] authenticators() default {Authenticator.class}; - /** - * Custom peer authenticators used to verify peer for this method. - */ - Class[] peerAuthenticators() default {PeerAuthenticator.class}; - /** * Whether or not API method should be ignored. */ diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/config/ApiMethodCacheControl.java b/endpoints-framework/src/main/java/com/google/api/server/spi/config/ApiMethodCacheControl.java deleted file mode 100644 index 49141f2a..00000000 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/config/ApiMethodCacheControl.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2016 Google Inc. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.api.server.spi.config; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Annotation for configuration of API method cache control. - * @deprecated ApiMethodCacheControl is deprecated and will be removed in a future version of - * Cloud Endpoints. - */ -// TODO: Delete this after a sufficient deprecation period. -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.METHOD) -@Deprecated -public @interface ApiMethodCacheControl { - - /** - * Disables caching of this method. The default value is true, so merely - * adding this annotation to your method config will disable caching for that - * method, unless you set this field to {@code false}. - */ - boolean noCache() default true; - - /** - * Overrides the maximum age to cache responses from this method. - */ - int maxAge() default 0; -} diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/config/ApiResourceProperty.java b/endpoints-framework/src/main/java/com/google/api/server/spi/config/ApiResourceProperty.java index 10dd0eb8..acd58e92 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/config/ApiResourceProperty.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/config/ApiResourceProperty.java @@ -34,11 +34,20 @@ String name() default ""; /** - * The description that the property represented by the annotated getter, setter, or field should appear - * as in the API. + * The description that the property represented by the annotated getter, setter, or field should + * appear as in the API. */ String description() default ""; + /** + * Whether or not the property represented by the annotated getter, setter or field is "required": + * - For requests, indicates the property is required for the resource to be accepted + * - For responses, indicates the property will be returned by the server (before applying + * partial response filtering) + * In both cases, this is only a "hint": this is not enforced in any way. + */ + AnnotationBoolean required() default AnnotationBoolean.UNSPECIFIED; + /** * Whether or not the property represented by the annotated getter, setter or field should be * ignored for the API. diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/config/Description.java b/endpoints-framework/src/main/java/com/google/api/server/spi/config/Description.java index 3e193911..877d052e 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/config/Description.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/config/Description.java @@ -21,14 +21,17 @@ import java.lang.annotation.Target; /** - * Annotation to specify the description of an API parameter or enum constants. + * Annotation to specify the description of a method parameter, enum type, enum constant or + * resource (type used as request body). + * If used on an enum type, the description will only be used for the Discovery format (OpenAPI + * will inline enum values, as the semantics is poorer). * The description will be ignored if the annotation is used on resource fields. */ -@Target({ElementType.PARAMETER, ElementType.FIELD}) +@Target({ElementType.PARAMETER, ElementType.FIELD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface Description { /** - * The parameter description. + * A description. */ String value() default ""; } diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/config/PeerAuthenticator.java b/endpoints-framework/src/main/java/com/google/api/server/spi/config/PeerAuthenticator.java deleted file mode 100644 index 10b9ded4..00000000 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/config/PeerAuthenticator.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2016 Google Inc. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.api.server.spi.config; - -import javax.servlet.http.HttpServletRequest; - -/** - * Peer authenticators aim to verify the peer and run before {@code Authenticator}. It returns false - * if authentication failed and stops handling the rest of the request; true if authentication - * succeeds and continue to execute rest of peer authenticators. - * - *

- * If no peer authenticator is set, {@code EndpointsPeerAuthenticator} will be the default to verify - * the request is from Google. If you supply your own peer authenticator, make sure you also put - * {@code EndpointsPeerAuthenticator} to the head of peerAuthenticators list to verify the request - * is from Google. - */ -public interface PeerAuthenticator { - boolean authenticate(HttpServletRequest request); -} diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/config/ResourcePropertySchema.java b/endpoints-framework/src/main/java/com/google/api/server/spi/config/ResourcePropertySchema.java index f7a3460d..8ed3b5e4 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/config/ResourcePropertySchema.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/config/ResourcePropertySchema.java @@ -15,6 +15,7 @@ */ package com.google.api.server.spi.config; +import com.google.api.server.spi.config.model.ApiValidationConstraints; import com.google.common.base.MoreObjects; import com.google.common.base.Preconditions; import com.google.common.reflect.TypeToken; @@ -30,6 +31,8 @@ public class ResourcePropertySchema { private final TypeToken type; private String description; + private Boolean required; + private ApiValidationConstraints validationConstraints; private ResourcePropertySchema(TypeToken type) { this.type = type; @@ -56,7 +59,24 @@ public String getDescription() { public void setDescription(String description) { this.description = description; } - + + public Boolean getRequired() { + return required; + } + + public ResourcePropertySchema setRequired(Boolean required) { + this.required = required; + return this; + } + + public ApiValidationConstraints getValidationConstraints() { + return validationConstraints; + } + + public void setValidationConstraints(ApiValidationConstraints validationConstraints) { + this.validationConstraints = validationConstraints; + } + /** * Returns a default resource property schema for a given type. * @@ -66,28 +86,31 @@ public void setDescription(String description) { public static ResourcePropertySchema of(TypeToken type) { return new ResourcePropertySchema(Preconditions.checkNotNull(type)); } - + @Override - public boolean equals(Object obj) { - if (this == obj) { + public boolean equals(Object o) { + if (this == o) { return true; } - if (!(obj instanceof ResourcePropertySchema)) { + if (o == null || getClass() != o.getClass()) { return false; } - ResourcePropertySchema that = (ResourcePropertySchema) obj; - return Objects.equals(this.type, that.type); + ResourcePropertySchema that = (ResourcePropertySchema) o; + return type.equals(that.type) && Objects.equals(description, that.description) && Objects.equals(required, that.required) && Objects.equals(validationConstraints, that.validationConstraints); } - + @Override public int hashCode() { - return Objects.hash(type); + return Objects.hash(type, description, required, validationConstraints); } - + @Override public String toString() { - return MoreObjects.toStringHelper(this.getClass()) - .add("type", type) - .toString(); + return MoreObjects.toStringHelper(this) + .add("type", type) + .add("description", description) + .add("required", required) + .add("validationConstraints", validationConstraints) + .toString(); } } diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/config/Singleton.java b/endpoints-framework/src/main/java/com/google/api/server/spi/config/Singleton.java index 198a7afc..b6c74bbb 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/config/Singleton.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/config/Singleton.java @@ -30,7 +30,7 @@ import java.util.logging.Level; /** - * Annotation used with Authenticator and PeerAuthenticator to denote only one instance will be + * Annotation used with Authenticator to denote only one instance will be * created for optimization. Implementation must be thread safe. Without the annotation a new * (peer)authenticator instance will be created for each request. */ diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/config/annotationreader/AnnotationUtil.java b/endpoints-framework/src/main/java/com/google/api/server/spi/config/annotationreader/AnnotationUtil.java index b0fd9a34..d3d68b1e 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/config/annotationreader/AnnotationUtil.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/config/annotationreader/AnnotationUtil.java @@ -17,7 +17,6 @@ import com.google.api.server.spi.config.Api; import com.google.api.server.spi.config.Authenticator; -import com.google.api.server.spi.config.PeerAuthenticator; import java.lang.annotation.Annotation; import java.lang.reflect.Method; @@ -118,9 +117,4 @@ public static boolean isUnspecified(Class[] values) { return values == null || (values.length == 1 && values[0].equals(Authenticator.class)); } - - public static boolean isUnspecifiedPeerAuthenticators( - Class[] values) { - return values == null || (values.length == 1 && values[0].equals(PeerAuthenticator.class)); - } } diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/config/annotationreader/ApiAnnotationConfig.java b/endpoints-framework/src/main/java/com/google/api/server/spi/config/annotationreader/ApiAnnotationConfig.java index 017ed0cc..90fad3e6 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/config/annotationreader/ApiAnnotationConfig.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/config/annotationreader/ApiAnnotationConfig.java @@ -19,7 +19,6 @@ import com.google.api.server.spi.config.ApiLimitMetric; import com.google.api.server.spi.config.AuthLevel; import com.google.api.server.spi.config.Authenticator; -import com.google.api.server.spi.config.PeerAuthenticator; import com.google.api.server.spi.config.model.ApiConfig; import com.google.api.server.spi.config.model.ApiIssuerAudienceConfig; import com.google.api.server.spi.config.model.ApiIssuerConfigs; @@ -175,13 +174,6 @@ public void setAuthenticatorsIfSpecified(Class[] authen } } - public void setPeerAuthenticatorsIfSpecified( - Class[] peerAuthenticators) { - if (!AnnotationUtil.isUnspecifiedPeerAuthenticators(peerAuthenticators)) { - config.setPeerAuthenticators(Arrays.asList(peerAuthenticators)); - } - } - public void setApiKeyRequiredIfSpecified(AnnotationBoolean apiKeyRequired) { if (apiKeyRequired == AnnotationBoolean.TRUE) { config.setApiKeyRequired(true); diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/config/annotationreader/ApiAnnotationIntrospector.java b/endpoints-framework/src/main/java/com/google/api/server/spi/config/annotationreader/ApiAnnotationIntrospector.java index ea06c968..0b225f73 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/config/annotationreader/ApiAnnotationIntrospector.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/config/annotationreader/ApiAnnotationIntrospector.java @@ -49,6 +49,7 @@ import java.util.List; import java.util.Map; +import javax.annotation.Nonnull; import javax.annotation.Nullable; /** @@ -58,10 +59,6 @@ public class ApiAnnotationIntrospector extends NopAnnotationIntrospector { private final ApiSerializationConfig config; - public ApiAnnotationIntrospector() { - this(new ApiSerializationConfig()); - } - public ApiAnnotationIntrospector(ApiSerializationConfig config) { this.config = config; } @@ -72,6 +69,23 @@ public boolean hasIgnoreMarker(AnnotatedMember member) { return apiProperty != null && apiProperty.ignored() == AnnotationBoolean.TRUE; } + @Override + public Boolean hasRequiredMarker(AnnotatedMember member) { + ApiResourceProperty apiProperty = member.getAnnotation(ApiResourceProperty.class); + Nonnull nonnull = member.getAnnotation(Nonnull.class); + Nullable nullable = member.getAnnotation(Nullable.class); + if (apiProperty != null && apiProperty.required() != AnnotationBoolean.UNSPECIFIED) { + return Boolean.parseBoolean(apiProperty.required().name()); + } + if (nonnull != null) { + return true; + } + if (nullable != null) { + return false; + } + return null; + } + @Override public PropertyName findNameForSerialization(Annotated a) { ApiResourceProperty apiName = a.getAnnotation(ApiResourceProperty.class); @@ -103,11 +117,6 @@ public JsonSerializer findSerializer(Annotated method) { return getJsonSerializer(findSerializerInstance(method)); } - @Override - public String findEnumValue(Enum value) { - return value.name(); - } - @Nullable private static JsonSerializer getJsonSerializer( @Nullable final Transformer serializer) { diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/config/annotationreader/ApiClassAnnotationConfig.java b/endpoints-framework/src/main/java/com/google/api/server/spi/config/annotationreader/ApiClassAnnotationConfig.java index 101cb8d9..ffd02789 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/config/annotationreader/ApiClassAnnotationConfig.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/config/annotationreader/ApiClassAnnotationConfig.java @@ -18,7 +18,6 @@ import com.google.api.server.spi.config.AnnotationBoolean; import com.google.api.server.spi.config.AuthLevel; import com.google.api.server.spi.config.Authenticator; -import com.google.api.server.spi.config.PeerAuthenticator; import com.google.api.server.spi.config.model.ApiClassConfig; import com.google.api.server.spi.config.model.ApiIssuerAudienceConfig; import com.google.api.server.spi.config.scope.AuthScopeExpressions; @@ -79,13 +78,6 @@ public void setAuthenticatorsIfSpecified(Class[] authen } } - public void setPeerAuthenticatorsIfSpecified( - Class[] peerAuthenticators) { - if (!AnnotationUtil.isUnspecifiedPeerAuthenticators(peerAuthenticators)) { - config.setPeerAuthenticators(Arrays.asList(peerAuthenticators)); - } - } - public void setUseDatastoreIfSpecified(AnnotationBoolean useDatastore) { if (useDatastore == AnnotationBoolean.TRUE) { config.setUseDatastore(true); diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/config/annotationreader/ApiConfigAnnotationReader.java b/endpoints-framework/src/main/java/com/google/api/server/spi/config/annotationreader/ApiConfigAnnotationReader.java index 27c6541f..e397ad6d 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/config/annotationreader/ApiConfigAnnotationReader.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/config/annotationreader/ApiConfigAnnotationReader.java @@ -19,33 +19,29 @@ import com.google.api.server.spi.MethodHierarchyReader; import com.google.api.server.spi.ServiceContext; import com.google.api.server.spi.TypeLoader; -import com.google.api.server.spi.config.AnnotationBoolean; import com.google.api.server.spi.config.ApiConfigException; import com.google.api.server.spi.config.ApiConfigSource; import com.google.api.server.spi.config.ApiIssuer; import com.google.api.server.spi.config.ApiIssuerAudience; -import com.google.api.server.spi.config.ApiLimitMetric; -import com.google.api.server.spi.config.ApiMetricCost; -import com.google.api.server.spi.config.AuthLevel; import com.google.api.server.spi.config.Authenticator; -import com.google.api.server.spi.config.PeerAuthenticator; import com.google.api.server.spi.config.Transformer; import com.google.api.server.spi.config.model.ApiClassConfig; +import com.google.api.server.spi.config.model.ApiClassConfig.MethodConfigMap; import com.google.api.server.spi.config.model.ApiConfig; import com.google.api.server.spi.config.model.ApiIssuerAudienceConfig; import com.google.api.server.spi.config.model.ApiIssuerConfigs; import com.google.api.server.spi.config.model.ApiMethodConfig; +import com.google.api.server.spi.config.model.ApiValidationConstraints; import com.google.api.server.spi.config.model.ApiParameterConfig; import com.google.api.server.spi.config.model.ApiSerializationConfig; +import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.common.reflect.TypeToken; - import java.lang.annotation.Annotation; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import java.util.List; +import java.util.Collection; import java.util.Map; - import javax.annotation.Nullable; /** @@ -158,15 +154,15 @@ private boolean readEndpointClass(ApiConfig config, Class endpointClass, Anno if (api != null) { readApi(new ApiAnnotationConfig(config), api); readApiAuth(new ApiAuthAnnotationConfig(config.getAuthConfig()), - (Annotation) getAnnotationProperty(api, "auth")); + getAnnotationProperty(api, "auth")); readApiFrontendLimits(new ApiFrontendLimitsAnnotationConfig(config.getFrontendLimitsConfig()), - (Annotation) getAnnotationProperty(api, "frontendLimits")); + getAnnotationProperty(api, "frontendLimits")); readApiCacheControl(new ApiCacheControlAnnotationConfig(config.getCacheControlConfig()), - (Annotation) getAnnotationProperty(api, "cacheControl")); + getAnnotationProperty(api, "cacheControl")); readApiNamespace(new ApiNamespaceAnnotationConfig(config.getNamespaceConfig()), - (Annotation) getAnnotationProperty(api, "namespace")); + getAnnotationProperty(api, "namespace")); readSerializers(config.getSerializationConfig(), - (Class>[]) getAnnotationProperty(api, "transformers")); + getAnnotationProperty(api, "transformers")); } if (apiClass != null) { @@ -178,40 +174,35 @@ private boolean readEndpointClass(ApiConfig config, Class endpointClass, Anno private void readApi(ApiAnnotationConfig config, Annotation api) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { - config.setIsAbstractIfSpecified((AnnotationBoolean) getAnnotationProperty(api, "isAbstract")); - config.setRootIfNotEmpty((String) getAnnotationProperty(api, "root")); + config.setIsAbstractIfSpecified(getAnnotationProperty(api, "isAbstract")); - config.setNameIfNotEmpty((String) getAnnotationProperty(api, "name")); - config.setCanonicalNameIfNotEmpty((String) getAnnotationProperty(api, "canonicalName")); + config.setNameIfNotEmpty(getAnnotationProperty(api, "name")); + config.setCanonicalNameIfNotEmpty(getAnnotationProperty(api, "canonicalName")); - config.setVersionIfNotEmpty((String) getAnnotationProperty(api, "version")); - config.setTitleIfNotEmpty((String) getAnnotationProperty(api, "title")); - config.setDescriptionIfNotEmpty((String) getAnnotationProperty(api, "description")); - config.setDocumentationLinkIfNotEmpty((String) getAnnotationProperty(api, "documentationLink")); + config.setVersionIfNotEmpty(getAnnotationProperty(api, "version")); + config.setTitleIfNotEmpty(getAnnotationProperty(api, "title")); + config.setDescriptionIfNotEmpty(getAnnotationProperty(api, "description")); + config.setDocumentationLinkIfNotEmpty(getAnnotationProperty(api, "documentationLink")); config.setIsDefaultVersionIfSpecified( - (AnnotationBoolean) getAnnotationProperty(api, "defaultVersion")); + getAnnotationProperty(api, "defaultVersion")); config.setIsDiscoverableIfSpecified( - (AnnotationBoolean) getAnnotationProperty(api, "discoverable")); + getAnnotationProperty(api, "discoverable")); config.setUseDatastoreIfSpecified( - (AnnotationBoolean) getAnnotationProperty(api, "useDatastoreForAdditionalConfig")); - - config.setBackendRootIfNotEmpty((String) getAnnotationProperty(api, "backendRoot")); + getAnnotationProperty(api, "useDatastoreForAdditionalConfig")); - config.setResourceIfNotEmpty((String) getAnnotationProperty(api, "resource")); - config.setAuthLevelIfSpecified((AuthLevel) getAnnotationProperty(api, "authLevel")); - config.setScopesIfSpecified((String[]) getAnnotationProperty(api, "scopes")); - config.setAudiencesIfSpecified((String[]) getAnnotationProperty(api, "audiences")); + config.setResourceIfNotEmpty(getAnnotationProperty(api, "resource")); + config.setAuthLevelIfSpecified(getAnnotationProperty(api, "authLevel")); + config.setScopesIfSpecified(getAnnotationProperty(api, "scopes")); + config.setAudiencesIfSpecified(getAnnotationProperty(api, "audiences")); config.setIssuersIfSpecified(getIssuerConfigs(api)); config.setIssuerAudiencesIfSpecified(getIssuerAudiences(api)); - config.setClientIdsIfSpecified((String[]) getAnnotationProperty(api, "clientIds")); + config.setClientIdsIfSpecified(getAnnotationProperty(api, "clientIds")); config.setAuthenticatorsIfSpecified( this.[]>getAnnotationProperty(api, "authenticators")); - config.setPeerAuthenticatorsIfSpecified(this - .[]>getAnnotationProperty(api, "peerAuthenticators")); config.setApiKeyRequiredIfSpecified( - (AnnotationBoolean) this.getAnnotationProperty(api, "apiKeyRequired")); + this.getAnnotationProperty(api, "apiKeyRequired")); config.setApiLimitMetrics( - (ApiLimitMetric[]) this.getAnnotationProperty(api, "limitDefinitions")); + this.getAnnotationProperty(api, "limitDefinitions")); } private ApiIssuerConfigs getIssuerConfigs(Annotation annotation) @@ -239,22 +230,22 @@ private T getAnnotationProperty(Annotation annotation, String name) protected void readApiAuth(ApiAuthAnnotationConfig config, Annotation auth) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { config.setAllowCookieAuthIfSpecified( - (AnnotationBoolean) getAnnotationProperty(auth, "allowCookieAuth")); - config.setBlockedRegionsIfNotEmpty((String[]) getAnnotationProperty(auth, "blockedRegions")); + getAnnotationProperty(auth, "allowCookieAuth")); + config.setBlockedRegionsIfNotEmpty(getAnnotationProperty(auth, "blockedRegions")); } private void readApiFrontendLimits(ApiFrontendLimitsAnnotationConfig config, Annotation frontendLimits) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { config.setUnregisteredUserQpsIfSpecified( - (Integer) getAnnotationProperty(frontendLimits, "unregisteredUserQps")); + getAnnotationProperty(frontendLimits, "unregisteredUserQps")); config.setUnregisteredQpsIfSpecified( - (Integer) getAnnotationProperty(frontendLimits, "unregisteredQps")); + getAnnotationProperty(frontendLimits, "unregisteredQps")); config.setUnregisteredDailyIfSpecified( - (Integer) getAnnotationProperty(frontendLimits, "unregisteredDaily")); + getAnnotationProperty(frontendLimits, "unregisteredDaily")); readApiFrontendLimitRules(config, - (Annotation[]) getAnnotationProperty(frontendLimits, "rules")); + getAnnotationProperty(frontendLimits, "rules")); } private void readSerializers( @@ -266,15 +257,15 @@ private void readSerializers( private void readApiCacheControl(ApiCacheControlAnnotationConfig config, Annotation cacheControl) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { - config.setTypeIfNotEmpty((String) getAnnotationProperty(cacheControl, "type")); - config.setMaxAgeIfSpecified((Integer) getAnnotationProperty(cacheControl, "maxAge")); + config.setTypeIfNotEmpty(getAnnotationProperty(cacheControl, "type")); + config.setMaxAgeIfSpecified(getAnnotationProperty(cacheControl, "maxAge")); } protected void readApiNamespace(ApiNamespaceAnnotationConfig config, Annotation namespace) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { - config.setOwnerDomainIfNotEmpty((String) getAnnotationProperty(namespace, "ownerDomain")); - config.setOwnerNameIfNotEmpty((String) getAnnotationProperty(namespace, "ownerName")); - config.setPackagePathIfNotEmpty((String) getAnnotationProperty(namespace, "packagePath")); + config.setOwnerDomainIfNotEmpty(getAnnotationProperty(namespace, "ownerDomain")); + config.setOwnerNameIfNotEmpty(getAnnotationProperty(namespace, "ownerName")); + config.setPackagePathIfNotEmpty(getAnnotationProperty(namespace, "packagePath")); } private void readApiFrontendLimitRules(ApiFrontendLimitsAnnotationConfig config, @@ -282,9 +273,9 @@ private void readApiFrontendLimitRules(ApiFrontendLimitsAnnotationConfig config, InvocationTargetException { for (Annotation rule : rules) { String match = getAnnotationProperty(rule, "match"); - int qps = (Integer) getAnnotationProperty(rule, "qps"); - int userQps = (Integer) getAnnotationProperty(rule, "userQps"); - int daily = (Integer) getAnnotationProperty(rule, "daily"); + int qps = getAnnotationProperty(rule, "qps"); + int userQps = getAnnotationProperty(rule, "userQps"); + int daily = getAnnotationProperty(rule, "daily"); String analyticsId = getAnnotationProperty(rule, "analyticsId"); config.getConfig().addRule(match, qps, userQps, daily, analyticsId); } @@ -292,20 +283,18 @@ private void readApiFrontendLimitRules(ApiFrontendLimitsAnnotationConfig config, private void readApiClass(ApiClassAnnotationConfig config, Annotation apiClass) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { - config.setResourceIfNotEmpty((String) getAnnotationProperty(apiClass, "resource")); - config.setAuthLevelIfSpecified((AuthLevel) getAnnotationProperty(apiClass, "authLevel")); - config.setScopesIfSpecified((String[]) getAnnotationProperty(apiClass, "scopes")); - config.setAudiencesIfSpecified((String[]) getAnnotationProperty(apiClass, "audiences")); + config.setResourceIfNotEmpty(getAnnotationProperty(apiClass, "resource")); + config.setAuthLevelIfSpecified(getAnnotationProperty(apiClass, "authLevel")); + config.setScopesIfSpecified(getAnnotationProperty(apiClass, "scopes")); + config.setAudiencesIfSpecified(getAnnotationProperty(apiClass, "audiences")); config.setIssuerAudiencesIfSpecified(getIssuerAudiences(apiClass)); - config.setClientIdsIfSpecified((String[]) getAnnotationProperty(apiClass, "clientIds")); + config.setClientIdsIfSpecified(getAnnotationProperty(apiClass, "clientIds")); config.setAuthenticatorsIfSpecified( this.[]>getAnnotationProperty(apiClass, "authenticators")); - config.setPeerAuthenticatorsIfSpecified(this.< - Class[]>getAnnotationProperty(apiClass, "peerAuthenticators")); config.setUseDatastoreIfSpecified( - (AnnotationBoolean) getAnnotationProperty(apiClass, "useDatastoreForAdditionalConfig")); + getAnnotationProperty(apiClass, "useDatastoreForAdditionalConfig")); config.setApiKeyRequiredIfSpecified( - (AnnotationBoolean) this.getAnnotationProperty(apiClass, "apiKeyRequired")); + this.getAnnotationProperty(apiClass, "apiKeyRequired")); } private void readEndpointMethods(Class endpointClass, @@ -313,55 +302,57 @@ private void readEndpointMethods(Class endpointClass, throws IllegalArgumentException, SecurityException, IllegalAccessException, InvocationTargetException, NoSuchMethodException { MethodHierarchyReader methodReader = new MethodHierarchyReader(endpointClass); - Iterable> methods = methodReader.getEndpointOverrides(); + Iterable> methods = methodReader.getEndpointOverrides(); - for (List overrides : methods) { - readEndpointMethod(methodConfigMap, overrides); + for (Collection overrides : methods) { + readEndpointMethod(methodConfigMap, overrides, + endpointClass.getAnnotation(Deprecated.class) != null); } } - private void readEndpointMethod(ApiClassConfig.MethodConfigMap methodConfigMap, - List overrides) + private void readEndpointMethod(MethodConfigMap methodConfigMap, + Collection overrides, boolean deprecated) throws IllegalArgumentException, SecurityException, IllegalAccessException, InvocationTargetException, NoSuchMethodException { Class apiMethodClass = annotationTypes.get("ApiMethod"); - final EndpointMethod finalMethod = overrides.get(0); + final EndpointMethod finalMethod = overrides.iterator().next(); ApiMethodConfig methodConfig = methodConfigMap.getOrCreate(finalMethod); readMethodRequestParameters(finalMethod, methodConfig); // Process overrides in reverse order. - for (EndpointMethod method : Lists.reverse(overrides)) { + for (EndpointMethod method : Lists.reverse(ImmutableList.copyOf(overrides))) { + ApiMethodAnnotationConfig config = new ApiMethodAnnotationConfig(methodConfig); Annotation apiMethod = method.getMethod().getAnnotation(apiMethodClass); if (apiMethod != null) { - readApiMethodInstance(new ApiMethodAnnotationConfig(methodConfig), apiMethod); + readApiMethodInstance(config, apiMethod); } + methodConfig.setDeprecated(deprecated + || method.getMethod().getAnnotation(Deprecated.class) != null); } } private void readApiMethodInstance(ApiMethodAnnotationConfig config, Annotation apiMethod) throws IllegalArgumentException, SecurityException, IllegalAccessException, InvocationTargetException, NoSuchMethodException { - config.setNameIfNotEmpty((String) getAnnotationProperty(apiMethod, "name")); - config.setDescriptionIfNotEmpty((String) getAnnotationProperty(apiMethod, "description")); - config.setPathIfNotEmpty((String) getAnnotationProperty(apiMethod, "path")); - config.setHttpMethodIfNotEmpty((String) getAnnotationProperty(apiMethod, "httpMethod")); - config.setAuthLevelIfSpecified((AuthLevel) getAnnotationProperty(apiMethod, "authLevel")); - config.setScopesIfSpecified((String[]) getAnnotationProperty(apiMethod, "scopes")); - config.setAudiencesIfSpecified((String[]) getAnnotationProperty(apiMethod, "audiences")); + config.setNameIfNotEmpty(getAnnotationProperty(apiMethod, "name")); + config.setDescriptionIfNotEmpty(getAnnotationProperty(apiMethod, "description")); + config.setPathIfNotEmpty(getAnnotationProperty(apiMethod, "path")); + config.setHttpMethodIfNotEmpty(getAnnotationProperty(apiMethod, "httpMethod")); + config.setResponseStatus(getAnnotationProperty(apiMethod, "responseStatus")); + config.setAuthLevelIfSpecified(getAnnotationProperty(apiMethod, "authLevel")); + config.setScopesIfSpecified(getAnnotationProperty(apiMethod, "scopes")); + config.setAudiencesIfSpecified(getAnnotationProperty(apiMethod, "audiences")); config.setIssuerAudiencesIfSpecified(getIssuerAudiences(apiMethod)); - config.setClientIdsIfSpecified((String[]) getAnnotationProperty(apiMethod, "clientIds")); + config.setClientIdsIfSpecified(getAnnotationProperty(apiMethod, "clientIds")); config.setAuthenticatorsIfSpecified( this.[]>getAnnotationProperty(apiMethod, "authenticators")); - config.setPeerAuthenticatorsIfSpecified(this.< - Class[]>getAnnotationProperty(apiMethod, - "peerAuthenticators")); - config.setIgnoredIfSpecified((AnnotationBoolean) getAnnotationProperty(apiMethod, "ignored")); + config.setIgnoredIfSpecified(getAnnotationProperty(apiMethod, "ignored")); config.setApiKeyRequiredIfSpecified( - (AnnotationBoolean) this.getAnnotationProperty(apiMethod, "apiKeyRequired")); + this.getAnnotationProperty(apiMethod, "apiKeyRequired")); config.setMetricCosts( - (ApiMetricCost[]) getAnnotationProperty(apiMethod, "metricCosts")); + getAnnotationProperty(apiMethod, "metricCosts")); } private void readMethodRequestParameters(EndpointMethod endpointMethod, @@ -376,39 +367,30 @@ private void readMethodRequestParameters(EndpointMethod endpointMethod, } for (int i = 0; i < parameterAnnotations.length; i++) { - Annotation parameterName = - AnnotationUtil.getNamedParameter(method, i, annotationTypes.get("Named")); - Annotation description = - AnnotationUtil.getParameterAnnotation(method, i, annotationTypes.get("Description")); - Annotation nullable = - AnnotationUtil.getNullableParameter(method, i, annotationTypes.get("Nullable")); - Annotation defaultValue = - AnnotationUtil.getParameterAnnotation(method, i, annotationTypes.get("DefaultValue")); - readMethodRequestParameter(methodConfig, parameterName, description, nullable, defaultValue, - parameterTypes[i]); + ApiConfigAnnotations configAnnotations = new ApiConfigAnnotations(method, i, annotationTypes); + readMethodRequestParameter(methodConfig, parameterTypes[i], configAnnotations); } } - private void readMethodRequestParameter(ApiMethodConfig methodConfig, Annotation parameterName, - Annotation description, Annotation nullable, Annotation defaultValue, TypeToken type) + private void readMethodRequestParameter(ApiMethodConfig methodConfig, TypeToken type, ApiConfigAnnotations annotations) throws IllegalArgumentException, SecurityException, IllegalAccessException, InvocationTargetException, NoSuchMethodException { String parameterNameString = null; - if (parameterName != null) { - parameterNameString = getAnnotationProperty(parameterName, "value"); + if (annotations.getParameterName() != null) { + parameterNameString = getAnnotationProperty(annotations.getParameterName(), "value"); } String descriptionString = null; - if (description != null) { - descriptionString = getAnnotationProperty(description, "value"); + if (annotations.getDescription() != null) { + descriptionString = getAnnotationProperty(annotations.getDescription(), "value"); } String defaultValueString = null; - if (defaultValue != null) { - defaultValueString = getAnnotationProperty(defaultValue, "value"); + if (annotations.getDefaultValue() != null) { + defaultValueString = getAnnotationProperty(annotations.getDefaultValue(), "value"); } - + ApiValidationConstraints validationConstraints = buildApiValidationConstraints(annotations); ApiParameterConfig parameterConfig = - methodConfig.addParameter(parameterNameString, descriptionString, nullable != null, - defaultValueString, type); + methodConfig.addParameter(parameterNameString, descriptionString, annotations.getNullable() != null, + defaultValueString, type, validationConstraints); Annotation apiSerializer = type.getRawType().getAnnotation(annotationTypes.get("ApiTransformer")); @@ -429,6 +411,45 @@ private void readMethodRequestParameter(ApiMethodConfig methodConfig, Annotation } } } + + public ApiValidationConstraints buildApiValidationConstraints(ApiConfigAnnotations annotations) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { + String patternString = null; + if (annotations.getPattern() != null) { + patternString = getAnnotationProperty(annotations.getPattern(), "regexp"); + } + Long minLong = null; + if (annotations.getMin() != null) { + minLong = getAnnotationProperty(annotations.getMin(), "value"); + } + Long maxLong = null; + if (annotations.getMax() != null) { + maxLong = getAnnotationProperty(annotations.getMax(), "value"); + } + String decimalMinString = null; + Boolean decimalMinInclusive = null; + if (annotations.getDecimalMin() != null) { + decimalMinString = getAnnotationProperty(annotations.getDecimalMin(), "value"); + decimalMinInclusive = getAnnotationProperty(annotations.getDecimalMin(), "inclusive"); + } + String decimalMaxString = null; + Boolean decimalMaxInclusive = null; + if (annotations.getDecimalMax() != null) { + decimalMaxString = getAnnotationProperty(annotations.getDecimalMax(), "value"); + decimalMaxInclusive = getAnnotationProperty(annotations.getDecimalMax(), "inclusive"); + } + Integer minSize = null; + Integer maxSize = null; + if (annotations.getSize() != null) { + minSize = getAnnotationProperty(annotations.getSize(), "min"); + maxSize = getAnnotationProperty(annotations.getSize(), "max"); + } + + ApiValidationConstraints validationConstraints = new ApiValidationConstraints( + patternString, minLong, maxLong, + decimalMinString, decimalMaxString, decimalMinInclusive, decimalMaxInclusive, + minSize, maxSize); + return validationConstraints; + } private static A getDeclaredAnnotation( Class clazz, Class annotationClass) { diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/config/annotationreader/ApiConfigAnnotations.java b/endpoints-framework/src/main/java/com/google/api/server/spi/config/annotationreader/ApiConfigAnnotations.java new file mode 100644 index 00000000..cecf9e8c --- /dev/null +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/config/annotationreader/ApiConfigAnnotations.java @@ -0,0 +1,90 @@ +package com.google.api.server.spi.config.annotationreader; + +import static com.google.api.server.spi.config.annotationreader.AnnotationUtil.getNamedParameter; +import static com.google.api.server.spi.config.annotationreader.AnnotationUtil.getNullableParameter; +import static com.google.api.server.spi.config.annotationreader.AnnotationUtil.getParameterAnnotation; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Method; +import java.util.Map; + +public final class ApiConfigAnnotations { + + private Annotation parameterName; + private Annotation description; + private Annotation nullable; + private Annotation defaultValue; + private Annotation pattern; + private Annotation min; + private Annotation max; + private Annotation decimalMin; + private Annotation decimalMax; + private Annotation size; + + public ApiConfigAnnotations(Method method, int parameterIndex, Map> annotationTypes) { + this.parameterName = getNamedParameter(method, parameterIndex, annotationTypes.get("Named")); + this.description = getParameterAnnotation(method, parameterIndex, annotationTypes.get("Description")); + this.nullable = getNullableParameter(method, parameterIndex, annotationTypes.get("Nullable")); + this.defaultValue = getParameterAnnotation(method, parameterIndex, annotationTypes.get("DefaultValue")); + this.pattern = getParameterAnnotation(method, parameterIndex, annotationTypes.get("Pattern")); + this.min = getParameterAnnotation(method, parameterIndex, annotationTypes.get("Min")); + this.max = getParameterAnnotation(method, parameterIndex, annotationTypes.get("Max")); + this.decimalMin = getParameterAnnotation(method, parameterIndex, annotationTypes.get("DecimalMin")); + this.decimalMax = getParameterAnnotation(method, parameterIndex, annotationTypes.get("DecimalMax")); + this.size = getParameterAnnotation(method, parameterIndex, annotationTypes.get("Size")); + } + + public ApiConfigAnnotations(AnnotatedElement annotatedElement, Map> annotationTypes) { + this.parameterName = annotatedElement.getAnnotation(annotationTypes.get("Named")); + this.description = annotatedElement.getAnnotation(annotationTypes.get("Description")); + this.nullable = annotatedElement.getAnnotation(annotationTypes.get("Nullable")); + this.defaultValue = annotatedElement.getAnnotation(annotationTypes.get("DefaultValue")); + this.pattern = annotatedElement.getAnnotation(annotationTypes.get("Pattern")); + this.min = annotatedElement.getAnnotation(annotationTypes.get("Min")); + this.max = annotatedElement.getAnnotation(annotationTypes.get("Max")); + this.decimalMin = annotatedElement.getAnnotation(annotationTypes.get("DecimalMin")); + this.decimalMax = annotatedElement.getAnnotation(annotationTypes.get("DecimalMax")); + this.size = annotatedElement.getAnnotation(annotationTypes.get("Size")); + } + + public Annotation getParameterName() { + return parameterName; + } + + public Annotation getDescription() { + return description; + } + + public Annotation getNullable() { + return nullable; + } + + public Annotation getDefaultValue() { + return defaultValue; + } + + public Annotation getPattern() { + return pattern; + } + + public Annotation getMin() { + return min; + } + + public Annotation getMax() { + return max; + } + + public Annotation getDecimalMin() { + return decimalMin; + } + + public Annotation getDecimalMax() { + return decimalMax; + } + + public Annotation getSize() { + return size; + } +} diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/config/annotationreader/ApiMethodAnnotationConfig.java b/endpoints-framework/src/main/java/com/google/api/server/spi/config/annotationreader/ApiMethodAnnotationConfig.java index 590857ec..dbb25384 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/config/annotationreader/ApiMethodAnnotationConfig.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/config/annotationreader/ApiMethodAnnotationConfig.java @@ -19,7 +19,6 @@ import com.google.api.server.spi.config.ApiMetricCost; import com.google.api.server.spi.config.AuthLevel; import com.google.api.server.spi.config.Authenticator; -import com.google.api.server.spi.config.PeerAuthenticator; import com.google.api.server.spi.config.model.ApiIssuerAudienceConfig; import com.google.api.server.spi.config.model.ApiMethodConfig; import com.google.api.server.spi.config.model.ApiMetricCostConfig; @@ -69,6 +68,10 @@ public void setHttpMethodIfNotEmpty(String httpMethod) { } } + public void setResponseStatus(int responseStatus) { + config.setResponseStatus(responseStatus); + } + public void setAuthLevelIfSpecified(AuthLevel authLevel) { if (authLevel != AuthLevel.UNSPECIFIED) { config.setAuthLevel(authLevel); @@ -105,13 +108,6 @@ public void setAuthenticatorsIfSpecified(Class[] authen } } - public void setPeerAuthenticatorsIfSpecified( - Class[] peerAuthenticators) { - if (!AnnotationUtil.isUnspecifiedPeerAuthenticators(peerAuthenticators)) { - config.setPeerAuthenticators(Arrays.asList(peerAuthenticators)); - } - } - public void setIgnoredIfSpecified(AnnotationBoolean ignored) { if (ignored == AnnotationBoolean.TRUE) { config.setIgnored(true); diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/config/annotationreader/IssuerUtil.java b/endpoints-framework/src/main/java/com/google/api/server/spi/config/annotationreader/IssuerUtil.java index 5ccdb16e..1d54ee39 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/config/annotationreader/IssuerUtil.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/config/annotationreader/IssuerUtil.java @@ -36,7 +36,8 @@ public static ApiIssuerConfigs toConfig(ApiIssuer[] issuerConfigs) { ApiIssuerConfigs.Builder builder = ApiIssuerConfigs.builder(); for (ApiIssuer issuerConfig : issuerConfigs) { builder.addIssuer( - new IssuerConfig(issuerConfig.name(), issuerConfig.issuer(), issuerConfig.jwksUri())); + new IssuerConfig(issuerConfig.name(), issuerConfig.issuer(), issuerConfig.jwksUri(), + issuerConfig.authorizationUrl(), issuerConfig.useScopesInAuthFlow())); } return builder.build(); } diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/config/jsonwriter/AbstractResourceSchemaProvider.java b/endpoints-framework/src/main/java/com/google/api/server/spi/config/jsonwriter/AbstractResourceSchemaProvider.java index d4092920..26a5545d 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/config/jsonwriter/AbstractResourceSchemaProvider.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/config/jsonwriter/AbstractResourceSchemaProvider.java @@ -40,7 +40,6 @@ public ResourceSchema getResourceSchema(TypeToken type, ApiConfig config) { @Nullable private ResourceSchema getResourceSchemaImpl(TypeToken type, ApiConfig config) { - Class clazz = type.getRawType(); List>> serializerClasses = Serializers.getSerializerClasses(type, config.getSerializationConfig()); if (!serializerClasses.isEmpty() && diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/config/jsonwriter/JacksonResourceSchemaProvider.java b/endpoints-framework/src/main/java/com/google/api/server/spi/config/jsonwriter/JacksonResourceSchemaProvider.java index df06c115..532a0867 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/config/jsonwriter/JacksonResourceSchemaProvider.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/config/jsonwriter/JacksonResourceSchemaProvider.java @@ -17,10 +17,14 @@ import com.google.api.client.util.ClassInfo; import com.google.api.server.spi.ObjectMapperUtil; +import com.google.api.server.spi.TypeLoader; import com.google.api.server.spi.config.ResourcePropertySchema; import com.google.api.server.spi.config.ResourceSchema; import com.google.api.server.spi.config.annotationreader.ApiAnnotationIntrospector; +import com.google.api.server.spi.config.annotationreader.ApiConfigAnnotationReader; +import com.google.api.server.spi.config.annotationreader.ApiConfigAnnotations; import com.google.api.server.spi.config.model.ApiConfig; +import com.google.api.server.spi.config.model.ApiValidationConstraints; import com.google.api.server.spi.config.model.Types; import com.google.common.collect.ImmutableSet; import com.google.common.flogger.FluentLogger; @@ -33,6 +37,7 @@ import com.fasterxml.jackson.databind.introspect.AnnotatedMethod; import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; @@ -47,6 +52,15 @@ public class JacksonResourceSchemaProvider extends AbstractResourceSchemaProvide private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + private final TypeLoader typeLoader; + private final ApiConfigAnnotationReader annotationReader; + + public JacksonResourceSchemaProvider(TypeLoader typeLoader) { + super(); + this.typeLoader = typeLoader; + this.annotationReader = new ApiConfigAnnotationReader(typeLoader.getAnnotationTypes()); + } + @Override public ResourceSchema getResourceSchema(TypeToken type, ApiConfig config) { ResourceSchema schema = super.getResourceSchema(type, config); @@ -72,6 +86,8 @@ public ResourceSchema getResourceSchema(TypeToken type, ApiConfig config) { if (propertyType != null) { ResourcePropertySchema propertySchema = ResourcePropertySchema.of(propertyType); propertySchema.setDescription(definition.getMetadata().getDescription()); + propertySchema.setRequired(definition.getMetadata().getRequired()); + propertySchema.setValidationConstraints(extractValidationConstraints(definition)); schemaBuilder.addProperty(name, propertySchema); } else { logger.atWarning().log("No type found for property '%s' on class '%s'.", name, type); @@ -84,6 +100,18 @@ public ResourceSchema getResourceSchema(TypeToken type, ApiConfig config) { return schemaBuilder.build(); } + private ApiValidationConstraints extractValidationConstraints(BeanPropertyDefinition definition) { + if (definition.getField() == null) { + return null; + } + ApiConfigAnnotations configAnnotations = new ApiConfigAnnotations(definition.getField().getAnnotated(), typeLoader.getAnnotationTypes()); + try { + return annotationReader.buildApiValidationConstraints(configAnnotations); + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new IllegalStateException(e); + } + } + private static Set getGenericDataFieldNames(TypeToken type) { if (!Types.isJavaClientEntity(type)) { return null; @@ -128,7 +156,7 @@ private TypeToken getPropertyType(TypeToken beanType, Method readMethod, M } } else if (field != null) { return ApiAnnotationIntrospector.getSchemaType( - beanType.resolveType(field.getGenericType()), config); + beanType.resolveType(field.getAnnotated().getGenericType()), config); } return null; } diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/config/jsonwriter/JsonConfigWriter.java b/endpoints-framework/src/main/java/com/google/api/server/spi/config/jsonwriter/JsonConfigWriter.java index 25980885..210d71c4 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/config/jsonwriter/JsonConfigWriter.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/config/jsonwriter/JsonConfigWriter.java @@ -75,18 +75,19 @@ public class JsonConfigWriter implements ApiConfigWriter { private final TypeLoader typeLoader; private final ApiConfigValidator validator; - - private final ResourceSchemaProvider resourceSchemaProvider = new JacksonResourceSchemaProvider(); + private final ResourceSchemaProvider resourceSchemaProvider; public JsonConfigWriter() throws ClassNotFoundException { this.typeLoader = new TypeLoader(JsonConfigWriter.class.getClassLoader()); this.validator = new ApiConfigValidator(typeLoader, new SchemaRepository(typeLoader)); + this.resourceSchemaProvider = new JacksonResourceSchemaProvider(typeLoader); } public JsonConfigWriter(TypeLoader typeLoader, ApiConfigValidator validator) throws ClassNotFoundException { this.typeLoader = typeLoader; this.validator = validator; + this.resourceSchemaProvider = new JacksonResourceSchemaProvider(typeLoader); } private static final ObjectMapper objectMapper = ObjectMapperUtil.createStandardObjectMapper(); diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/config/model/ApiClassConfig.java b/endpoints-framework/src/main/java/com/google/api/server/spi/config/model/ApiClassConfig.java index 1cb4cbfb..3ad9e2ae 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/config/model/ApiClassConfig.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/config/model/ApiClassConfig.java @@ -15,12 +15,10 @@ */ package com.google.api.server.spi.config.model; -import com.google.api.server.spi.Constant; import com.google.api.server.spi.EndpointMethod; import com.google.api.server.spi.TypeLoader; import com.google.api.server.spi.config.AuthLevel; import com.google.api.server.spi.config.Authenticator; -import com.google.api.server.spi.config.PeerAuthenticator; import com.google.api.server.spi.config.scope.AuthScopeExpression; import com.google.common.base.Preconditions; @@ -52,7 +50,6 @@ public class ApiClassConfig { private ApiIssuerAudienceConfig issuerAudiences; private List clientIds; private List> authenticators; - private List> peerAuthenticators; private Boolean apiKeyRequired; private final MethodConfigMap methods; @@ -69,7 +66,6 @@ public ApiClassConfig(ApiConfig apiConfig, TypeLoader typeLoader, Class apiCl this.issuerAudiences = ApiIssuerAudienceConfig.UNSPECIFIED; this.clientIds = null; this.authenticators = null; - this.peerAuthenticators = null; this.useDatastore = null; this.methods = new MethodConfigMap(this); this.apiKeyRequired = null; @@ -88,8 +84,6 @@ public ApiClassConfig(ApiClassConfig original, ApiConfig apiConfig) { this.clientIds = original.clientIds == null ? null : new ArrayList<>(original.clientIds); this.authenticators = original.authenticators == null ? null : new ArrayList<>(original.authenticators); - this.peerAuthenticators = - original.peerAuthenticators == null ? null : new ArrayList<>(original.peerAuthenticators); this.useDatastore = original.useDatastore; this.methods = new MethodConfigMap(original.methods, this); this.apiKeyRequired = original.apiKeyRequired; @@ -111,7 +105,6 @@ public boolean equals(Object o) { Objects.equals(issuerAudiences, config.issuerAudiences) && Objects.equals(clientIds, config.clientIds) && Objects.equals(authenticators, config.authenticators) && - Objects.equals(peerAuthenticators, config.peerAuthenticators) && Objects.equals(useDatastore, config.useDatastore) && methods.equals(config.methods) && Objects.equals(apiKeyRequired, config.apiKeyRequired); @@ -123,7 +116,7 @@ public boolean equals(Object o) { @Override public int hashCode() { return Objects.hash(apiClassJavaName, apiClassJavaSimpleName, typeLoader, resource, - authLevel, scopeExpression, audiences, clientIds, authenticators, peerAuthenticators, + authLevel, scopeExpression, audiences, clientIds, authenticators, useDatastore, methods, issuerAudiences, apiKeyRequired); } @@ -174,7 +167,7 @@ public List getAudiences() { public void setIssuerAudiences(ApiIssuerAudienceConfig issuerAudiences) { Preconditions.checkNotNull(issuerAudiences, "issuerAudiences should never be null"); this.issuerAudiences = issuerAudiences; - if (issuerAudiences.hasIssuer(Constant.GOOGLE_ID_TOKEN_NAME)) { + if (issuerAudiences.hasGoogleIssuer()) { getApiConfig().ensureGoogleIssuer(); } } @@ -199,15 +192,6 @@ public List> getAuthenticators() { return authenticators != null ? authenticators : apiConfig.getAuthenticators(); } - - public void setPeerAuthenticators(List> peerAuthenticators) { - this.peerAuthenticators = peerAuthenticators; - } - - public List> getPeerAuthenticators() { - return peerAuthenticators != null ? peerAuthenticators : apiConfig.getPeerAuthenticators(); - } - public void setUseDatastore(boolean useDatastore) { this.useDatastore = useDatastore; } diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/config/model/ApiConfig.java b/endpoints-framework/src/main/java/com/google/api/server/spi/config/model/ApiConfig.java index a76b26bc..93965ec1 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/config/model/ApiConfig.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/config/model/ApiConfig.java @@ -21,7 +21,6 @@ import com.google.api.server.spi.config.ApiConfigInconsistency; import com.google.api.server.spi.config.AuthLevel; import com.google.api.server.spi.config.Authenticator; -import com.google.api.server.spi.config.PeerAuthenticator; import com.google.api.server.spi.config.scope.AuthScopeExpression; import com.google.api.server.spi.config.scope.AuthScopeExpressions; import com.google.common.base.Preconditions; @@ -74,7 +73,6 @@ public class ApiConfig { private ApiIssuerAudienceConfig issuerAudiences; private List clientIds; private List> authenticators; - private List> peerAuthenticators; private boolean apiKeyRequired; private final ApiAuthConfig authConfig; @@ -141,7 +139,6 @@ protected ApiConfig(ApiConfig original) { this.issuerAudiences = original.issuerAudiences; this.clientIds = original.clientIds == null ? null : new ArrayList<>(original.clientIds); this.authenticators = original.authenticators; - this.peerAuthenticators = original.peerAuthenticators; this.apiKeyRequired = original.apiKeyRequired; this.apiLimitMetrics = original.apiLimitMetrics; this.authConfig = new ApiAuthConfig(original.authConfig); @@ -193,7 +190,6 @@ public Iterable> getConfigurationInconsistencies( .addIfInconsistent("issuerAudiencies", issuerAudiences, config.issuerAudiences) .addIfInconsistent("clientIds", clientIds, config.clientIds) .addIfInconsistent("authenticators", authenticators, config.authenticators) - .addIfInconsistent("peerAuthenticators", peerAuthenticators, config.peerAuthenticators) .addIfInconsistent("apiKeyRequired", apiKeyRequired, config.apiKeyRequired) .addIfInconsistent("apiLimitMetrics", apiLimitMetrics, config.apiLimitMetrics) .addAll(authConfig.getConfigurationInconsistencies(config.authConfig)) @@ -209,7 +205,7 @@ public int hashCode() { return Objects.hash(typeLoader, root, name, canonicalName, version, title, description, documentationLink, backendRoot, isAbstract, defaultVersion, discoverable, useDatastore, resource, authLevel, scopeExpression, audiences, clientIds, authenticators, - peerAuthenticators, authConfig, cacheControlConfig, frontendLimitsConfig, + authConfig, cacheControlConfig, frontendLimitsConfig, serializationConfig, apiClassConfig, issuers, issuerAudiences, apiKeyRequired, apiLimitMetrics); } @@ -279,7 +275,6 @@ protected void setDefaults(ServiceContext serviceContext) { issuerAudiences = ApiIssuerAudienceConfig.EMPTY; clientIds = DEFAULT_CLIENT_IDS; authenticators = null; - peerAuthenticators = null; apiKeyRequired = false; apiLimitMetrics = ImmutableList.of(); } @@ -453,7 +448,7 @@ public ApiIssuerConfigs getIssuers() { public void setIssuerAudiences(ApiIssuerAudienceConfig issuerAudiences) { Preconditions.checkNotNull(issuerAudiences, "issuerAudiences should never be null"); this.issuerAudiences = issuerAudiences; - if (issuerAudiences.hasIssuer(Constant.GOOGLE_ID_TOKEN_NAME)) { + if (issuerAudiences.hasGoogleIssuer()) { ensureGoogleIssuer(); } } @@ -478,14 +473,6 @@ public List> getAuthenticators() { return authenticators; } - public void setPeerAuthenticators(List> peerAuthenticators) { - this.peerAuthenticators = peerAuthenticators; - } - - public List> getPeerAuthenticators() { - return peerAuthenticators; - } - public void setApiKeyRequired(boolean apiKeyRequired) { this.apiKeyRequired = apiKeyRequired; } diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/config/model/ApiIssuerAudienceConfig.java b/endpoints-framework/src/main/java/com/google/api/server/spi/config/model/ApiIssuerAudienceConfig.java index a1aab9d1..192ea800 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/config/model/ApiIssuerAudienceConfig.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/config/model/ApiIssuerAudienceConfig.java @@ -15,7 +15,7 @@ */ package com.google.api.server.spi.config.model; -import com.google.common.collect.ImmutableListMultimap; +import com.google.api.server.spi.Constant; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; @@ -51,8 +51,9 @@ public boolean isEmpty() { return issuerAudiences.isEmpty(); } - public boolean hasIssuer(String issuer) { - return issuerAudiences.containsKey(issuer); + public boolean hasGoogleIssuer() { + return issuerAudiences.containsKey(Constant.GOOGLE_ID_TOKEN_NAME) + || issuerAudiences.containsKey(Constant.GOOGLE_ID_TOKEN_ALT); } public ImmutableSet getIssuerNames() { diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/config/model/ApiIssuerConfigs.java b/endpoints-framework/src/main/java/com/google/api/server/spi/config/model/ApiIssuerConfigs.java index 296ad793..db70fcde 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/config/model/ApiIssuerConfigs.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/config/model/ApiIssuerConfigs.java @@ -10,14 +10,19 @@ */ public class ApiIssuerConfigs { static final String UNSPECIFIED_NAME = "_unspecified_issuer_name"; + + //according to https://developers.google.com/identity/protocols/OpenIDConnect#server-flow + //issuer can be either with or without https:// prefix public static final IssuerConfig GOOGLE_ID_TOKEN_ISSUER = new IssuerConfig( - Constant.GOOGLE_ID_TOKEN_NAME, "accounts.google.com", - "https://www.googleapis.com/oauth2/v1/certs"); + Constant.GOOGLE_ID_TOKEN_NAME, "https://accounts.google.com", + Constant.GOOGLE_JWKS_URI, + Constant.GOOGLE_AUTH_URL, true); public static final IssuerConfig GOOGLE_ID_TOKEN_ISSUER_ALT = new IssuerConfig( - Constant.GOOGLE_ID_TOKEN_NAME_HTTPS, "https://accounts.google.com", - "https://www.googleapis.com/oauth2/v1/certs"); + Constant.GOOGLE_ID_TOKEN_ALT, "accounts.google.com", + Constant.GOOGLE_JWKS_URI, + Constant.GOOGLE_AUTH_URL, true); public static final ApiIssuerConfigs UNSPECIFIED = builder() - .addIssuer(new IssuerConfig(UNSPECIFIED_NAME, null, null)) + .addIssuer(new IssuerConfig(UNSPECIFIED_NAME, null, null, "", false)) .build(); public static final ApiIssuerConfigs EMPTY = builder().build(); private final ImmutableMap issuerConfigs; @@ -43,8 +48,7 @@ public boolean isSpecified() { } public ApiIssuerConfigs withGoogleIdToken() { - if (hasIssuer(Constant.GOOGLE_ID_TOKEN_NAME) - && hasIssuer(Constant.GOOGLE_ID_TOKEN_NAME_HTTPS)) { + if (hasIssuer(Constant.GOOGLE_ID_TOKEN_NAME) && hasIssuer(Constant.GOOGLE_ID_TOKEN_ALT)) { return this; } Builder builder = builder(); @@ -74,11 +78,16 @@ public static class IssuerConfig { private final String name; private final String issuer; private final String jwksUri; + private final String authorizationUrl; + private final boolean useScopesInAuthFlow; - public IssuerConfig(String name, String issuer, String jwksUri) { + public IssuerConfig(String name, String issuer, String jwksUri, String authorizationUrl, + boolean useScopesInAuthFlow) { this.name = name; this.issuer = issuer; this.jwksUri = jwksUri; + this.authorizationUrl = authorizationUrl; + this.useScopesInAuthFlow = useScopesInAuthFlow; } public String getName() { @@ -93,12 +102,22 @@ public String getJwksUri() { return jwksUri; } + public String getAuthorizationUrl() { + return authorizationUrl; + } + + public boolean isUseScopesInAuthFlow() { + return useScopesInAuthFlow; + } + @Override public boolean equals(Object o) { return o != null && o instanceof IssuerConfig && Objects.equals(name, ((IssuerConfig) o).name) && Objects.equals(issuer, ((IssuerConfig) o).issuer) - && Objects.equals(jwksUri, ((IssuerConfig) o).jwksUri); + && Objects.equals(jwksUri, ((IssuerConfig) o).jwksUri) + && Objects.equals(authorizationUrl, ((IssuerConfig) o).authorizationUrl) + && Objects.equals(useScopesInAuthFlow, ((IssuerConfig) o).useScopesInAuthFlow); } @Override diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/config/model/ApiMethodConfig.java b/endpoints-framework/src/main/java/com/google/api/server/spi/config/model/ApiMethodConfig.java index be1e6adf..db02eeac 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/config/model/ApiMethodConfig.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/config/model/ApiMethodConfig.java @@ -15,16 +15,27 @@ */ package com.google.api.server.spi.config.model; -import com.google.api.server.spi.Constant; +import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT; +import static javax.servlet.http.HttpServletResponse.SC_OK; + import com.google.api.server.spi.EndpointMethod; +import com.google.api.server.spi.ServiceException; import com.google.api.server.spi.TypeLoader; import com.google.api.server.spi.config.AuthLevel; import com.google.api.server.spi.config.Authenticator; -import com.google.api.server.spi.config.PeerAuthenticator; import com.google.api.server.spi.config.model.ApiParameterConfig.Classification; import com.google.api.server.spi.config.scope.AuthScopeExpression; +import com.google.api.server.spi.response.BadRequestException; +import com.google.api.server.spi.response.ConflictException; +import com.google.api.server.spi.response.ForbiddenException; +import com.google.api.server.spi.response.InternalServerErrorException; +import com.google.api.server.spi.response.NotFoundException; +import com.google.api.server.spi.response.ServiceUnavailableException; +import com.google.api.server.spi.response.UnauthorizedException; +import com.google.common.base.Joiner; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.common.reflect.TypeToken; import java.lang.reflect.Method; import java.lang.reflect.Type; @@ -36,6 +47,7 @@ import java.util.Objects; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.apache.commons.lang3.StringUtils; /** * Flattened method configuration for a swarm endpoint method. Data generally originates from @@ -45,6 +57,17 @@ */ public class ApiMethodConfig { + private static final Map, Integer> + KNOWN_EXCEPTION_CODES = ImmutableMap., Integer>builder() + .put(BadRequestException.class, BadRequestException.CODE) + .put(ForbiddenException.class, ForbiddenException.CODE) + .put(ServiceUnavailableException.class, ServiceUnavailableException.CODE) + .put(UnauthorizedException.class, UnauthorizedException.CODE) + .put(NotFoundException.class, NotFoundException.CODE) + .put(ConflictException.class, ConflictException.CODE) + .put(InternalServerErrorException.class, InternalServerErrorException.CODE) + .build(); + private enum RestMethod { LIST("list", "GET") { @Override @@ -135,6 +158,9 @@ public String guessResourceName( } } + /** Value of the response status when not set in the definition of the method. */ + public static final int RESPONSE_STATUS_UNSPECIFIED = -1; + private final String endpointMethodName; private final List parameterConfigs; @@ -154,10 +180,12 @@ public String guessResourceName( private ApiIssuerAudienceConfig issuerAudiences; private List clientIds; private List> authenticators; - private List> peerAuthenticators; private boolean ignored = false; + private boolean deprecated = false; private Boolean apiKeyRequired; private TypeToken returnType; + private int responseStatus; + private Class[] exceptionTypes; private List metricCosts; private final TypeLoader typeLoader; @@ -185,11 +213,10 @@ public ApiMethodConfig(ApiMethodConfig original, ApiClassConfig apiClassConfig) this.clientIds = original.clientIds == null ? null : new ArrayList<>(original.clientIds); this.authenticators = original.authenticators == null ? null : new ArrayList<>(original.authenticators); - this.peerAuthenticators = - original.peerAuthenticators == null ? null : new ArrayList<>(original.peerAuthenticators); this.ignored = original.ignored; this.apiKeyRequired = original.apiKeyRequired; this.returnType = original.returnType; + this.responseStatus = original.responseStatus; this.typeLoader = original.typeLoader; this.metricCosts = original.metricCosts; @@ -226,10 +253,11 @@ protected void setDefaults(EndpointMethod endpointMethod, TypeLoader typeLoader, issuerAudiences = ApiIssuerAudienceConfig.UNSPECIFIED; clientIds = null; authenticators = null; - peerAuthenticators = null; ignored = false; apiKeyRequired = null; returnType = endpointMethod.getReturnType(); + responseStatus = RESPONSE_STATUS_UNSPECIFIED; + exceptionTypes = endpointMethod.getMethod().getExceptionTypes(); metricCosts = ImmutableList.of(); } @@ -257,11 +285,11 @@ public boolean equals(Object o) { Objects.equals(issuerAudiences, config.issuerAudiences) && Objects.equals(clientIds, config.clientIds) && Objects.equals(authenticators, config.authenticators) && - Objects.equals(peerAuthenticators, config.peerAuthenticators) && Objects.equals(typeLoader, config.typeLoader) && ignored == config.ignored && apiKeyRequired == config.apiKeyRequired && Objects.equals(returnType, config.returnType) && + responseStatus == config.responseStatus && Objects.equals(metricCosts, config.metricCosts); } else { return false; @@ -271,8 +299,8 @@ public boolean equals(Object o) { @Override public int hashCode() { return Objects.hash(endpointMethodName, parameterConfigs, name, path, httpMethod, - scopeExpression, audiences, clientIds, authenticators, peerAuthenticators, typeLoader, - ignored, issuerAudiences, apiKeyRequired, returnType, metricCosts); + scopeExpression, audiences, clientIds, authenticators, typeLoader, + ignored, issuerAudiences, apiKeyRequired, returnType, responseStatus, metricCosts); } public ApiClassConfig getApiClassConfig() { @@ -305,9 +333,9 @@ public String getFullJavaName() { * it is non-optional and has no default. */ public ApiParameterConfig addParameter(String name, String description, boolean nullable, - String defaultValue, TypeToken type) { + String defaultValue, TypeToken type, ApiValidationConstraints validationConstraints) { ApiParameterConfig config = - new ApiParameterConfig(this, name, description, nullable, defaultValue, type, typeLoader); + new ApiParameterConfig(this, name, description, nullable, defaultValue, type, typeLoader, validationConstraints); parameterConfigs.add(config); if (config.getClassification() != Classification.INJECTED && name != null && !nullable @@ -442,7 +470,7 @@ public List getAudiences() { public void setIssuerAudiences(ApiIssuerAudienceConfig issuerAudiences) { Preconditions.checkNotNull(issuerAudiences, "issuerAudiences should never be null"); this.issuerAudiences = issuerAudiences; - if (issuerAudiences.hasIssuer(Constant.GOOGLE_ID_TOKEN_NAME)) { + if (issuerAudiences.hasGoogleIssuer()) { getApiClassConfig().getApiConfig().ensureGoogleIssuer(); } } @@ -467,14 +495,6 @@ public List> getAuthenticators() { return authenticators != null ? authenticators : apiClassConfig.getAuthenticators(); } - public void setPeerAuthenticators(List> peerAuthenticators) { - this.peerAuthenticators = peerAuthenticators; - } - - public List> getPeerAuthenticators() { - return peerAuthenticators != null ? peerAuthenticators : apiClassConfig.getPeerAuthenticators(); - } - public void setIgnored(boolean ignored) { this.ignored = ignored; } @@ -483,6 +503,14 @@ public boolean isIgnored() { return ignored; } + public void setDeprecated(boolean deprecated) { + this.deprecated = deprecated; + } + + public boolean isDeprecated() { + return deprecated; + } + public void setApiKeyRequired(boolean apiKeyRequired) { this.apiKeyRequired = apiKeyRequired; } @@ -510,6 +538,44 @@ public TypeToken getReturnType() { return returnType; } + public void setResponseStatus(int responseStatus) { + this.responseStatus = responseStatus; + } + + public int getResponseStatus() { + return responseStatus; + } + + public int getEffectiveResponseStatus() { + return responseStatus == RESPONSE_STATUS_UNSPECIFIED ? (hasResourceInResponse() ? SC_OK : SC_NO_CONTENT) : responseStatus; + } + + public class ErrorResponse { + public final int code; + public final String name; + public final String description; + + public ErrorResponse(int code, String name, String description) { + this.code = code; + this.name = name; + this.description = description; + } + } + + public List getErrorReponses() { + List responses = new ArrayList<>(); + for (Class exceptionType : exceptionTypes) { + //TODO allow custom exceptions by introducing a new annotation + Integer code = KNOWN_EXCEPTION_CODES.get(exceptionType); + if (code != null) { + String name = exceptionType.getSimpleName().replace("Exception", ""); + String description = Joiner.on(' ').join(StringUtils.splitByCharacterTypeCamelCase(name)); + responses.add(new ErrorResponse(code, name, description)); + } + } + return responses; + } + /** * Returns whether or not the method has a resource (is non-void) in the response. */ diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/config/model/ApiParameterConfig.java b/endpoints-framework/src/main/java/com/google/api/server/spi/config/model/ApiParameterConfig.java index 2e20321a..c98f71c7 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/config/model/ApiParameterConfig.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/config/model/ApiParameterConfig.java @@ -35,6 +35,7 @@ public class ApiParameterConfig { private final boolean nullable; private final String defaultValue; private final TypeToken type; + private final ApiValidationConstraints validationConstraints; private Class> serializer; private Class> repeatedItemSerializer; @@ -58,7 +59,8 @@ public enum Classification { } public ApiParameterConfig(ApiMethodConfig apiMethodConfig, String name, String description, - boolean nullable, String defaultValue, TypeToken type, TypeLoader typeLoader) { + boolean nullable, String defaultValue, TypeToken type, TypeLoader typeLoader, + ApiValidationConstraints validationConstraints) { this.apiMethodConfig = apiMethodConfig; this.name = name; this.description = description; @@ -68,6 +70,7 @@ public ApiParameterConfig(ApiMethodConfig apiMethodConfig, String name, String d this.serializer = null; this.repeatedItemSerializer = null; this.typeLoader = typeLoader; + this.validationConstraints = validationConstraints; } public ApiParameterConfig(ApiParameterConfig original, ApiMethodConfig apiMethodConfig) { @@ -80,6 +83,7 @@ public ApiParameterConfig(ApiParameterConfig original, ApiMethodConfig apiMethod this.serializer = original.serializer; this.repeatedItemSerializer = original.repeatedItemSerializer; this.typeLoader = original.typeLoader; + this.validationConstraints = new ApiValidationConstraints(original.validationConstraints); } @Override @@ -94,7 +98,8 @@ public boolean equals(Object o) { && Objects.equals(type, parameter.type) && Objects.equals(serializer, parameter.serializer) && Objects.equals(repeatedItemSerializer, parameter.repeatedItemSerializer) - && Objects.equals(typeLoader, parameter.typeLoader); + && Objects.equals(typeLoader, parameter.typeLoader) + && Objects.equals(validationConstraints, parameter.validationConstraints); } else { return false; } @@ -103,7 +108,7 @@ public boolean equals(Object o) { @Override public int hashCode() { return Objects.hash(name, nullable, defaultValue, type, serializer, repeatedItemSerializer, - typeLoader); + typeLoader, validationConstraints); } public ApiMethodConfig getApiMethodConfig() { @@ -130,6 +135,10 @@ public TypeToken getType() { return type; } + public ApiValidationConstraints getValidationConstraints() { + return validationConstraints; + } + /** * If the serialized type of the parameter is a repeated type, returns the individual item type. * Otherwise returns {@code null}. diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/config/model/ApiValidationConstraints.java b/endpoints-framework/src/main/java/com/google/api/server/spi/config/model/ApiValidationConstraints.java new file mode 100644 index 00000000..f925ea5f --- /dev/null +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/config/model/ApiValidationConstraints.java @@ -0,0 +1,97 @@ +package com.google.api.server.spi.config.model; + +import java.util.Objects; +import java.util.stream.Stream; + +public class ApiValidationConstraints { + + private final String pattern; + + private final Long min; + private final Long max; + + private final String decimalMin; + private final String decimalMax; + + private final Boolean decimalMinInclusive; + private final Boolean decimalMaxInclusive; + + private final Integer minSize; + private final Integer maxSize; + + public ApiValidationConstraints(String pattern, Long min, Long max, String decimalMin, String decimalMax, + Boolean decimalMinInclusive, Boolean decimalMaxInclusive, Integer minSize, Integer maxSize) { + this.pattern = pattern; + this.min = min; + this.max = max; + this.decimalMin = decimalMin; + this.decimalMax = decimalMax; + this.decimalMinInclusive = decimalMinInclusive; + this.decimalMaxInclusive = decimalMaxInclusive; + this.minSize = minSize; + this.maxSize = maxSize; + } + + public ApiValidationConstraints(ApiValidationConstraints original) { + this(original.pattern, original.min, original.max, original.decimalMin, original.decimalMax, + original.decimalMinInclusive, original.decimalMaxInclusive, original.minSize, original.maxSize); + } + + public String getPattern() { + return pattern; + } + + public Long getMin() { + return min; + } + + public Long getMax() { + return max; + } + + public String getDecimalMin() { + return decimalMin; + } + + public String getDecimalMax() { + return decimalMax; + } + + public Boolean getDecimalMinInclusive() { + return decimalMinInclusive; + } + + public Boolean getDecimalMaxInclusive() { + return decimalMaxInclusive; + } + + public Integer getMinSize() { + return minSize; + } + + public Integer getMaxSize() { + return maxSize; + } + + public boolean isEmpty() { + return Stream.of(pattern, min, max, decimalMin, decimalMax, decimalMinInclusive, decimalMaxInclusive, minSize, maxSize) + .allMatch(Objects::isNull); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ApiValidationConstraints that = (ApiValidationConstraints) o; + return Objects.equals(pattern, that.pattern) && Objects.equals(min, that.min) && Objects.equals(max, that.max) && Objects.equals(decimalMin, that.decimalMin) && Objects.equals(decimalMax, that.decimalMax) && Objects.equals(decimalMinInclusive, that.decimalMinInclusive) && Objects.equals(decimalMaxInclusive, that.decimalMaxInclusive) && Objects.equals(minSize, that.minSize) && Objects.equals(maxSize, that.maxSize); + } + + @Override + public int hashCode() { + return Objects.hash(pattern, min, max, decimalMin, decimalMax, decimalMinInclusive, decimalMaxInclusive, minSize, maxSize); + } +} diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/config/model/AuthScopeRepository.java b/endpoints-framework/src/main/java/com/google/api/server/spi/config/model/AuthScopeRepository.java index 73a4eea3..d5a2718d 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/config/model/AuthScopeRepository.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/config/model/AuthScopeRepository.java @@ -36,6 +36,10 @@ private static ImmutableMap loadScopeDescriptions(String fileNam throw new IllegalStateException("Cannot load scope descriptions from " + fileName, e); } } + + public static String getDescription(String scope) { + return MoreObjects.firstNonNull(GOOGLE_SCOPE_DESCRIPTIONS.get(scope), scope); + } private final SortedMap descriptionsByScope = new TreeMap<>(); @@ -46,8 +50,7 @@ public AuthScopeRepository() { public void add(AuthScopeExpression scopeExpression) { for (String scope : scopeExpression.getAllScopes()) { - String description = MoreObjects.firstNonNull(GOOGLE_SCOPE_DESCRIPTIONS.get(scope), scope); - descriptionsByScope.put(scope, description); + descriptionsByScope.put(scope, getDescription(scope)); } } diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/config/model/FieldType.java b/endpoints-framework/src/main/java/com/google/api/server/spi/config/model/FieldType.java index b96f10b4..8c29b72f 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/config/model/FieldType.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/config/model/FieldType.java @@ -7,6 +7,9 @@ import java.lang.reflect.Type; import java.util.Date; +import java.util.OptionalDouble; +import java.util.OptionalInt; +import java.util.OptionalLong; /** * Classifier and metadata for field types. @@ -62,12 +65,15 @@ public String getCollectionName() { .put(Character.TYPE, STRING) .put(Integer.class, INT32) .put(Integer.TYPE, INT32) + .put(OptionalInt.class, INT32) .put(Long.class, INT64) .put(Long.TYPE, INT64) + .put(OptionalLong.class, INT64) .put(Float.class, FLOAT) .put(Float.TYPE, FLOAT) .put(Double.class, DOUBLE) .put(Double.TYPE, DOUBLE) + .put(OptionalDouble.class, DOUBLE) .put(Boolean.class, BOOLEAN) .put(Boolean.TYPE, BOOLEAN) .put(Date.class, DATE_TIME) @@ -88,6 +94,9 @@ public static FieldType fromType(TypeToken type) { FieldType ft = TYPE_MAP.get(type.getRawType()); if (ft != null) { return ft; + } else if (Types.isOptional(type)) { + //simply unwrap the optional to get the type + return fromType(Types.getTypeParameter(type, 0)); } else if (Types.getArrayItemType(type) != null) { return ARRAY; } else if (Types.isEnumType(type)) { diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/config/model/Schema.java b/endpoints-framework/src/main/java/com/google/api/server/spi/config/model/Schema.java index 6658e566..d7e7c879 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/config/model/Schema.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/config/model/Schema.java @@ -27,12 +27,12 @@ public abstract class Schema { /** * If the schema is an enum, a list of possible enum values in their string representation. */ - @Nullable public abstract ImmutableList enumValues(); + public abstract ImmutableList enumValues(); /** * If the schema is an enum, a list of enum value descriptions. */ - @Nullable public abstract ImmutableList enumDescriptions(); + public abstract ImmutableList enumDescriptions(); public static Builder builder() { return new AutoValue_Schema.Builder(); @@ -82,8 +82,12 @@ public static abstract class Field { /** The type classification of the field. */ public abstract FieldType type(); + /** The description of the field. */ @Nullable public abstract String description(); + /** The required status of the field. */ + @Nullable public abstract Boolean required(); + /** * If {@link #type()} is {@link FieldType#OBJECT}, a reference to the schema type that the field * refers to. @@ -94,6 +98,10 @@ public static abstract class Field { * If {@link type()} is {@link FieldType#ARRAY}, a reference to the array item type. */ @Nullable public abstract Field arrayItemSchema(); + + /** The validation constraints of the field. */ + @Nullable public abstract FieldConstraints constraints(); + public static Builder builder() { return new AutoValue_Schema_Field.Builder(); } @@ -106,12 +114,56 @@ public abstract static class Builder { public abstract Builder setName(String name); public abstract Builder setType(FieldType type); public abstract Builder setDescription(String description); + public abstract Builder setRequired(Boolean required); public abstract Builder setSchemaReference(SchemaReference ref); public abstract Builder setArrayItemSchema(Field schema); + public abstract Builder setConstraints(FieldConstraints constraints); public abstract Field build(); } } + /** + * Representation of a field in a JSON object. + */ + @AutoValue + public static abstract class FieldConstraints { + + @Nullable public abstract String pattern(); + + @Nullable public abstract Long min(); + @Nullable public abstract Long max(); + + @Nullable public abstract String decimalMin(); + @Nullable public abstract String decimalMax(); + + @Nullable public abstract Boolean decimalMinInclusive(); + @Nullable public abstract Boolean decimalMaxInclusive(); + + @Nullable public abstract Integer minSize(); + @Nullable public abstract Integer maxSize(); + + public static Builder builder() { + return new AutoValue_Schema_FieldConstraints.Builder(); + } + + /** + * A {@link FieldConstraints} builder. + */ + @AutoValue.Builder + public abstract static class Builder { + public abstract Builder setPattern(String pattern); + public abstract Builder setMin(Long min); + public abstract Builder setMax(Long max); + public abstract Builder setDecimalMin(String decimalMin); + public abstract Builder setDecimalMax(String decimalMax); + public abstract Builder setDecimalMinInclusive(Boolean decimalMinInclusive); + public abstract Builder setDecimalMaxInclusive(Boolean decimalMaxInclusive); + public abstract Builder setMinSize(Integer minSize); + public abstract Builder setMaxSize(Integer maxSize); + public abstract FieldConstraints build(); + } + } + /** * A lazy reference to another {@link Schema} within a {@link SchemaRepository}. Because of the * way types are constructed, some schema references shouldn't be resolved until needed. This diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/config/model/SchemaRepository.java b/endpoints-framework/src/main/java/com/google/api/server/spi/config/model/SchemaRepository.java index 56dc21cd..b78d2b90 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/config/model/SchemaRepository.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/config/model/SchemaRepository.java @@ -8,17 +8,18 @@ import com.google.api.server.spi.config.annotationreader.ApiAnnotationIntrospector; import com.google.api.server.spi.config.jsonwriter.JacksonResourceSchemaProvider; import com.google.api.server.spi.config.jsonwriter.ResourceSchemaProvider; +import com.google.api.server.spi.config.model.Schema.Builder; import com.google.api.server.spi.config.model.Schema.Field; import com.google.api.server.spi.config.model.Schema.SchemaReference; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Optional; +import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.LinkedHashMultimap; import com.google.common.collect.Multimap; import com.google.common.reflect.TypeToken; import java.util.EnumSet; -import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -60,12 +61,13 @@ public class SchemaRepository { private final Multimap schemaByApiKeys = LinkedHashMultimap.create(); private final Map, Schema>> types = Maps.newLinkedHashMap(); - private final ResourceSchemaProvider resourceSchemaProvider = new JacksonResourceSchemaProvider(); + private final ResourceSchemaProvider resourceSchemaProvider; private final TypeLoader typeLoader; public SchemaRepository(TypeLoader typeLoader) { this.typeLoader = typeLoader; + this.resourceSchemaProvider = new JacksonResourceSchemaProvider(typeLoader); } /** @@ -123,7 +125,7 @@ private Map, Schema> getAllTypesForConfig(ApiConfig config) { } private Schema getOrCreateTypeForConfig( - TypeToken type, Map, Schema> typesForConfig, ApiConfig config) { + TypeToken type, Map, Schema> typesForConfig, ApiConfig config) { type = ApiAnnotationIntrospector.getSchemaType(type, config); Schema schema = typesForConfig.get(type); ApiKey key = config.getApiKey().withoutRoot(); @@ -138,62 +140,50 @@ private Schema getOrCreateTypeForConfig( // We put a placeholder in because this is a recursive process that may result in circular // references. This should never be returned in the public interface. typesForConfig.put(type, PLACEHOLDER_SCHEMA); - TypeToken arrayItemType = Types.getArrayItemType(type); if (typeLoader.isSchemaType(type)) { - throw new IllegalArgumentException("Can't add a primitive type as a resource"); - } else if (arrayItemType != null) { - Field.Builder arrayItemSchema = Field.builder().setName(ARRAY_UNUSED_MSG); - fillInFieldInformation(arrayItemSchema, arrayItemType, null, typesForConfig, config); - schema = Schema.builder() - .setName(Types.getSimpleName(type, config.getSerializationConfig())) - .setType("object") - .addField("items", Field.builder() - .setName("items") - .setType(FieldType.ARRAY) - .setArrayItemSchema(arrayItemSchema.build()) - .build()) - .build(); - typesForConfig.put(type, schema); - schemaByApiKeys.put(key, schema); - return schema; - } else if (Types.isObject(type)) { - typesForConfig.put(type, ANY_SCHEMA); - schemaByApiKeys.put(key, ANY_SCHEMA); - return ANY_SCHEMA; - } else if (Types.isMapType(type)) { - schema = MAP_SCHEMA; - final TypeToken> mapSupertype = type.getSupertype(Map.class); - final boolean hasConcreteKeyValue = Types.isConcreteType(mapSupertype.getType()); - boolean forceJsonMapSchema = EndpointsFlag.MAP_SCHEMA_FORCE_JSON_MAP_SCHEMA.isEnabled(); - if (hasConcreteKeyValue && !forceJsonMapSchema) { - schema = createMapSchema(mapSupertype, typesForConfig, config).or(schema); - } - typesForConfig.put(type, schema); - schemaByApiKeys.put(key, schema); - return schema; - } else if (Types.isEnumType(type)) { - Schema.Builder builder = Schema.builder() - .setName(Types.getSimpleName(type, config.getSerializationConfig())) - .setType("string"); - for (java.lang.reflect.Field field : type.getRawType().getFields()) { - if (field.isEnumConstant()) { - builder.addEnumValue(field.getName()); - Description description = field.getAnnotation(Description.class); - builder.addEnumDescription(description == null ? "" : description.value()); + throw new IllegalArgumentException("Can't use a primitive type as a resource" + getExceptionSuffix(type, config)); + } else { + if (Types.isArrayType(type)) { + schema = createArraySchema(type, typesForConfig, config); + } else if (Types.isObject(type)) { + schema = ANY_SCHEMA; + } else if (Types.isMapType(type)) { + schema = MAP_SCHEMA; + final TypeToken> mapSupertype = ((TypeToken) type).getSupertype(Map.class); + final boolean hasConcreteKeyValue = Types.isConcreteType(mapSupertype.getType()); + boolean forceJsonMapSchema = EndpointsFlag.MAP_SCHEMA_FORCE_JSON_MAP_SCHEMA.isEnabled(); + if (hasConcreteKeyValue && !forceJsonMapSchema) { + schema = createMapSchema(mapSupertype, typesForConfig, config).or(schema); + } + } else if (Types.isEnumType(type)) { + schema = createEnumSchema(type, config); + } else if (Types.isOptional(type)) { + schema = ANY_SCHEMA; + if (Types.isConcreteType(type.getType())) { + TypeToken optionalType = Types.getTypeParameter(type, 0); + if (Types.isOptional(optionalType)) { + throw new IllegalArgumentException("Recursive Optional is not supported" + getExceptionSuffix(type, config)); + } + if (Types.isArrayType(optionalType) || Types.isMapType(optionalType) || Types.isObject(optionalType)) { + throw new IllegalArgumentException("Optional of array-like type, Map or Object is not supported" + getExceptionSuffix(type, config)); + } + schema = Types.isEnumType(optionalType) + ? createEnumSchema(optionalType, config) + : createBeanSchema(optionalType, typesForConfig, config); } + } else { + schema = createBeanSchema(type, typesForConfig, config); } - schema = builder.build(); - typesForConfig.put(type, schema); - schemaByApiKeys.put(key, schema); - return schema; - } else { - schema = createBeanSchema(type, typesForConfig, config); typesForConfig.put(type, schema); schemaByApiKeys.put(key, schema); return schema; } } + private String getExceptionSuffix(TypeToken type, ApiConfig config) { + return ": '" + type + "' used in " + config.getApiKey(); + } + private void addSchemaToApi(ApiKey key, Schema schema) { if (schemaByApiKeys.containsEntry(key, schema)) { return; @@ -208,10 +198,37 @@ private void addSchemaToApi(ApiKey key, Schema schema) { } } Field mapValueSchema = schema.mapValueSchema(); - if (mapValueSchema != null && mapValueSchema.schemaReference() != null) { - addSchemaToApi(key, mapValueSchema.schemaReference().get()); + if (mapValueSchema != null) { + while (mapValueSchema.type() == FieldType.ARRAY) { + mapValueSchema = mapValueSchema.arrayItemSchema(); + } + if (mapValueSchema.schemaReference() != null) { + addSchemaToApi(key, mapValueSchema.schemaReference().get()); + } } } + + private Schema createArraySchema( + TypeToken type, Map, Schema> typesForConfig, ApiConfig config) { + TypeToken arrayItemType = Types.getArrayItemType(type); + String simpleName = Types.getSimpleName(type, config.getSerializationConfig()); + Field.Builder arrayItemSchema = Field.builder().setName(ARRAY_UNUSED_MSG); + fillInFieldInformation(arrayItemSchema, arrayItemType, typesForConfig, config); + Field arrayField = arrayItemSchema.build(); + Builder builder = Schema.builder() + .setName(simpleName) + .setType("object") + .addField("items", Field.builder() + .setName("items") + .setType(FieldType.ARRAY) + .setArrayItemSchema(arrayField) + .build()); + SchemaReference itemSchema = arrayField.schemaReference(); + if (itemSchema != null) { + builder.setDescription("An ordered list of " + itemSchema.get().name()); + } + return builder.build(); + } private Optional createMapSchema( TypeToken> mapType, Map, Schema> typesForConfig, ApiConfig config) { @@ -241,8 +258,14 @@ private Optional createMapSchema( .setName(Types.getSimpleName(mapType, config.getSerializationConfig())) .setType("object"); Field.Builder fieldBuilder = Field.builder().setName(MAP_UNUSED_MSG); - fillInFieldInformation(fieldBuilder, valueSchemaType, null, typesForConfig, config); - return Optional.of(builder.setMapValueSchema(fieldBuilder.build()).build()); + fillInFieldInformation(fieldBuilder, valueSchemaType, typesForConfig, config); + Field mapValueField = fieldBuilder.build(); + SchemaReference valueSchema = mapValueField.schemaReference(); + if (valueSchema != null) { + builder.setDescription( + String.format("A collection of name / %s pairs", valueSchema.get().name())); + } + return Optional.of(builder.setMapValueSchema(mapValueField).build()); } private Schema createBeanSchema( @@ -250,26 +273,62 @@ private Schema createBeanSchema( Schema.Builder builder = Schema.builder() .setName(Types.getSimpleName(type, config.getSerializationConfig())) .setType("object"); + setSchemaDescription(type, builder); ResourceSchema schema = resourceSchemaProvider.getResourceSchema(type, config); for (Entry entry : schema.getProperties().entrySet()) { String propertyName = entry.getKey(); ResourcePropertySchema propertySchema = entry.getValue(); TypeToken propertyType = propertySchema.getType(); if (propertyType != null) { - Field.Builder fieldBuilder = Field.builder().setName(propertyName); - fillInFieldInformation(fieldBuilder, propertyType, propertySchema.getDescription(), - typesForConfig, config); + String description = propertySchema.getDescription(); + Field.Builder fieldBuilder = Field.builder() + .setName(propertyName) + .setDescription(Strings.isNullOrEmpty(description) ? null : description) + .setRequired(propertySchema.getRequired()) + .setConstraints(toFieldConstraints(propertySchema.getValidationConstraints())); + fillInFieldInformation(fieldBuilder, propertyType, typesForConfig, config); builder.addField(propertyName, fieldBuilder.build()); } } return builder.build(); } - + + private Schema.FieldConstraints toFieldConstraints(ApiValidationConstraints validationConstraints) { + Schema.FieldConstraints constraints = null; + if (validationConstraints != null && !validationConstraints.isEmpty()) { + constraints = Schema.FieldConstraints.builder() + .setPattern(validationConstraints.getPattern()) + .setMin(validationConstraints.getMin()) + .setMax(validationConstraints.getMax()) + .setDecimalMin(validationConstraints.getDecimalMin()) + .setDecimalMax(validationConstraints.getDecimalMax()) + .setDecimalMinInclusive(validationConstraints.getDecimalMinInclusive()) + .setDecimalMaxInclusive(validationConstraints.getDecimalMaxInclusive()) + .setMinSize(validationConstraints.getMinSize()) + .setMaxSize(validationConstraints.getMaxSize()) + .build(); + } + return constraints; + } + + private Schema createEnumSchema(TypeToken type, ApiConfig config) { + Map valuesAndDescriptions + = Types.getEnumValuesAndDescriptions((TypeToken>) type); + Builder builder = Schema.builder() + .setName(Types.getSimpleName(type, config.getSerializationConfig())) + .setType("string"); + setSchemaDescription(type, builder); + for (Entry entry : valuesAndDescriptions.entrySet()) { + builder.addEnumValue(entry.getKey()); + builder.addEnumDescription(entry.getValue()); + } + return builder.build(); + } + private void fillInFieldInformation(Field.Builder builder, TypeToken fieldType, - String description, Map, Schema> typesForConfig, ApiConfig config) { + Map, Schema> typesForConfig, ApiConfig config) { FieldType ft = FieldType.fromType(fieldType); builder.setType(ft); - builder.setDescription(description); if (ft == FieldType.OBJECT || ft == FieldType.ENUM) { getOrCreateTypeForConfig(fieldType, typesForConfig, config); builder.setSchemaReference(SchemaReference.create(this, config, fieldType)); @@ -278,10 +337,21 @@ private void fillInFieldInformation(Field.Builder builder, TypeToken fieldTyp fillInFieldInformation( arrayItemBuilder, ApiAnnotationIntrospector.getSchemaType(Types.getArrayItemType(fieldType), config), - null, typesForConfig, config); builder.setArrayItemSchema(arrayItemBuilder.build()); } } + + private void setSchemaDescription(TypeToken type, Builder builder) { + Description description = type.getRawType().getAnnotation(Description.class); + if (description != null && !Strings.isNullOrEmpty(description.value())) { + builder.setDescription(description.value()); + } + } + + public static boolean isJsonMapSchema(Schema schema) { + return schema == MAP_SCHEMA; + } + } diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/config/model/Types.java b/endpoints-framework/src/main/java/com/google/api/server/spi/config/model/Types.java index 1a3d80d2..21b20a16 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/config/model/Types.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/config/model/Types.java @@ -15,8 +15,12 @@ */ package com.google.api.server.spi.config.model; +import com.fasterxml.jackson.databind.SerializationConfig; +import com.fasterxml.jackson.databind.util.EnumValues; import com.google.api.client.util.GenericData; import com.google.api.client.util.Preconditions; +import com.google.api.server.spi.ObjectMapperUtil; +import com.google.api.server.spi.config.Description; import com.google.api.server.spi.config.ResourceSchema; import com.google.api.server.spi.config.ResourceTransformer; import com.google.api.server.spi.config.Transformer; @@ -32,7 +36,10 @@ import java.lang.reflect.WildcardType; import java.util.Arrays; import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; +import java.util.Optional; import javax.annotation.Nullable; @@ -56,6 +63,33 @@ public static boolean isEnumType(TypeToken type) { return type.isSubtypeOf(Enum.class); } + /** + * Determines enum constant names for serialization, and their description for API schema. + * + * @param type an enum type + * @return a map containing the enum field names as keys, + * and schema description as values (possibly empty but not null). + */ + public static Map getEnumValuesAndDescriptions(TypeToken> type) { + Class> enumType = (Class>) type.getRawType(); + Map descriptions = new HashMap<>(); + for (java.lang.reflect.Field field : enumType.getFields()) { + if (field.isEnumConstant()) { + Description description = field.getAnnotation(Description.class); + descriptions.put(field.getName(), description != null ? description.value() : ""); + } + } + SerializationConfig serializationConfig = ObjectMapperUtil.createStandardObjectMapper() + .getSerializationConfig(); + EnumValues enumValues = EnumValues.construct(serializationConfig, enumType); + Map valueAndDescription = new LinkedHashMap<>(); + for (Enum enumConstant : enumType.getEnumConstants()) { + valueAndDescription.put(enumValues.serializedValueFor(enumConstant).toString(), + descriptions.get(enumConstant.name())); + } + return valueAndDescription; + } + /** * Returns whether or not this type is a {@link Map}. This excludes {@link GenericData}, which is * used by the Google Java client library as a supertype of resource types with concrete fields. @@ -119,8 +153,14 @@ public static boolean isWildcardType(TypeToken type) { public static boolean isObject(TypeToken type) { return type.getType() == Object.class; } - - + + /** + * Returns whether or not this type is {@link Optional}. + */ + public static boolean isOptional(TypeToken type) { + return type.getRawType() == Optional.class; + } + /** * Gets a simple name for a type that's suitable for use as a schema name. This will resolve any * transformations on the type, which may affect the type name. diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/config/validation/ApiConfigValidator.java b/endpoints-framework/src/main/java/com/google/api/server/spi/config/validation/ApiConfigValidator.java index e605420b..37337103 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/config/validation/ApiConfigValidator.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/config/validation/ApiConfigValidator.java @@ -239,7 +239,6 @@ private void validateMethod(ApiMethodConfig config) throws ApiMethodConfigInvali } validateNullaryConstructor(config.getAuthenticators(), config, "custom authenticator"); - validateNullaryConstructor(config.getPeerAuthenticators(), config, "custom peer authenticator"); Set parameterNames = Sets.newHashSet(); for (ApiParameterConfig parameter : config.getParameterConfigs()) { @@ -251,6 +250,11 @@ private void validateMethod(ApiMethodConfig config) throws ApiMethodConfigInvali if (typeLoader.isSchemaType(returnType) || Types.isEnumType(returnType)) { throw new InvalidReturnTypeException(config, returnType); } + + int responseStatus = config.getResponseStatus(); + if (responseStatus != ApiMethodConfig.RESPONSE_STATUS_UNSPECIFIED && (responseStatus < 200 || responseStatus > 299)) { + throw new InvalidResponseStatusException(config, responseStatus); + } } private void validateThirdPartyAuth(ApiMethodConfig config) diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/config/validation/InvalidResponseStatusException.java b/endpoints-framework/src/main/java/com/google/api/server/spi/config/validation/InvalidResponseStatusException.java new file mode 100644 index 00000000..70815918 --- /dev/null +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/config/validation/InvalidResponseStatusException.java @@ -0,0 +1,17 @@ +package com.google.api.server.spi.config.validation; + +import com.google.api.server.spi.config.model.ApiMethodConfig; + +/** + * Exception for API methods with an invalid status code (not a '2xx Success' code). + */ +public class InvalidResponseStatusException extends ApiMethodConfigInvalidException { + public InvalidResponseStatusException(ApiMethodConfig config, int responseStatus) { + super(config, getErrorMessage(responseStatus)); + } + + private static String getErrorMessage(int responseStatus) { + return String.format( + "Invalid response status '%d'. The response status must be a 2xx success code.", responseStatus); + } +} diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/discovery/CachingDiscoveryProvider.java b/endpoints-framework/src/main/java/com/google/api/server/spi/discovery/CachingDiscoveryProvider.java index 47ffa6d3..8d8b2c5b 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/discovery/CachingDiscoveryProvider.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/discovery/CachingDiscoveryProvider.java @@ -20,7 +20,6 @@ import com.google.api.server.spi.response.NotFoundException; import com.google.api.services.discovery.model.DirectoryList; import com.google.api.services.discovery.model.RestDescription; -import com.google.api.services.discovery.model.RpcDescription; import com.google.common.annotations.VisibleForTesting; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; @@ -42,7 +41,6 @@ public class CachingDiscoveryProvider implements DiscoveryProvider { private static final int CACHE_EXPIRY_MINS = 10; private final Cache restDocuments; - private final Cache rpcDocuments; private final Cache directoryByRoot; private final DiscoveryProvider delegate; @@ -56,9 +54,6 @@ public CachingDiscoveryProvider( restDocuments = CacheBuilder.newBuilder() .expireAfterAccess(cacheExpiry, cacheExpiryUnit) .build(); - rpcDocuments = CacheBuilder.newBuilder() - .expireAfterAccess(cacheExpiry, cacheExpiryUnit) - .build(); directoryByRoot = CacheBuilder.newBuilder() .expireAfterAccess(cacheExpiry, cacheExpiryUnit) .build(); @@ -75,17 +70,6 @@ public RestDescription call() throws NotFoundException, InternalServerErrorExcep }); } - @Override - public RpcDescription getRpcDocument(final String root, final String name, final String version) - throws NotFoundException, InternalServerErrorException { - return getDiscoveryDoc(rpcDocuments, root, name, version, new Callable() { - @Override - public RpcDescription call() throws NotFoundException, InternalServerErrorException { - return delegate.getRpcDocument(root, name, version); - } - }); - } - @Override public DirectoryList getDirectory(final String root) throws InternalServerErrorException { try { @@ -109,7 +93,6 @@ public DirectoryList call() throws Exception { @VisibleForTesting void cleanUp() { restDocuments.cleanUp(); - rpcDocuments.cleanUp(); directoryByRoot.cleanUp(); } diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/discovery/DiscoveryGenerator.java b/endpoints-framework/src/main/java/com/google/api/server/spi/discovery/DiscoveryGenerator.java index 4e741acf..ebe7a1c8 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/discovery/DiscoveryGenerator.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/discovery/DiscoveryGenerator.java @@ -20,7 +20,6 @@ import com.google.api.server.spi.ObjectMapperUtil; import com.google.api.server.spi.Strings; import com.google.api.server.spi.TypeLoader; -import com.google.api.server.spi.config.Description; import com.google.api.server.spi.config.annotationreader.ApiAnnotationIntrospector; import com.google.api.server.spi.config.model.ApiConfig; import com.google.api.server.spi.config.model.ApiKey; @@ -34,6 +33,7 @@ import com.google.api.server.spi.config.model.SchemaRepository; import com.google.api.server.spi.config.model.AuthScopeRepository; import com.google.api.server.spi.config.model.StandardParameters; +import com.google.api.server.spi.config.model.Types; import com.google.api.server.spi.config.scope.AuthScopeExpression; import com.google.api.server.spi.config.scope.AuthScopeExpressions; import com.google.api.services.discovery.model.DirectoryList; @@ -61,6 +61,8 @@ import com.google.common.collect.Maps; import com.google.common.collect.Multimaps; import com.google.common.reflect.TypeToken; + +import java.math.BigDecimal; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; @@ -69,7 +71,15 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Objects; +import java.util.Optional; import java.util.TreeMap; +import java.util.stream.Stream; + +import io.swagger.models.properties.ArrayProperty; +import io.swagger.models.properties.BaseIntegerProperty; +import io.swagger.models.properties.DecimalProperty; +import io.swagger.models.properties.StringProperty; /** * Generates discovery documents without contacting the discovery generator service. @@ -279,9 +289,21 @@ private JsonSchema convertToDiscoverySchema(Field f) { .setType(f.type().getDiscoveryType()) .setDescription(f.description()) .setFormat(f.type().getDiscoveryFormat()); + if (f.required() != null) { + fieldSchema.setRequired(f.required()); + } if (f.type() == FieldType.ARRAY) { fieldSchema.setItems(convertToDiscoverySchema(f.arrayItemSchema())); } + + Optional.ofNullable(f.constraints()).ifPresent(constraints -> { + fieldSchema.setPattern(constraints.pattern()); + // DecimalMin/Max annotations take precedence over Min/Max + Stream.of(constraints.decimalMin(), constraints.min()).filter(Objects::nonNull) + .findFirst().map(Objects::toString).ifPresent(fieldSchema::setMinimum); + Stream.of(constraints.decimalMax(), constraints.max()).filter(Objects::nonNull) + .findFirst().map(Objects::toString).ifPresent(fieldSchema::setMaximum); + }); return fieldSchema; } @@ -363,14 +385,13 @@ private JsonSchema convertMethodParameter( } if (parameterConfig.isEnum()) { + Map enumValuesAndDescriptions = Types + .getEnumValuesAndDescriptions((TypeToken>) type); List enumValues = Lists.newArrayList(); List enumDescriptions = Lists.newArrayList(); - for (java.lang.reflect.Field field : type.getRawType().getFields()) { - if (field.isEnumConstant()) { - enumValues.add(field.getName()); - Description description = field.getAnnotation(Description.class); - enumDescriptions.add(description == null ? "" : description.value()); - } + for (Entry entry : enumValuesAndDescriptions.entrySet()) { + enumValues.add(entry.getKey()); + enumDescriptions.add(entry.getValue()); } schema.setEnum(enumValues); schema.setEnumDescriptions(enumDescriptions); @@ -405,6 +426,16 @@ private JsonSchema convertMethodParameter( if (parameterConfig.getDescription() != null) { schema.setDescription(parameterConfig.getDescription()); } + Optional.ofNullable(parameterConfig.getValidationConstraints()).ifPresent(constraints -> { + if (constraints.getPattern() != null) { + schema.setPattern(constraints.getPattern()); + } + // DecimalMin/Max annotations take precedence over Min/Max + Stream.of(constraints.getDecimalMin(), constraints.getMin()).filter(Objects::nonNull) + .findFirst().map(Objects::toString).ifPresent(schema::setMinimum); + Stream.of(constraints.getDecimalMax(), constraints.getMax()).filter(Objects::nonNull) + .findFirst().map(Objects::toString).ifPresent(schema::setMaximum); + }); return schema; } diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/discovery/DiscoveryProvider.java b/endpoints-framework/src/main/java/com/google/api/server/spi/discovery/DiscoveryProvider.java index 1d48cf50..ec5ead69 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/discovery/DiscoveryProvider.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/discovery/DiscoveryProvider.java @@ -19,7 +19,6 @@ import com.google.api.server.spi.response.NotFoundException; import com.google.api.services.discovery.model.DirectoryList; import com.google.api.services.discovery.model.RestDescription; -import com.google.api.services.discovery.model.RpcDescription; /** * An interface for generating discovery documents from API configurations. @@ -34,15 +33,6 @@ public interface DiscoveryProvider { RestDescription getRestDocument(String root, String name, String version) throws NotFoundException, InternalServerErrorException; - /** - * Gets an RPC discovery document for an API. - * - * @throws NotFoundException if the API doesn't exist - * @throws InternalServerErrorException an error takes place when getting the document - */ - RpcDescription getRpcDocument(String root, String name, String version) - throws NotFoundException, InternalServerErrorException; - /** * Gets a list of REST discovery documents hosted by the current server. This method will never * return RPC discovery documents, as everything that uses online discovery uses the REST diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/discovery/LocalDiscoveryProvider.java b/endpoints-framework/src/main/java/com/google/api/server/spi/discovery/LocalDiscoveryProvider.java index 5f949932..fa39a9ef 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/discovery/LocalDiscoveryProvider.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/discovery/LocalDiscoveryProvider.java @@ -9,7 +9,6 @@ import com.google.api.services.discovery.model.DirectoryList; import com.google.api.services.discovery.model.DirectoryList.Items; import com.google.api.services.discovery.model.RestDescription; -import com.google.api.services.discovery.model.RpcDescription; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -43,12 +42,6 @@ public RestDescription getRestDocument(String root, String name, String version) return replaceRoot(doc, root); } - @Override - public RpcDescription getRpcDocument(String root, String name, String version) - throws NotFoundException { - throw new NotFoundException("RPC discovery is no longer supported."); - } - @Override public DirectoryList getDirectory(String root) { ensureDiscoveryResult(); diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/discovery/ProxyingDiscoveryProvider.java b/endpoints-framework/src/main/java/com/google/api/server/spi/discovery/ProxyingDiscoveryProvider.java index 3fb84c77..f5c64037 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/discovery/ProxyingDiscoveryProvider.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/discovery/ProxyingDiscoveryProvider.java @@ -26,7 +26,6 @@ import com.google.api.services.discovery.model.ApiConfigs; import com.google.api.services.discovery.model.DirectoryList; import com.google.api.services.discovery.model.RestDescription; -import com.google.api.services.discovery.model.RpcDescription; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; @@ -67,19 +66,6 @@ public RestDescription getRestDocument(String root, String name, String version) } } - @Override - public RpcDescription getRpcDocument(String root, String name, String version) - throws NotFoundException, InternalServerErrorException { - try { - return discovery.apis() - .generateRpc(new com.google.api.services.discovery.model.ApiConfig().setConfig( - getApiConfigStringWithRoot(getApiConfigs(name, version), root))).execute(); - } catch (IOException | ApiConfigException e) { - logger.atSevere().withCause(e).log("Could not generate or cache discovery doc"); - throw new InternalServerErrorException("Internal Server Error", e); - } - } - @Override public DirectoryList getDirectory(String root) throws InternalServerErrorException { try { diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/discovery/ProxyingDiscoveryService.java b/endpoints-framework/src/main/java/com/google/api/server/spi/discovery/ProxyingDiscoveryService.java index 1923a90f..af0a9cb4 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/discovery/ProxyingDiscoveryService.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/discovery/ProxyingDiscoveryService.java @@ -15,6 +15,7 @@ */ package com.google.api.server.spi.discovery; +import com.google.api.server.spi.RequestUtil; import com.google.api.server.spi.config.AnnotationBoolean; import com.google.api.server.spi.config.Api; import com.google.api.server.spi.config.ApiMethod; @@ -23,7 +24,6 @@ import com.google.api.server.spi.response.NotFoundException; import com.google.api.services.discovery.model.DirectoryList; import com.google.api.services.discovery.model.RestDescription; -import com.google.api.services.discovery.model.RpcDescription; import com.google.common.annotations.VisibleForTesting; import com.google.common.flogger.FluentLogger; import javax.servlet.http.HttpServletRequest; @@ -64,16 +64,6 @@ public RestDescription getRestDocument(HttpServletRequest request, @Named("api") return discoveryProvider.getRestDocument(getActualRoot(request), name, version); } - @ApiMethod( - name = "apis.getRpc", - path = "apis/{api}/{version}/rpc" - ) - public RpcDescription getRpcDocument(HttpServletRequest request, @Named("api") String name, - @Named("version") String version) throws NotFoundException, InternalServerErrorException { - checkIsInitialized(); - return discoveryProvider.getRpcDocument(getActualRoot(request), name, version); - } - @ApiMethod( name = "apis.list", path = "apis" @@ -91,8 +81,7 @@ private void checkIsInitialized() throws InternalServerErrorException { } @VisibleForTesting - static String getActualRoot(HttpServletRequest request) - throws InternalServerErrorException { + static String getActualRoot(HttpServletRequest request) throws InternalServerErrorException { String uri = request.getRequestURI(); int index = uri.indexOf("discovery/v1/apis"); if (index == -1) { @@ -100,7 +89,7 @@ static String getActualRoot(HttpServletRequest request) .log("Could not compute discovery root from url: %s", request.getRequestURI()); throw new InternalServerErrorException("Internal Server Error"); } - StringBuffer url = request.getRequestURL(); - return url.substring(0, url.length() - (uri.length() - index)); + String requestUrl = RequestUtil.getOriginalRequestUrl(request); + return requestUrl.substring(0, requestUrl.length() - (uri.length() - index)); } } diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/discovery/UNSUPPORTED_FEATURES.md b/endpoints-framework/src/main/java/com/google/api/server/spi/discovery/UNSUPPORTED_FEATURES.md new file mode 100644 index 00000000..04d2d663 --- /dev/null +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/discovery/UNSUPPORTED_FEATURES.md @@ -0,0 +1,2 @@ +- Models: + - readonly,annotations diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/dispatcher/PathTrie.java b/endpoints-framework/src/main/java/com/google/api/server/spi/dispatcher/PathTrie.java index 563bf519..aaef53b1 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/dispatcher/PathTrie.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/dispatcher/PathTrie.java @@ -17,7 +17,6 @@ import com.google.common.base.CharMatcher; import com.google.common.base.Preconditions; -import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Maps; @@ -26,12 +25,12 @@ import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.util.ArrayList; +import java.util.Arrays; import java.util.EnumMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; -import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -43,14 +42,19 @@ * path is resolved, a map from parameter names to raw String values is returned as part of the * result. Null values are not acceptable values in this trie. Parameter names can only contain * alphanumeric characters or underscores, and cannot start with a numeric. + * + * A path with a custom method + * is also supported. It has limited backward compatibility: it is not possible to mix custom + * methods with unescaped colon in path parameters. */ public class PathTrie { private static final FluentLogger log = FluentLogger.forEnclosingClass(); - private static final Splitter PATH_SPLITTER = Splitter.on('/'); private static final String PARAMETER_PATH_SEGMENT = "{}"; private static final Pattern PARAMETER_NAME_PATTERN = Pattern.compile("[a-zA-Z_][a-zA-Z_\\d]*"); // General delimiters that must be URL encoded, as defined by RFC 3986. private static final CharMatcher RESERVED_URL_CHARS = CharMatcher.anyOf(":/?#[]{}"); + //this will split a String while capturing the delimiter + private static final String SPLITTER_WITH_DELIMITER = "((?=[%1$s]))"; private final ImmutableMap> subTries; private final ImmutableMap> httpMethodMap; @@ -72,7 +76,12 @@ private PathTrie(Builder builder) { public Result resolve(HttpMethod method, String path) { Preconditions.checkNotNull(method, "method"); Preconditions.checkNotNull(path, "path"); - return resolve(method, getPathSegments(path), 0, new ArrayList()); + Result resolve = resolve(method, getPathSegments(path, "/:", false), 0, new ArrayList<>()); + if (resolve == null) { + //required for backward compatibility of clients not encoding : in path segments as expected + resolve = resolve(method, getPathSegments(path, "/", false), 0, new ArrayList<>()); + } + return resolve; } private Result resolve( @@ -89,7 +98,7 @@ private Result resolve( subTrie = subTries.get(PARAMETER_PATH_SEGMENT); if (subTrie != null) { // TODO: We likely need to enforce non-empty values here. - rawParameters.add(segment); + rawParameters.add(segment.substring(1)); Result result = subTrie.resolve(method, pathSegments, index + 1, rawParameters); if (result == null) { rawParameters.remove(rawParameters.size() - 1); @@ -174,7 +183,7 @@ public Builder add(HttpMethod method, String path, T value) { Preconditions.checkNotNull(path, "path"); Preconditions.checkNotNull(value, "value"); // TODO: We likely want to do something about trailing slashes here (make configurable) - add(method, path, getPathSegments(path).iterator(), value, new ArrayList()); + add(method, path, getPathSegments(path, "/:", true).iterator(), value, new ArrayList<>()); return this; } @@ -186,19 +195,20 @@ private void add(HttpMethod method, String path, Iterator pathSegments, List parameterNames) { if (pathSegments.hasNext()) { String segment = pathSegments.next(); - if (segment.startsWith("{")) { - if (segment.endsWith("}")) { - parameterNames.add(getAndCheckParameterName(segment)); + String segmentWithoutDelimiter = segment.substring(1); + if (segmentWithoutDelimiter.startsWith("{")) { + if (segmentWithoutDelimiter.endsWith("}")) { + parameterNames.add(getAndCheckParameterName(segmentWithoutDelimiter)); getOrCreateSubBuilder(PARAMETER_PATH_SEGMENT) .add(method, path, pathSegments, value, parameterNames); } else { throw new IllegalArgumentException( - String.format("'%s' contains invalid parameter syntax: %s", path, segment)); + String.format("'%s' contains invalid parameter syntax: %s", path, segmentWithoutDelimiter)); } } else { - if (RESERVED_URL_CHARS.matchesAnyOf(segment)) { + if (RESERVED_URL_CHARS.matchesAnyOf(segmentWithoutDelimiter)) { throw new IllegalArgumentException( - String.format("'%s' contains invalid path segment: %s", path, segment)); + String.format("'%s' contains invalid path segment: %s", path, segmentWithoutDelimiter)); } getOrCreateSubBuilder(segment).add(method, path, pathSegments, value, parameterNames); } @@ -234,8 +244,22 @@ private Builder getOrCreateSubBuilder(String segment) { } } - private static List getPathSegments(String path) { - return PATH_SPLITTER.splitToList(path); + private static List getPathSegments(String path, String pathDelimiters, boolean validateCustomMethod) { + if (!path.startsWith("/")) path = "/" + path; + List pathSegments = Arrays.asList(path.split(String.format(SPLITTER_WITH_DELIMITER, pathDelimiters))); + if (validateCustomMethod && pathSegments.size() > 1) { + boolean colonInNonFinalSegment = pathSegments.subList(0, pathSegments.size() - 1) + .stream().anyMatch(segment -> segment.startsWith(":")); + if (colonInNonFinalSegment) { + throw new IllegalArgumentException( + "Custom method syntax with ':' is only authorized at the end of the path"); + } + String last = pathSegments.get(pathSegments.size() - 1); + if (last.startsWith(":{")) { + throw new IllegalArgumentException("Parameterized custom method are not authorized"); + } + } + return pathSegments; } private static String decodeUri(String value) { diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/handlers/ApiProxyHandler.java b/endpoints-framework/src/main/java/com/google/api/server/spi/handlers/ApiProxyHandler.java index d362a129..e7cf2d67 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/handlers/ApiProxyHandler.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/handlers/ApiProxyHandler.java @@ -35,7 +35,7 @@ public void handle(EndpointsContext context) throws IOException { context.getResponse().setContentType("text/html"); // This is a nonstandard value, but it seems sometimes X-Frame-Options can be injected by // a proxy. We set this explicitly in hopes that the proxy won't override a set value. - context.getResponse().addHeader("X-Frame-Options", "ALLOWALL"); + context.getResponse().setHeader("X-Frame-Options", "ALLOWALL"); context.getResponse().getWriter().write(cachedProxyHtml); } } diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/handlers/EndpointsMethodHandler.java b/endpoints-framework/src/main/java/com/google/api/server/spi/handlers/EndpointsMethodHandler.java index 4f7d222d..bb2e05fe 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/handlers/EndpointsMethodHandler.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/handlers/EndpointsMethodHandler.java @@ -15,6 +15,9 @@ */ package com.google.api.server.spi.handlers; +import static org.apache.http.HttpHeaders.CONTENT_TYPE; +import static org.apache.http.HttpHeaders.LOCATION; + import com.google.api.server.spi.EndpointMethod; import com.google.api.server.spi.EndpointsContext; import com.google.api.server.spi.Headers; @@ -30,11 +33,15 @@ import com.google.api.server.spi.request.ParamReader; import com.google.api.server.spi.request.RestServletRequestParamReader; import com.google.api.server.spi.response.InternalServerErrorException; +import com.google.api.server.spi.response.RedirectException; import com.google.api.server.spi.response.RestResponseResultWriter; import com.google.api.server.spi.response.ResultWriter; import com.google.common.annotations.VisibleForTesting; import com.google.common.flogger.FluentLogger; import java.io.IOException; +import java.io.PrintWriter; +import java.net.MalformedURLException; +import java.net.URL; import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -79,9 +86,9 @@ public DispatcherHandler getRestHandler() { @VisibleForTesting protected ParamReader createRestParamReader(EndpointsContext context, - ApiSerializationConfig serializationConfig) { - return new RestServletRequestParamReader(endpointMethod, context, - servletContext, serializationConfig, methodConfig); + ApiSerializationConfig serializationConfig, Object apiService) { + return new RestServletRequestParamReader(apiService, endpointMethod, context, + servletContext, serializationConfig, methodConfig, initParameters); } /** @@ -99,6 +106,49 @@ private void writeError(EndpointsContext context, ServiceException error) throws _createResultWriter(context, null).writeError(error); } + /* + * Commits the response with a status redirect and the location. The location is written in a hyperlink note, + * as advised in https://tools.ietf.org/html/rfc7231#section-6.4. + */ + private void writeRedirect(EndpointsContext context, RedirectException e) throws IOException { + String location = getRedirectLocation(context, e.getLocation()); + HttpServletResponse response = context.getResponse(); + response.setHeader(LOCATION, location); + response.setHeader(CONTENT_TYPE, "text/html"); + response.setStatus(e.getStatusCode()); + String body = "\n" + + "" + + "\n" + + "redirection" + + ""; + PrintWriter out = response.getWriter(); + out.write(body); + out.flush(); + logger.atInfo().log(e.getMessage()); + } + + @VisibleForTesting + static String getRedirectLocation(EndpointsContext context, String redirectDestination) { + String location; + try { + location = new URL(redirectDestination).toString(); + } catch (MalformedURLException e) { + if (redirectDestination.startsWith("/")) { + // relative to the server + location = redirectDestination; + } else { + // relative to the request + String request = context.getRequest().getRequestURI(); + if (request.endsWith("/")) { + location = request + redirectDestination; + } else { + location = request + "/" + redirectDestination; + } + } + } + return location; + } + private ResultWriter _createResultWriter(EndpointsContext context, ApiSerializationConfig serializationConfig) { return new RestResponseResultWriter(context.getResponse(), serializationConfig, @@ -117,14 +167,16 @@ public void handle(EndpointsContext context) throws IOException { Object service = systemService.findService(serviceName); ApiSerializationConfig serializationConfig = systemService.getSerializationConfig( serviceName); - ParamReader reader = createRestParamReader(context, serializationConfig); + ParamReader reader = createRestParamReader(context, serializationConfig, service); ResultWriter writer = createResultWriter(context, serializationConfig); if (request.getHeader(Headers.ORIGIN) != null) { HttpServletResponse response = context.getResponse(); CorsHandler.allowOrigin(request, response); CorsHandler.setAccessControlAllowCredentials(response); } - systemService.invokeServiceMethod(service, endpointMethod.getMethod(), reader, writer); + systemService.invokeServiceMethod(service, endpointMethod.getMethod(), methodConfig.getEffectiveResponseStatus(), reader, writer); + } catch (RedirectException e) { + writeRedirect(context, e); } catch (ServiceException e) { writeError(context, e); } catch (Exception e) { diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/handlers/ExplorerHandler.java b/endpoints-framework/src/main/java/com/google/api/server/spi/handlers/ExplorerHandler.java index e961a908..acc26306 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/handlers/ExplorerHandler.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/handlers/ExplorerHandler.java @@ -16,44 +16,42 @@ package com.google.api.server.spi.handlers; import com.google.api.server.spi.EndpointsContext; +import com.google.api.server.spi.RequestUtil; import com.google.api.server.spi.Strings; import com.google.api.server.spi.dispatcher.DispatcherHandler; import java.io.IOException; +import java.util.Optional; import javax.servlet.http.HttpServletRequest; /** * A handler which sends a redirect to the API Explorer. */ public class ExplorerHandler implements DispatcherHandler { - private static final String EXPLORER_URL = "http://apis-explorer.appspot.com/apis-explorer/"; + + private static final String DEFAULT_TEMPLATE + = "https://apis-explorer.appspot.com/apis-explorer/?base=${apiBase}"; + + private final String urlTemplate; + + public ExplorerHandler(String urlTemplate) { + this.urlTemplate = Optional.ofNullable(urlTemplate).orElse(DEFAULT_TEMPLATE); + } @Override public void handle(EndpointsContext context) throws IOException { - context.getResponse().sendRedirect(getExplorerUrl(context.getRequest(), context.getPath())); + context.getResponse() + .sendRedirect(getExplorerUrl(context.getRequest(), context.getPath())); } private String getExplorerUrl(HttpServletRequest req, String path) { - String url = stripRedundantPorts(Strings.stripTrailingSlash(req.getRequestURL().toString())); + String requestUrl = RequestUtil.getOriginalRequestUrl(req); + requestUrl = Strings.stripTrailingSlash(requestUrl); // This will convert http://localhost:8080/_ah/api/explorer to - // http://apis-explorer.appspot.com/apis-explorer/?base=http://localhost:8080/_ah/api& - // root=http://localhost:8080/_ah/api - // The root parameter is necessary for the non-default module case and the case where the - // host is manually specified. This will override the root, which API explorer now respects - // by default. - String apiRoot = url.substring(0, url.length() - path.length() - 1); - return EXPLORER_URL + "?base=" + apiRoot + "&root=" + apiRoot; - } - - private static String stripRedundantPorts(String url) { - if (url == null) { - return null; - } else if (url.startsWith("http:") && url.contains(":80/")) { - return url.replace(":80/", "/"); - } else if (url.startsWith("https:") && url.contains(":443/")) { - return url.replace(":443/", "/"); - } - return url; + // ${EXPLORER_URL}?base=http://localhost:8080/_ah/api + String apiBase = requestUrl.substring(0, requestUrl.length() - path.length() - 1); + return urlTemplate.replace("${apiBase}", apiBase); } + } diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/request/AbstractParamReader.java b/endpoints-framework/src/main/java/com/google/api/server/spi/request/AbstractParamReader.java index d0ea871d..4f73d03c 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/request/AbstractParamReader.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/request/AbstractParamReader.java @@ -21,12 +21,18 @@ * Implementation of functionality common to all implementations of {@link ParamReader}. */ public abstract class AbstractParamReader implements ParamReader { + private final Object apiService; private final EndpointMethod method; - protected AbstractParamReader(EndpointMethod method) { + protected AbstractParamReader(Object apiService, EndpointMethod method) { + this.apiService = apiService; this.method = method; } + protected Object getApiService() { + return apiService; + } + protected EndpointMethod getMethod() { return method; } diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/request/Attribute.java b/endpoints-framework/src/main/java/com/google/api/server/spi/request/Attribute.java index e56b4965..27c4f1df 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/request/Attribute.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/request/Attribute.java @@ -42,10 +42,6 @@ public class Attribute { */ public static final String ENABLE_CLIENT_ID_WHITELIST = "endpoints:Enable-Client-Id-Whitelist"; - /** - * @deprecated - */ - public static final String RESTRICT_SERVLET = "endpoints:Restrict-Servlet"; /** * A {@link Boolean} indicating if the App Engine user should be populated. */ @@ -106,7 +102,6 @@ public static Attribute bindStandardRequestAttributes(HttpServletRequest request ApiMethodConfig methodConfig, ServletInitializationParameters initParameters) { Attribute attr = Attribute.from(request); - attr.set(Attribute.RESTRICT_SERVLET, initParameters.isServletRestricted()); attr.set(Attribute.ENABLE_CLIENT_ID_WHITELIST, initParameters.isClientIdWhitelistEnabled()); attr.set(Attribute.API_METHOD_CONFIG, methodConfig); // No clientId is allowed. Producer is not interested in Jwt/OAuth2 authentication. diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/request/Auth.java b/endpoints-framework/src/main/java/com/google/api/server/spi/request/Auth.java index a98121e6..b43b064d 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/request/Auth.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/request/Auth.java @@ -15,6 +15,8 @@ */ package com.google.api.server.spi.request; +import static com.google.api.server.spi.EnvUtil.hasForceAuthenticationEnabled; + import com.google.api.server.spi.EnvUtil; import com.google.api.server.spi.ServiceException; import com.google.api.server.spi.auth.EndpointsAuthenticator; @@ -86,7 +88,7 @@ User authenticate() throws ServiceException { * only run once per request. */ com.google.appengine.api.users.User authenticateAppEngineUser() throws ServiceException { - if (!EnvUtil.isRunningOnAppEngine()) { + if (!EnvUtil.isRunningOnAppEngine() && !hasForceAuthenticationEnabled()) { return null; } attr.set(Attribute.REQUIRE_APPENGINE_USER, true); diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/request/RestServletRequestParamReader.java b/endpoints-framework/src/main/java/com/google/api/server/spi/request/RestServletRequestParamReader.java index 02d2e5e6..dc298774 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/request/RestServletRequestParamReader.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/request/RestServletRequestParamReader.java @@ -15,10 +15,12 @@ */ package com.google.api.server.spi.request; +import com.fasterxml.jackson.databind.exc.MismatchedInputException; import com.google.api.server.spi.EndpointMethod; import com.google.api.server.spi.EndpointsContext; import com.google.api.server.spi.IoUtil; import com.google.api.server.spi.ServiceException; +import com.google.api.server.spi.ServletInitializationParameters; import com.google.api.server.spi.Strings; import com.google.api.server.spi.config.model.ApiMethodConfig; import com.google.api.server.spi.config.model.ApiParameterConfig; @@ -43,8 +45,7 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; -import java.util.logging.Level; -import java.util.logging.Logger; +import java.util.Objects; import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; @@ -63,10 +64,10 @@ public class RestServletRequestParamReader extends ServletRequestParamReader { private final Map rawPathParameters; private final Map parameterConfigMap; - public RestServletRequestParamReader(EndpointMethod method, + public RestServletRequestParamReader(Object apiService, EndpointMethod method, EndpointsContext endpointsContext, ServletContext servletContext, - ApiSerializationConfig serializationConfig, ApiMethodConfig methodConfig) { - super(method, endpointsContext, servletContext, serializationConfig, methodConfig); + ApiSerializationConfig serializationConfig, ApiMethodConfig methodConfig, ServletInitializationParameters initializationParameters) { + super(apiService, method, endpointsContext, servletContext, serializationConfig, methodConfig, initializationParameters); this.rawPathParameters = endpointsContext.getRawPathParameters(); ImmutableMap.Builder builder = ImmutableMap.builder(); for (ApiParameterConfig config : methodConfig.getParameterConfigs()) { @@ -87,7 +88,8 @@ public Object[] read() throws ServiceException { return new Object[0]; } HttpServletRequest servletRequest = endpointsContext.getRequest(); - JsonNode node; + ObjectNode body = (ObjectNode) objectReader.createObjectNode(); + ObjectNode params = (ObjectNode) objectReader.createObjectNode(); // multipart/form-data requests can be used for requests which have no resource body. In // this case, each part represents a named parameter instead. if (ServletFileUpload.isMultipartContent(servletRequest)) { @@ -103,7 +105,7 @@ public Object[] read() throws ServiceException { throw new BadRequestException("unable to parse multipart form field"); } } - node = obj; + params = obj; } catch (FileUploadException e) { throw new BadRequestException("unable to parse multipart request", e); } @@ -113,56 +115,69 @@ public Object[] read() throws ServiceException { // Unlike the Lily protocol, which essentially always requires a JSON body to exist (due to // path and query parameters being injected into the body), bodies are optional here, so we // create an empty body and inject named parameters to make deserialize work. - node = Strings.isEmptyOrWhitespace(requestBody) ? objectReader.createObjectNode() - : objectReader.readTree(requestBody); - } - if (!node.isObject()) { - throw new BadRequestException("expected a JSON object body"); + if (!Strings.isEmptyOrWhitespace(requestBody)) { + validateRequestContentType(servletRequest); + JsonNode node = objectReader.readTree(requestBody); + if (!node.isObject()) { + throw new BadRequestException("expected a JSON object body"); + } + body = (ObjectNode) node; + } } - ObjectNode body = (ObjectNode) node; Map> parameterMap = getParameterMap(method); // First add query parameters, then add path parameters. If the parameters already exist in // the resource, then the they aren't added to the body object. For compatibility reasons, // the order of precedence is resource field > query parameter > path parameter. for (Enumeration e = servletRequest.getParameterNames(); e.hasMoreElements(); ) { String parameterName = (String) e.nextElement(); - if (!body.has(parameterName)) { - Class parameterClass = parameterMap.get(parameterName); - ApiParameterConfig parameterConfig = parameterConfigMap.get(parameterName); - if (parameterClass != null && parameterConfig.isRepeated()) { - ArrayNode values = body.putArray(parameterName); - for (String value : servletRequest.getParameterValues(parameterName)) { - values.add(value); - } - } else { - body.put(parameterName, servletRequest.getParameterValues(parameterName)[0]); + Class parameterClass = parameterMap.get(parameterName); + ApiParameterConfig parameterConfig = parameterConfigMap.get(parameterName); + if (parameterClass != null && parameterConfig.isRepeated()) { + ArrayNode values = params.putArray(parameterName); + for (String value : servletRequest.getParameterValues(parameterName)) { + values.add(value); } + } else { + params.put(parameterName, servletRequest.getParameterValues(parameterName)[0]); } } for (Entry entry : rawPathParameters.entrySet()) { String parameterName = entry.getKey(); Class parameterClass = parameterMap.get(parameterName); - if (parameterClass != null && !body.has(parameterName)) { + if (parameterClass != null && !params.has(parameterName)) { if (parameterConfigMap.get(parameterName).isRepeated()) { - ArrayNode values = body.putArray(parameterName); + ArrayNode values = params.putArray(parameterName); for (String value : COMPOSITE_PATH_SPLITTER.split(entry.getValue())) { values.add(value); } } else { - body.put(parameterName, entry.getValue()); + params.put(parameterName, entry.getValue()); } } } for (Entry entry : parameterConfigMap.entrySet()) { - if (!body.has(entry.getKey()) && entry.getValue().getDefaultValue() != null) { - body.put(entry.getKey(), entry.getValue().getDefaultValue()); + if (!params.has(entry.getKey()) && entry.getValue().getDefaultValue() != null) { + params.put(entry.getKey(), entry.getValue().getDefaultValue()); } } - return deserializeParams(body); + return validateParameters(deserializeParams(body, params)); + } catch (MismatchedInputException e) { + logger.atInfo().withCause(e).log("Unable to read request parameter(s)"); + throw translateJsonException(e); } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | IOException e) { logger.atInfo().withCause(e).log("Unable to read request parameter(s)"); - throw new BadRequestException(e); + throw new BadRequestException("Parse error", "parseError", e); + } + } + + private void validateRequestContentType(HttpServletRequest httpServletRequest) throws ServiceException { + if (!initParameters.isContentTypeValidationEnabled()) { + return; + } + String contentType = httpServletRequest.getContentType(); + if (Objects.isNull(contentType) || !contentType.startsWith("application/json")) { + throw new ServiceException(406, "Expecting application/json content-type."); } } diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/request/ServletRequestParamReader.java b/endpoints-framework/src/main/java/com/google/api/server/spi/request/ServletRequestParamReader.java index 69ff31d4..94a962a3 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/request/ServletRequestParamReader.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/request/ServletRequestParamReader.java @@ -20,6 +20,7 @@ import com.google.api.server.spi.EndpointsContext; import com.google.api.server.spi.IoUtil; import com.google.api.server.spi.ServiceException; +import com.google.api.server.spi.ServletInitializationParameters; import com.google.api.server.spi.auth.common.User; import com.google.api.server.spi.config.AuthLevel; import com.google.api.server.spi.config.Named; @@ -35,16 +36,28 @@ import com.google.appengine.api.datastore.Blob; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; +import com.google.common.collect.Streams; import com.google.common.flogger.FluentLogger; import com.google.common.reflect.TypeToken; +import com.fasterxml.jackson.core.Base64Variants; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.Version; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException.Reference; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectReader; +import com.fasterxml.jackson.databind.exc.InvalidFormatException; +import com.fasterxml.jackson.databind.exc.MismatchedInputException; import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.node.ObjectNode; +import javax.servlet.ServletContext; +import javax.servlet.http.HttpServletRequest; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Path; +import jakarta.validation.Validation; +import jakarta.validation.Validator; import java.io.IOException; import java.lang.annotation.Annotation; @@ -52,6 +65,7 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; +import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -60,13 +74,11 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Set; -import java.util.logging.Level; -import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; -import javax.servlet.ServletContext; -import javax.servlet.http.HttpServletRequest; +import org.hibernate.validator.messageinterpolation.ParameterMessageInterpolator; /** * Reads parameters from an {@link HttpServletRequest}. @@ -76,6 +88,11 @@ public class ServletRequestParamReader extends AbstractParamReader { private static final FluentLogger logger = FluentLogger.forEnclosingClass(); private static final Set READER_MODULES; private static final String APPENGINE_USER_CLASS_NAME = "com.google.appengine.api.users.User"; + private static final Validator VALIDATOR = Validation.byDefaultProvider() + .configure() + .messageInterpolator(new ParameterMessageInterpolator()) + .buildValidatorFactory() + .getValidator(); static { Set modules = new LinkedHashSet<>(); @@ -128,7 +145,7 @@ protected static List getParameterNames(EndpointMethod endpointMethod) return parameterNames; } - protected Object[] deserializeParams(JsonNode node) throws IOException, IllegalAccessException, + protected Object[] deserializeParams(JsonNode body, JsonNode parameters) throws IOException, IllegalAccessException, InvocationTargetException, NoSuchMethodException, ServiceException { EndpointMethod method = getMethod(); Class[] paramClasses = method.getParameterClasses(); @@ -174,13 +191,13 @@ protected Object[] deserializeParams(JsonNode node) throws IOException, IllegalA } else { String name = parameterNames.get(i); if (Strings.isNullOrEmpty(name)) { - params[i] = (node == null) ? null : objectReader.forType(clazz).readValue(node); + params[i] = (body == null) ? null : objectReader.forType(clazz).readValue(body); logger.atFine().log("deserialize: %s %s injected into unnamed param[%d]", clazz, params[i], i); } else if (StandardParameters.isStandardParamName(name)) { - params[i] = getStandardParamValue(node, name); + params[i] = getStandardParamValue(parameters, name); } else { - JsonNode nodeValue = node.get(name); + JsonNode nodeValue = parameters.get(name); if (nodeValue == null) { params[i] = null; } else { @@ -256,9 +273,13 @@ private static class DateDeserializer extends JsonDeserializer { @Override public Date deserialize(JsonParser jsonParser, DeserializationContext context) throws IOException { - com.google.api.client.util.DateTime date = - new com.google.api.client.util.DateTime(jsonParser.readValueAs(String.class)); - return new Date(date.getValue()); + String value = jsonParser.readValueAs(String.class); + try { + com.google.api.client.util.DateTime date = new com.google.api.client.util.DateTime(value); + return new Date(date.getValue()); + } catch (NumberFormatException e) { + throw InvalidFormatException.from(jsonParser, e.getMessage(), value, Date.class); + } } } @@ -266,7 +287,12 @@ private static class DateAndTimeDeserializer extends JsonDeserializer { @@ -302,14 +338,16 @@ public Blob deserialize(JsonParser jsonParser, DeserializationContext context) private final ServletContext servletContext; protected final ObjectReader objectReader; protected final ApiMethodConfig methodConfig; + protected final ServletInitializationParameters initParameters; public ServletRequestParamReader( - EndpointMethod method, + Object apiService, EndpointMethod method, EndpointsContext endpointsContext, ServletContext servletContext, ApiSerializationConfig serializationConfig, - ApiMethodConfig methodConfig) { - super(method); + ApiMethodConfig methodConfig, + ServletInitializationParameters initParameters) { + super(apiService, method); this.methodConfig = methodConfig; this.endpointsContext = endpointsContext; @@ -322,7 +360,9 @@ public ServletRequestParamReader( .apiSerializationConfig(serializationConfig) .addRegisteredModules(modules) .build() - .reader(); + .reader() + .with(Base64Variants.MIME_NO_LINEFEEDS); + this.initParameters = initParameters; } @Override @@ -336,10 +376,111 @@ public Object[] read() throws ServiceException { return new Object[0]; } JsonNode node = objectReader.readTree(requestBody); - return deserializeParams(node); + if (!node.isObject()) { + throw new BadRequestException("expected a JSON object body"); + } + //this convention comes from gapi.client to separate params and body + JsonNode resource = node.get("resource"); + ((ObjectNode) node).remove("resource"); + return validateParameters(deserializeParams(resource, node)); + } catch (MismatchedInputException e) { + logger.atInfo().withCause(e).log("Unable to read request parameter(s)"); + throw translateJsonException(e); } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | IOException e) { throw new BadRequestException(e); } } + + BadRequestException translateJsonException(MismatchedInputException e) { + String reason = "parseError"; + + //resolve path + List path = e.getPath(); + String location; + if (path.isEmpty()) { + //query / path parameter name can't be retrieved from the error + location = "a parameter"; + } else { + String fieldPath = path.stream() + .map(reference -> reference.getIndex() != -1 + ? "[" + reference.getIndex() + "]" + : "." + reference.getFieldName()) + .collect(Collectors.joining()); + location = "field '" + fieldPath.substring(1) + "'"; + } + + //resolve type + Class targetType = e.getTargetType(); + boolean isArrayElement = targetType.isArray() && !path.isEmpty() + && path.get(path.size() - 1).getIndex() != -1; + if (isArrayElement) { + targetType = targetType.getComponentType(); + } + String type = " of type '" + targetType.getSimpleName() + "'"; + + //add details if possible + String messagePattern = ": invalid {0} value \"{1}\".{2}"; + String details = ""; + if (e instanceof InvalidFormatException) { + Object value = ((InvalidFormatException) e).getValue(); + if (targetType.isEnum()) { + details = (MessageFormat.format(messagePattern, "enum", + value, + " Valid values are " + Arrays.toString(targetType.getEnumConstants())) + ); + } else if (isNumber(targetType)) { + details = MessageFormat.format(messagePattern, "number", value, ""); + } else if (isBoolean(targetType)) { + details = MessageFormat.format(messagePattern,"boolean", value, " Valid values are [true, false]"); + } else if (isDate(targetType)) { + details = MessageFormat.format(messagePattern, "date", value, ""); + } + } + + return new BadRequestException("Parse error for " + location + type + details, reason, e); + } + + private boolean isBoolean(Class clazz) { + return Boolean.class.equals(clazz) || boolean.class.equals(clazz); + } + + private boolean isDate(Class clazz) { + return Date.class.isAssignableFrom(clazz) + || DateAndTime.class.equals(clazz) + || SimpleDate.class.equals(clazz); + } + + private boolean isNumber(Class clazz) { + return Number.class.isAssignableFrom(clazz) + || byte.class.equals(clazz) + || int.class.equals(clazz) + || long.class.equals(clazz) + || float.class.equals(clazz) + || double.class.equals(clazz); + } + + protected Object[] validateParameters(Object[] parameterValues) throws BadRequestException { + if (initParameters.isParameterValidationEnabled()) { + Set> constraintViolations = VALIDATOR.forExecutables().validateParameters(getApiService(), getMethod().getMethod(), parameterValues); + if (!constraintViolations.isEmpty()) { + String errors = constraintViolations.stream() + .map(violation -> sanitizedPath(violation) + " " + violation.getMessage()) + .collect(Collectors.joining(", ")); + throw new BadRequestException("Invalid parameters: " + errors); + } + } + return parameterValues; + } + + /** + * Skips useless and misleading method name (java name instead of API method name) + */ + private static String sanitizedPath(ConstraintViolation violation) { + String path = Streams.stream(violation.getPropertyPath()) + .map(Path.Node::getName) + .skip(1) + .collect(Collectors.joining(".")); + return path; + } } diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/response/BadRequestException.java b/endpoints-framework/src/main/java/com/google/api/server/spi/response/BadRequestException.java index 378826cb..3686a4ee 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/response/BadRequestException.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/response/BadRequestException.java @@ -22,7 +22,7 @@ */ public class BadRequestException extends ServiceException { - private static final int CODE = 400; + public static final int CODE = 400; public BadRequestException(String message) { super(CODE, message); diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/response/ConflictException.java b/endpoints-framework/src/main/java/com/google/api/server/spi/response/ConflictException.java index e85523bf..e3722f7c 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/response/ConflictException.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/response/ConflictException.java @@ -22,7 +22,7 @@ */ public class ConflictException extends ServiceException { - private static final int CODE = 409; + public static final int CODE = 409; public ConflictException(String message) { super(CODE, message); diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/response/ForbiddenException.java b/endpoints-framework/src/main/java/com/google/api/server/spi/response/ForbiddenException.java index 1ed9e4e4..db0b7df1 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/response/ForbiddenException.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/response/ForbiddenException.java @@ -22,7 +22,7 @@ */ public class ForbiddenException extends ServiceException { - private static final int CODE = 403; + public static final int CODE = 403; public ForbiddenException(String message) { super(CODE, message); diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/response/FoundRedirectException.java b/endpoints-framework/src/main/java/com/google/api/server/spi/response/FoundRedirectException.java new file mode 100644 index 00000000..9e2ce6a0 --- /dev/null +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/response/FoundRedirectException.java @@ -0,0 +1,13 @@ +package com.google.api.server.spi.response; + +/** + * Found exception that is mapped to a 302 response and a redirection to the given location. + */ +public class FoundRedirectException extends RedirectException { + + public static final int CODE = 302; + + public FoundRedirectException(String statusMessage, String location) { + super(CODE, statusMessage, location); + } +} diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/response/InternalServerErrorException.java b/endpoints-framework/src/main/java/com/google/api/server/spi/response/InternalServerErrorException.java index 12ba1ac2..27cb065f 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/response/InternalServerErrorException.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/response/InternalServerErrorException.java @@ -22,7 +22,7 @@ */ public class InternalServerErrorException extends ServiceException { - private static final int CODE = 500; + public static final int CODE = 500; public InternalServerErrorException(String message) { super(CODE, message); diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/response/NotFoundException.java b/endpoints-framework/src/main/java/com/google/api/server/spi/response/NotFoundException.java index e620bd7d..b7717b61 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/response/NotFoundException.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/response/NotFoundException.java @@ -22,7 +22,7 @@ */ public class NotFoundException extends ServiceException { - private static final int CODE = 404; + public static final int CODE = 404; public NotFoundException(String message) { super(CODE, message); diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/response/RedirectException.java b/endpoints-framework/src/main/java/com/google/api/server/spi/response/RedirectException.java new file mode 100644 index 00000000..d25b157c --- /dev/null +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/response/RedirectException.java @@ -0,0 +1,33 @@ +package com.google.api.server.spi.response; + +import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; +import static javax.servlet.http.HttpServletResponse.SC_MULTIPLE_CHOICES; + +import com.google.api.server.spi.ServiceException; +import com.google.common.base.Preconditions; + +/** + * Exception to be thrown by endpoint methods for returning a redirect response + * instead of the declared response payload. + * The status must be a 3xx redirect code. + */ +public class RedirectException extends ServiceException { + + private final String location; + + public RedirectException(int statusCode, String statusMessage, String location) { + super(validateStatusCode(statusCode), statusMessage); + this.location = Preconditions.checkNotNull(location); + } + + public String getLocation() { + return location; + } + + private static int validateStatusCode(int statusCode) { + if (statusCode < SC_MULTIPLE_CHOICES || statusCode >= SC_BAD_REQUEST) { + throw new IllegalArgumentException("Not a 3xx redirect code: " + statusCode); + } + return statusCode; + } +} diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/response/RestResponseResultWriter.java b/endpoints-framework/src/main/java/com/google/api/server/spi/response/RestResponseResultWriter.java index 5fb1732f..4ddd0f90 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/response/RestResponseResultWriter.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/response/RestResponseResultWriter.java @@ -23,6 +23,7 @@ import com.google.common.base.Strings; import java.io.IOException; +import java.util.Map; import javax.servlet.http.HttpServletResponse; @@ -67,16 +68,19 @@ public void writeError(ServiceException e) throws IOException { e.getReason() : errorMap.getReason(e.getStatusCode()); String domain = !Strings.isNullOrEmpty(e.getDomain()) ? e.getDomain() : errorMap.getDomain(e.getStatusCode()); - write(code, e.getHeaders(), createError(code, reason, domain, e.getMessage())); + write(code, e.getHeaders(), createError(code, reason, domain, e.getMessage(), e.getExtraFields()), true); } - private Object createError(int code, String reason, String domain, String message) { + private Object createError(int code, String reason, String domain, String message, Map extraFields) { ObjectNode topLevel = objectMapper.createObjectNode(); ObjectNode topError = objectMapper.createObjectNode(); ObjectNode error = objectMapper.createObjectNode(); error.put("domain", domain); error.put("reason", reason); error.put("message", message); + for (Map.Entry extraField : extraFields.entrySet()) { + error.putPOJO(extraField.getKey(), extraField.getValue()); + } topError.set("errors", objectMapper.createArrayNode().add(error)); topError.put("code", code); topError.put("message", message); diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/response/ResultWriter.java b/endpoints-framework/src/main/java/com/google/api/server/spi/response/ResultWriter.java index 1aed75b0..3b556465 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/response/ResultWriter.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/response/ResultWriter.java @@ -25,10 +25,10 @@ public interface ResultWriter { /** - * Writes a result JSON object. + * Writes a result JSON object, with specified status code. * @throws IOException */ - void write(Object result) throws IOException; + void write(Object result, int status) throws IOException; /** * Writes an error response with the HTTP status code and JSON body of {"message": diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/response/SeeOtherRedirectException.java b/endpoints-framework/src/main/java/com/google/api/server/spi/response/SeeOtherRedirectException.java new file mode 100644 index 00000000..5450fc61 --- /dev/null +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/response/SeeOtherRedirectException.java @@ -0,0 +1,13 @@ +package com.google.api.server.spi.response; + +/** + * See other exception that is mapped to a 303 response and a redirection to the given location. + */ +public class SeeOtherRedirectException extends RedirectException { + + public static final int CODE = 303; + + public SeeOtherRedirectException(String statusMessage, String location) { + super(CODE, statusMessage, location); + } +} diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/response/ServiceUnavailableException.java b/endpoints-framework/src/main/java/com/google/api/server/spi/response/ServiceUnavailableException.java index ed6decf9..dd7e19b3 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/response/ServiceUnavailableException.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/response/ServiceUnavailableException.java @@ -22,7 +22,7 @@ */ public class ServiceUnavailableException extends ServiceException { - private static final int CODE = 503; + public static final int CODE = 503; public ServiceUnavailableException(String message) { super(CODE, message); diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/response/ServletResponseResultWriter.java b/endpoints-framework/src/main/java/com/google/api/server/spi/response/ServletResponseResultWriter.java index 783a7256..29232609 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/response/ServletResponseResultWriter.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/response/ServletResponseResultWriter.java @@ -15,6 +15,8 @@ */ package com.google.api.server.spi.response; +import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT; + import com.google.api.server.spi.ConfiguredObjectMapper; import com.google.api.server.spi.Constant; import com.google.api.server.spi.ServiceException; @@ -23,6 +25,7 @@ import com.google.api.server.spi.types.DateAndTime; import com.google.api.server.spi.types.SimpleDate; import com.google.appengine.api.datastore.Blob; +import com.google.common.annotations.VisibleForTesting; import com.google.common.io.ByteStreams; import com.google.common.io.CountingOutputStream; @@ -39,6 +42,7 @@ import java.util.HashMap; import java.util.LinkedHashSet; import java.util.Map; +import java.util.OptionalLong; import java.util.Set; import javax.servlet.http.HttpServletResponse; @@ -48,7 +52,8 @@ */ public class ServletResponseResultWriter implements ResultWriter { - private static final Set WRITER_MODULES; + @VisibleForTesting + protected static final Set WRITER_MODULES; static { Set modules = new LinkedHashSet<>(); @@ -69,29 +74,27 @@ public class ServletResponseResultWriter implements ResultWriter { private final HttpServletResponse servletResponse; private final ObjectWriter objectWriter; + private final ObjectWriter errorObjectWriter; private final boolean addContentLength; public ServletResponseResultWriter( - HttpServletResponse servletResponse, ApiSerializationConfig serializationConfig) { - this(servletResponse, serializationConfig, false /* prettyPrint */, false /* addContentLength */); + HttpServletResponse servletResponse, ApiSerializationConfig serializationConfig, + boolean prettyPrint, boolean addContentLength) { + this(servletResponse, ConfiguredObjectMapper.builder() + .apiSerializationConfig(serializationConfig) + .addRegisteredModules(WRITER_MODULES) + .build().writer(), prettyPrint, addContentLength); } public ServletResponseResultWriter( - HttpServletResponse servletResponse, ApiSerializationConfig serializationConfig, + HttpServletResponse servletResponse, ObjectWriter objectWriter, boolean prettyPrint, boolean addContentLength) { this.servletResponse = servletResponse; - Set modules = new LinkedHashSet<>(); - modules.addAll(WRITER_MODULES); - ObjectWriter objectWriter = ConfiguredObjectMapper.builder() - .apiSerializationConfig(serializationConfig) - .addRegisteredModules(modules) - .build() - .writer(); - if (prettyPrint) { objectWriter = objectWriter.with(new EndpointsPrettyPrinter()); } this.objectWriter = configureWriter(objectWriter); + this.errorObjectWriter = objectWriter; this.addContentLength = addContentLength; } @@ -106,22 +109,20 @@ protected ObjectWriter configureWriter(ObjectWriter objectWriter) { } @Override - public void write(Object response) throws IOException { - if (response == null) { - write(HttpServletResponse.SC_NO_CONTENT, null, null); - } else { - write(HttpServletResponse.SC_OK, null, ResponseUtil.wrapCollection(response)); - } + public void write(Object response, int status) throws IOException { + int finalStatus = response == null ? SC_NO_CONTENT : status; + write(finalStatus, null, ResponseUtil.wrapCollection(response), false); } @Override public void writeError(ServiceException e) throws IOException { Map errors = new HashMap<>(); errors.put(Constant.ERROR_MESSAGE, e.getMessage()); - write(e.getStatusCode(), e.getHeaders(), errors); + write(e.getStatusCode(), e.getHeaders(), errors, true); } - protected void write(int status, Map headers, Object content) throws IOException { + protected void write(int status, Map headers, Object content, boolean isError) throws IOException { + // write response status code servletResponse.setStatus(status); @@ -133,14 +134,15 @@ protected void write(int status, Map headers, Object content) th } // write response body + ObjectWriter writer = isError ? errorObjectWriter: objectWriter; if (content != null) { servletResponse.setContentType(SystemService.MIME_JSON); if (addContentLength) { CountingOutputStream counter = new CountingOutputStream(ByteStreams.nullOutputStream()); - objectWriter.writeValue(counter, content); + writer.writeValue(counter, content); servletResponse.setContentLength((int) counter.getCount()); } - objectWriter.writeValue(servletResponse.getOutputStream(), content); + writer.writeValue(servletResponse.getOutputStream(), content); } } @@ -152,10 +154,18 @@ public void serialize(Long value, JsonGenerator jgen, SerializerProvider provide jgen.writeString(value.toString()); } }; + JsonSerializer optionalLongJsonSerializer = new JsonSerializer() { + @Override + public void serialize(OptionalLong value, JsonGenerator jgen, SerializerProvider provider) + throws IOException { + jgen.writeString(value.isPresent() ? String.valueOf(value.getAsLong()) : null); + } + }; SimpleModule writeLongAsStringModule = new SimpleModule("writeLongAsStringModule", new Version(1, 0, 0, null, null, null)); writeLongAsStringModule.addSerializer(Long.TYPE, longSerializer); // long (primitive) writeLongAsStringModule.addSerializer(Long.class, longSerializer); // Long (class) + writeLongAsStringModule.addSerializer(OptionalLong.class, optionalLongJsonSerializer); // Long (class) return writeLongAsStringModule; } diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/response/UnauthorizedException.java b/endpoints-framework/src/main/java/com/google/api/server/spi/response/UnauthorizedException.java index 97e4c82e..4016c014 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/response/UnauthorizedException.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/response/UnauthorizedException.java @@ -29,7 +29,7 @@ public class UnauthorizedException extends ServiceException { public static final String AUTH_SCHEME_BEARER = "Bearer"; private static final Map GOOGLE_REALM = ImmutableMap.of("realm", "\"https://accounts.google.com/\""); - private static final int CODE = 401; + public static final int CODE = 401; private final String authScheme; private final Map params; diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/swagger/SwaggerGenerator.java b/endpoints-framework/src/main/java/com/google/api/server/spi/swagger/SwaggerGenerator.java index 948fdba4..4b7f7e5d 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/swagger/SwaggerGenerator.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/swagger/SwaggerGenerator.java @@ -15,6 +15,8 @@ */ package com.google.api.server.spi.swagger; +import com.google.api.client.googleapis.json.GoogleJsonError; +import com.google.api.client.googleapis.json.GoogleJsonErrorContainer; import com.google.api.server.spi.EndpointMethod; import com.google.api.server.spi.Strings; import com.google.api.server.spi.TypeLoader; @@ -27,12 +29,16 @@ import com.google.api.server.spi.config.model.ApiKey; import com.google.api.server.spi.config.model.ApiLimitMetricConfig; import com.google.api.server.spi.config.model.ApiMethodConfig; +import com.google.api.server.spi.config.model.ApiMethodConfig.ErrorResponse; import com.google.api.server.spi.config.model.ApiMetricCostConfig; import com.google.api.server.spi.config.model.ApiParameterConfig; +import com.google.api.server.spi.config.model.AuthScopeRepository; import com.google.api.server.spi.config.model.FieldType; import com.google.api.server.spi.config.model.Schema; import com.google.api.server.spi.config.model.Schema.Field; +import com.google.api.server.spi.config.model.Schema.SchemaReference; import com.google.api.server.spi.config.model.SchemaRepository; +import com.google.api.server.spi.config.model.Types; import com.google.api.server.spi.config.validation.ApiConfigValidator; import com.google.api.server.spi.types.DateAndTime; import com.google.api.server.spi.types.SimpleDate; @@ -41,55 +47,77 @@ import com.google.common.base.Function; import com.google.common.base.Joiner; import com.google.common.collect.FluentIterable; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.HashMultiset; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableMap.Builder; import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Lists; import com.google.common.collect.Maps; +import com.google.common.collect.Multimap; +import com.google.common.collect.Multiset; +import com.google.common.collect.Multisets; +import com.google.common.net.UrlEscapers; import com.google.common.reflect.TypeToken; - -import java.lang.reflect.Type; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Date; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.TreeMap; - +import io.swagger.models.ExternalDocs; import io.swagger.models.Info; import io.swagger.models.Model; import io.swagger.models.ModelImpl; import io.swagger.models.Operation; import io.swagger.models.Path; import io.swagger.models.RefModel; +import io.swagger.models.RefResponse; import io.swagger.models.Response; import io.swagger.models.Scheme; import io.swagger.models.Swagger; +import io.swagger.models.Tag; import io.swagger.models.auth.ApiKeyAuthDefinition; import io.swagger.models.auth.In; import io.swagger.models.auth.OAuth2Definition; import io.swagger.models.auth.SecuritySchemeDefinition; +import io.swagger.models.parameters.AbstractSerializableParameter; import io.swagger.models.parameters.BodyParameter; +import io.swagger.models.parameters.Parameter; import io.swagger.models.parameters.PathParameter; import io.swagger.models.parameters.QueryParameter; -import io.swagger.models.parameters.SerializableParameter; +import io.swagger.models.parameters.RefParameter; +import io.swagger.models.properties.AbstractNumericProperty; import io.swagger.models.properties.ArrayProperty; +import io.swagger.models.properties.BaseIntegerProperty; import io.swagger.models.properties.BooleanProperty; import io.swagger.models.properties.ByteArrayProperty; import io.swagger.models.properties.DateProperty; import io.swagger.models.properties.DateTimeProperty; +import io.swagger.models.properties.DecimalProperty; import io.swagger.models.properties.DoubleProperty; import io.swagger.models.properties.FloatProperty; import io.swagger.models.properties.IntegerProperty; import io.swagger.models.properties.LongProperty; +import io.swagger.models.properties.MapProperty; +import io.swagger.models.properties.ObjectProperty; import io.swagger.models.properties.Property; +import io.swagger.models.properties.PropertyBuilder; import io.swagger.models.properties.RefProperty; import io.swagger.models.properties.StringProperty; +import io.swagger.models.refs.RefType; +import java.lang.reflect.Type; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Optional; +import java.util.TreeMap; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.commons.lang3.text.StrSubstitutor; /** * Generates a {@link Swagger} object representing a set of {@link ApiConfig} objects. @@ -113,10 +141,7 @@ public class SwaggerGenerator { private static final String METRIC_KIND = "GAUGE"; private static final String METRICS_KEY = "metrics"; private static final String QUOTA_KEY = "quota"; - - private static final Converter CONVERTER = - CaseFormat.LOWER_CAMEL.converterTo(CaseFormat.UPPER_CAMEL); - private static final Joiner JOINER = Joiner.on("").skipNulls(); + private static final ImmutableMap TYPE_TO_STRING_MAP = ImmutableMap.builder() .put(String.class, "string") @@ -164,23 +189,25 @@ public class SwaggerGenerator { .put(FieldType.INT64, LongProperty.class) .put(FieldType.STRING, StringProperty.class) .build(); - + //expected "additionalProperties: true" for free-from objects is not possible with Java API + //using an object property with empty properties is semantically identical + private static final ObjectProperty FREE_FORM_PROPERTY = new ObjectProperty() + .properties(Collections.emptyMap()); + //some well-known types should be inlined to avoid polluting model namespace + private static final ImmutableSet INLINED_MODEL_NAMES = ImmutableSet.of( + GoogleJsonError.class.getSimpleName(), GoogleJsonError.ErrorInfo.class.getSimpleName() + ); + private static final Function CONFIG_TO_ROOTLESS_KEY = - new Function() { - @Override - public ApiKey apply(ApiConfig config) { - return new ApiKey(config.getName(), config.getVersion(), null /* root */); - } - }; + config -> new ApiKey(config.getName(), config.getVersion(), null /* root */); - public Swagger writeSwagger(Iterable configs, boolean writeInternal, - SwaggerContext context) throws ApiConfigException { + public Swagger writeSwagger(Iterable configs, SwaggerContext context) + throws ApiConfigException { try { TypeLoader typeLoader = new TypeLoader(SwaggerGenerator.class.getClassLoader()); SchemaRepository repo = new SchemaRepository(typeLoader); GenerationContext genCtx = new GenerationContext(); genCtx.validator = new ApiConfigValidator(typeLoader, repo); - genCtx.writeInternal = writeInternal; genCtx.schemata = new SchemaRepository(typeLoader); return writeSwagger(configs, context, genCtx); } catch (ClassNotFoundException e) { @@ -200,35 +227,166 @@ private Swagger writeSwagger(Iterable configs, SwaggerContext context .host(context.hostname) .basePath(context.basePath) .info(new Info() - .title(context.hostname) - .version(context.docVersion)); + .title(context.title != null ? context.title : context.hostname) + .description(context.description) + .version(context.docVersion) + //TODO contact, license, termsOfService could be configured + ); + if (!Strings.isEmptyOrWhitespace(context.apiName)) { + swagger.vendorExtension("x-google-api-name", context.apiName); + } for (ApiKey apiKey : configsByKey.keySet()) { - writeApi(apiKey, configsByKey.get(apiKey), swagger, genCtx); + writeApi(apiKey, configsByKey.get(apiKey), swagger, context, genCtx); } + checkEquivalentPaths(swagger); + combineCommonParameters(swagger, context); + //TODO could also combine common responses + normalizeOperationParameters(swagger); writeQuotaDefinitions(swagger, genCtx); return swagger; } + /* + A generated spec might have "equivalent" paths like this: + - POST /myapi/v1/foo/{id} + - GET /myapi/v1/foo/{fooId} + This is valid for the Discovery format, but won't work on Swagger. + */ + private void checkEquivalentPaths(Swagger swagger) { + List>> duplicatePaths = swagger.getPaths().keySet().stream() + .collect(Collectors.groupingBy(path -> path.replaceAll("\\{[^}]+}", "{%}"))) + .entrySet().stream() + .filter(entry -> entry.getValue().size() > 1) + .collect(Collectors.toList()); + if (!duplicatePaths.isEmpty()) { + throw new IllegalStateException("Equivalent paths found:" + duplicatePaths.stream() + .map(entry -> String.format("\n%s -> %s", entry.getKey(), entry.getValue())) + .collect(Collectors.joining())); + } + } + + /* + * Swagger library will set parameters to empty by default. We force them to be null. + * If not empty, makes sure the body is always last. + */ + public static void normalizeOperationParameters(Swagger swagger) { + swagger.getPaths().values().stream() + .flatMap(path -> path.getOperations().stream()) + .forEach(operation -> { + List parameters = operation.getParameters(); + if (parameters != null && parameters.isEmpty()) { + operation.setParameters(null); + } + }); + } + + private void combineCommonParameters(Swagger swagger, SwaggerContext context) { + if (!context.extractCommonParametersAsRefs && !context.combineCommonParametersInSamePath) { + return; + } + + Map> paramNameCounter = new LinkedHashMap<>(); + Multimap specLevelParameters = HashMultimap.create(); + Map> pathLevelParameters = Maps.newHashMap(); + + //collect parameters on all operations + swagger.getPaths().values().forEach(path -> { + Multimap parameters = HashMultimap.create(); + path.getOperations().forEach(operation -> { + operation.getParameters().forEach(parameter -> { + Multiset counter = Optional + .ofNullable(paramNameCounter.get(getRefName(parameter))) + .orElse(HashMultiset.create()); + counter.add(parameter); + paramNameCounter.put(getRefName(parameter), counter); + specLevelParameters.put(parameter, path); + parameters.put(parameter, operation); + }); + pathLevelParameters.put(path, parameters); + }); + }); + + if (context.extractCommonParametersAsRefs) { + //combine common spec-level params (only if more than one path) + specLevelParameters.asMap().forEach((parameter, paths) -> { + //parameters used in more than one path are replaced + if (paths.size() > 1) { + //if multiple params are named the same, only replace the one with the most occurrences + //TODO add param name suffix depending on "in" and "required" values to deduplicate + Multiset paramCounter = Multisets + .copyHighestCountFirst(paramNameCounter.get(getRefName(parameter))); + if (paramCounter.iterator().next().equals(parameter)) { + addGlobalParameter(swagger, parameter); + swagger.getPaths().values().forEach(path -> path.getOperations() + .forEach(operation -> replaceParameterByRef(operation.getParameters(), parameter) + )); + pathLevelParameters.values().forEach(pathParameters -> pathParameters.removeAll(parameter)); + } + } + }); + } + + //combine remaining common path-level params + pathLevelParameters.forEach((path, parameterMap) -> { + parameterMap.asMap().forEach((parameter, operations) -> { + //if parameter is used in all operations on this path, move it to path level + boolean combined = false; + if (context.combineCommonParametersInSamePath + && operations.size() == path.getOperations().size()) { + path.addParameter(parameter); + operations.forEach(operation -> operation.getParameters().remove(parameter)); + combined = true; + } + //if parameter is more than once in this path but was not extracted before, extract as ref + if (context.extractCommonParametersAsRefs && operations.size() > 1 && !combined) { + addGlobalParameter(swagger, parameter); + operations.forEach(operation -> + replaceParameterByRef(operation.getParameters(), parameter)); + } + }); + }); + } + + private void addGlobalParameter(Swagger swagger, Parameter parameter) { + if (swagger.getParameters() == null) { + swagger.setParameters(new TreeMap<>()); + } + swagger.addParameter(getRefName(parameter), parameter); + } + + private void replaceParameterByRef(List opParameters, Parameter parameter) { + int index = opParameters.indexOf(parameter); + if (index != -1) { + opParameters.add(index, + new RefParameter(getFullRef(RefType.PARAMETER, getRefName(parameter)))); + opParameters.remove(parameter); + } + } + + private String getRefName(Parameter parameter) { + String suffix = "_" + parameter.getIn() + "_parameter"; + return parameter.getName() + suffix; + } + private void writeQuotaDefinitions(Swagger swagger, GenerationContext genCtx) { if (!genCtx.limitMetrics.isEmpty()) { Map>> quotaDefinitions = new HashMap<>(); List> limits = new ArrayList<>(); List> metrics = new ArrayList<>(); for (ApiLimitMetricConfig limitMetric : genCtx.limitMetrics.values()) { - metrics.add(ImmutableMap.builder() + Builder builder = ImmutableMap.builder() .put(METRIC_NAME_KEY, limitMetric.name()) .put(METRIC_VALUE_TYPE_KEY, METRIC_VALUE_TYPE) - .put(METRIC_KIND_KEY, METRIC_KIND) - .build()); - ImmutableMap.Builder limitBuilder = ImmutableMap.builder() + .put(METRIC_KIND_KEY, METRIC_KIND); + if (!Strings.isEmptyOrWhitespace(limitMetric.displayName())) { + builder.put(LIMIT_DISPLAY_NAME_KEY, limitMetric.displayName()); + } + metrics.add(builder.build()); + limits.add(ImmutableMap.builder() .put(LIMIT_NAME_KEY, limitMetric.name()) .put(LIMIT_METRIC_KEY, limitMetric.name()) .put(LIMIT_DEFAULT_LIMIT_KEY, ImmutableMap.of("STANDARD", limitMetric.limit())) - .put(LIMIT_UNIT_KEY, LIMIT_PER_MINUTE_PER_PROJECT); - if (limitMetric.displayName() != null && !"".equals(limitMetric.displayName())) { - limitBuilder.put(LIMIT_DISPLAY_NAME_KEY, limitMetric.displayName()); - } - limits.add(limitBuilder.build()); + .put(LIMIT_UNIT_KEY, LIMIT_PER_MINUTE_PER_PROJECT).build()); } quotaDefinitions.put(LIMITS_KEY, limits); swagger.setVendorExtension(MANAGEMENT_DEFINITIONS_KEY, @@ -237,7 +395,7 @@ private void writeQuotaDefinitions(Swagger swagger, GenerationContext genCtx) { } private void writeApi(ApiKey apiKey, ImmutableList apiConfigs, - Swagger swagger, GenerationContext genCtx) + Swagger swagger, SwaggerContext context, GenerationContext genCtx) throws ApiConfigException { // TODO: This may result in duplicate validations in the future if made available online genCtx.validator.validate(apiConfigs); @@ -245,14 +403,42 @@ private void writeApi(ApiKey apiKey, ImmutableList apiConfi for (ApiLimitMetricConfig limitMetric : apiConfig.getApiLimitMetrics()) { addNonConflictingApiLimitMetric(genCtx.limitMetrics, limitMetric); } - writeApiClass(apiConfig, swagger, genCtx); + writeApiClass(apiConfig, swagger, context, genCtx); + swagger.tag(getTag(apiConfig, context)); } List schemas = genCtx.schemata.getAllSchemaForApi(apiKey); for (Schema schema : schemas) { - if (schema.enumValues().isEmpty()) { - getOrCreateDefinitionMap(swagger).put(schema.name(), convertToSwaggerSchema(schema)); + //enum, maps and some explicitly listed models should be inlined + if (isEnumModel(schema) || isMapModel(schema) || isInlinedModel(schema)) { + continue; } + getOrCreateDefinitionMap(swagger).put(schema.name(), convertToSwaggerSchema(schema)); + } + } + + private boolean isEnumModel(Schema schema) { + return !schema.enumValues().isEmpty(); + } + + private boolean isMapModel(Schema schema) { + return SchemaRepository.isJsonMapSchema(schema) || schema.mapValueSchema() != null; + } + + private boolean isInlinedModel(Schema schema) { + return INLINED_MODEL_NAMES.contains(schema.name()); + } + + private Tag getTag(ApiConfig apiConfig, SwaggerContext context) { + Tag tag = new Tag().name(getTagName(apiConfig, context)); + String description = apiConfig.getDescription(); + if (!Strings.isEmptyOrWhitespace(description)) { + tag.description(description); + } + String documentationLink = apiConfig.getDocumentationLink(); + if (!Strings.isEmptyOrWhitespace(documentationLink)) { + tag.externalDocs(new ExternalDocs().url(documentationLink)); } + return tag; } private void addNonConflictingApiLimitMetric( @@ -267,62 +453,104 @@ private void addNonConflictingApiLimitMetric( limitMetrics.put(limitMetric.name(), limitMetric); } - private void writeApiClass(ApiConfig apiConfig, Swagger swagger, + private void writeApiClass(ApiConfig apiConfig, Swagger swagger, SwaggerContext context, GenerationContext genCtx) throws ApiConfigException { Map methodConfigs = apiConfig.getApiClassConfig().getMethods(); for (Map.Entry methodConfig : methodConfigs.entrySet()) { if (!methodConfig.getValue().isIgnored()) { ApiMethodConfig config = methodConfig.getValue(); - writeApiMethod(config, apiConfig, swagger, genCtx); + writeApiMethod(config, apiConfig, swagger, context, genCtx); } } } - private void writeApiMethod( - ApiMethodConfig methodConfig, ApiConfig apiConfig, Swagger swagger, GenerationContext genCtx) - throws ApiConfigException { + private void writeApiMethod(ApiMethodConfig methodConfig, ApiConfig apiConfig, Swagger swagger, + SwaggerContext context, GenerationContext genCtx) throws ApiConfigException { Path path = getOrCreatePath(swagger, methodConfig); - Operation operation = new Operation(); - operation.setOperationId(getOperationId(apiConfig, methodConfig)); - operation.setDescription(methodConfig.getDescription()); + Operation operation = new Operation() + .operationId(getOperationId(apiConfig, methodConfig, context)) + .tags(Collections.singletonList(getTagName(apiConfig, context))) + .description(methodConfig.getDescription()) + .deprecated(methodConfig.isDeprecated() ? true : null); Collection pathParameters = methodConfig.getPathParameters(); for (ApiParameterConfig parameterConfig : methodConfig.getParameterConfigs()) { + boolean isPathParameter = pathParameters.contains(parameterConfig.getName()); switch (parameterConfig.getClassification()) { case API_PARAMETER: - boolean isPathParameter = pathParameters.contains(parameterConfig.getName()); - SerializableParameter parameter = + AbstractSerializableParameter parameter = isPathParameter ? new PathParameter() : new QueryParameter(); - parameter.setName(parameterConfig.getName()); - parameter.setDescription(parameterConfig.getDescription()); + parameter.name(parameterConfig.getName()).description(parameterConfig.getDescription()); + String defaultValue = parameterConfig.getDefaultValue(); + if (!Strings.isEmptyOrWhitespace(defaultValue)) { + parameter.setDefaultValue(defaultValue); + } + Optional.ofNullable(parameterConfig.getValidationConstraints()).ifPresent(constraints -> { + String pattern = constraints.getPattern(); + if (!Strings.isEmptyOrWhitespace(pattern)) { + parameter.setPattern(pattern); + } + // DecimalMin/Max annotations take precedence over Min/Max + if (constraints.getDecimalMin() != null) { + parameter.setMinimum(new BigDecimal(constraints.getDecimalMin())); + parameter.setExclusiveMinimum(!constraints.getDecimalMinInclusive()); + } else if (constraints.getMin() != null) { + parameter.setMinimum(new BigDecimal(constraints.getMin())); + } + if (constraints.getDecimalMax() != null) { + parameter.setMaximum(new BigDecimal(constraints.getDecimalMax())); + parameter.setExclusiveMaximum(!constraints.getDecimalMaxInclusive()); + } else if (constraints.getMax() != null) { + parameter.setMaximum(new BigDecimal(constraints.getMax())); + } + if (constraints.getMinSize() != null && constraints.getMinSize() > 0) { + if (parameterConfig.isRepeated()) { + parameter.setMinItems(constraints.getMinSize()); + } else if (parameterConfig.getType().getType() == String.class) { + parameter.setMinLength(constraints.getMinSize()); + } + } + if (constraints.getMaxSize() != null && constraints.getMaxSize() < Integer.MAX_VALUE) { + if (parameterConfig.isRepeated()) { + parameter.setMaxItems(constraints.getMaxSize()); + } else if (parameterConfig.getType().getType() == String.class) { + parameter.setMaxLength(constraints.getMaxSize()); + } + } + }); boolean required = isPathParameter || (!parameterConfig.getNullable() - && parameterConfig.getDefaultValue() == null); + && defaultValue == null); if (parameterConfig.isRepeated()) { TypeToken t = parameterConfig.getRepeatedItemSerializedType(); - parameter.setType("array"); + parameter.type("array") + //RestServletRequestParamReader uses "," as a separator for repeated path params + // => csv, but reads multiple occurrences of query parameters => multi + .collectionFormat(isPathParameter ? "csv" : "multi"); Property p = getSwaggerArrayProperty(t); if (parameterConfig.isEnum()) { // TODO: Not sure if this is the right check - ((StringProperty) p).setEnum(getEnumValues(t)); + ((StringProperty) p)._enum(getEnumValues(t)); } - parameter.setItems(p); + parameter.items(p); } else if (parameterConfig.isEnum()) { - parameter.setType("string"); - parameter.setEnum(getEnumValues(parameterConfig.getType())); - parameter.setRequired(required); + parameter.type("string") + ._enum(getEnumValues(parameterConfig.getType())) + .required(required); } else { - parameter.setType( - TYPE_TO_STRING_MAP.get(parameterConfig.getSchemaBaseType().getType())); - parameter.setFormat( - TYPE_TO_FORMAT_MAP.get(parameterConfig.getSchemaBaseType().getType())); - parameter.setRequired(required); + parameter.type( + TYPE_TO_STRING_MAP.get(parameterConfig.getSchemaBaseType().getType())) + .format( + TYPE_TO_FORMAT_MAP.get(parameterConfig.getSchemaBaseType().getType())) + .required(required); } operation.parameter(parameter); break; case RESOURCE: TypeToken requestType = parameterConfig.getSchemaBaseType(); Schema schema = genCtx.schemata.getOrAdd(requestType, apiConfig); - BodyParameter bodyParameter = new BodyParameter(); - bodyParameter.setName("body"); - bodyParameter.setSchema(new RefModel(schema.name())); + BodyParameter bodyParameter = new BodyParameter() + .name(schema.name()) + .description(parameterConfig.getDescription()) + .schema(getSchema(schema)); + bodyParameter.setRequired(true); operation.addParameter(bodyParameter); break; case UNKNOWN: @@ -332,13 +560,35 @@ private void writeApiMethod( } } Response response = new Response().description("A successful response"); + int responseCode = methodConfig.getEffectiveResponseStatus(); if (methodConfig.hasResourceInResponse()) { TypeToken returnType = ApiAnnotationIntrospector.getSchemaType(methodConfig.getReturnType(), apiConfig); Schema schema = genCtx.schemata.getOrAdd(returnType, apiConfig); - response.setSchema(new RefProperty(schema.name())); + response.responseSchema(getSchema(schema)) + .description("A " + schema.name() + " response"); } - operation.response(200, response); + operation.response(responseCode, response); + + boolean addGoogleJsonErrorAsDefaultResponse = context.addGoogleJsonErrorAsDefaultResponse; + boolean addErrorCodesForServiceExceptions = context.addErrorCodesForServiceExceptions; + if (addGoogleJsonErrorAsDefaultResponse || addErrorCodesForServiceExceptions) { + //add error response model only if necessary + if (addErrorCodesForServiceExceptions) { + //add error code specific to the exceptions thrown by the method + List errorCodes = methodConfig.getErrorReponses(); + for (ErrorResponse error : errorCodes) { + operation.response(error.code, + getOrCreateErrorModelRef(swagger, apiConfig, genCtx, error.name, error.description)); + } + } + if (addGoogleJsonErrorAsDefaultResponse) { + //add GoogleJsonError as the default response + operation.defaultResponse( + getOrCreateErrorModelRef(swagger, apiConfig, genCtx, null,null)); + } + } + writeAuthConfig(swagger, methodConfig, operation); if (methodConfig.isApiKeyRequired()) { List>> security = operation.getSecurity(); @@ -361,6 +611,31 @@ private void writeApiMethod( addDefinedMetricCosts(genCtx.limitMetrics, operation, methodConfig.getMetricCosts()); } + private RefResponse getOrCreateErrorModelRef(Swagger swagger, ApiConfig apiConfig, + GenerationContext genCtx, String name, String description) { + Model schema = getSchema(genCtx.schemata + .getOrAdd(TypeToken.of(GoogleJsonErrorContainer.class), apiConfig)); + if (swagger.getResponses() == null) { + swagger.setResponses(new TreeMap<>()); + } + String ref = Optional.ofNullable(name).orElse("DefaultError"); + swagger.response(ref, new Response() + .description(Optional.ofNullable(description).orElse("A failed response")) + .responseSchema(schema)); + return new RefResponse(getFullRef(RefType.RESPONSE, ref)); + } + + private Model getSchema(Schema schema) { + if (SchemaRepository.isJsonMapSchema(schema)) { + return new ModelImpl().additionalProperties(FREE_FORM_PROPERTY); + } + Field mapField = schema.mapValueSchema(); + if (mapField != null) { + return new ModelImpl().additionalProperties(convertToSwaggerProperty(mapField)); + } + return new RefModel(getFullRef(RefType.DEFINITION, schema.name())); + } + private void writeAuthConfig(Swagger swagger, ApiMethodConfig methodConfig, Operation operation) throws ApiConfigException { ApiIssuerAudienceConfig issuerAudiences = methodConfig.getIssuerAudiences(); @@ -370,22 +645,27 @@ private void writeAuthConfig(Swagger swagger, ApiMethodConfig methodConfig, Oper if (issuerAudiencesIsEmpty && legacyAudiencesIsEmpty) { return; } + ImmutableList scopes = ImmutableList + .copyOf(methodConfig.getScopeExpression().getAllScopes()); if (!issuerAudiencesIsEmpty) { for (String issuer : issuerAudiences.getIssuerNames()) { ImmutableSet audiences = issuerAudiences.getAudiences(issuer); IssuerConfig issuerConfig = methodConfig.getApiConfig().getIssuers().getIssuer(issuer); - String fullIssuer = addNonConflictingSecurityDefinition(swagger, issuerConfig, audiences); - operation.addSecurity(fullIssuer, ImmutableList.of()); + List requiredScopes = issuerConfig.isUseScopesInAuthFlow() ? scopes : + Collections.emptyList(); + String fullIssuer = addNonConflictingSecurityDefinition(swagger, issuerConfig, audiences, + requiredScopes); + operation.addSecurity(fullIssuer, requiredScopes); } } if (!legacyAudiencesIsEmpty) { ImmutableSet legacyAudienceSet = ImmutableSet.copyOf(legacyAudiences); String fullIssuer = addNonConflictingSecurityDefinition( - swagger, ApiIssuerConfigs.GOOGLE_ID_TOKEN_ISSUER, legacyAudienceSet); + swagger, ApiIssuerConfigs.GOOGLE_ID_TOKEN_ISSUER, legacyAudienceSet, scopes); String fullAltIssuer = addNonConflictingSecurityDefinition( - swagger, ApiIssuerConfigs.GOOGLE_ID_TOKEN_ISSUER_ALT, legacyAudienceSet); - operation.addSecurity(fullIssuer, ImmutableList.of()); - operation.addSecurity(fullAltIssuer, ImmutableList.of()); + swagger, ApiIssuerConfigs.GOOGLE_ID_TOKEN_ISSUER_ALT, legacyAudienceSet, scopes); + operation.addSecurity(fullIssuer, scopes); + operation.addSecurity(fullAltIssuer, scopes); } } @@ -407,15 +687,21 @@ private void addDefinedMetricCosts(Map limitMetric private Model convertToSwaggerSchema(Schema schema) { ModelImpl docSchema = new ModelImpl().type("object"); - Map fields = Maps.newLinkedHashMap(); + String description = schema.description(); + if (!Strings.isEmptyOrWhitespace(description)) { + docSchema.description(description); + } if (!schema.fields().isEmpty()) { + Map fields = new TreeMap<>(); for (Field f : schema.fields().values()) { fields.put(f.name(), convertToSwaggerProperty(f)); } docSchema.setProperties(fields); } - if (schema.mapValueSchema() != null) { - docSchema.setAdditionalProperties(convertToSwaggerProperty(schema.mapValueSchema())); + //map schema should be inlined, but handling anyway + Field mapValueSchema = schema.mapValueSchema(); + if (mapValueSchema != null) { + docSchema.setAdditionalProperties(convertToSwaggerProperty(mapValueSchema)); } return docSchema; } @@ -430,12 +716,21 @@ private Property convertToSwaggerProperty(Field f) { //cannot happen, as Property subclasses are guaranteed to have a default constructor } } else { + SchemaReference schemaReference = f.schemaReference(); if (f.type() == FieldType.OBJECT) { - p = new RefProperty(f.schemaReference().get().name()); + Schema schema = schemaReference.get(); + if (isInlinedModel(schema)) { + p = inlineObjectProperty(schemaReference); + } else if (isMapModel(schema)) { + p = inlineMapProperty(schemaReference); + } else { + String name = schema.name(); + p = new RefProperty(getFullRef(RefType.DEFINITION, name)); + } } else if (f.type() == FieldType.ARRAY) { p = new ArrayProperty(convertToSwaggerProperty(f.arrayItemSchema())); } else if (f.type() == FieldType.ENUM) { - p = new StringProperty()._enum(getEnumValues(f.schemaReference().type())); + p = new StringProperty()._enum(getEnumValues(schemaReference.type())); } } if (p == null) { @@ -444,14 +739,70 @@ private Property convertToSwaggerProperty(Field f) { //the spec explicitly disallows description on $ref if (!(p instanceof RefProperty)) { p.description(f.description()); + if (f.required() != null) { + p.setRequired(f.required()); + } + if (f.constraints() != null) { + if (p instanceof StringProperty) { + ((StringProperty) p).setPattern(f.constraints().pattern()); + ((StringProperty) p).setMinLength(f.constraints().minSize()); + ((StringProperty) p).setMaxLength(f.constraints().maxSize()); + } else if (p instanceof BaseIntegerProperty) { + if (f.constraints().min() != null) { + ((BaseIntegerProperty) p).setMinimum(BigDecimal.valueOf(f.constraints().min())); + } + if (f.constraints().max() != null) { + ((BaseIntegerProperty) p).setMaximum(BigDecimal.valueOf(f.constraints().max())); + } + } else if (p instanceof DecimalProperty) { + if (f.constraints().decimalMin() != null) { + ((DecimalProperty) p).setMinimum(new BigDecimal(f.constraints().decimalMin())); + ((DecimalProperty) p).setExclusiveMinimum(!f.constraints().decimalMinInclusive()); + } + if (f.constraints().decimalMax() != null) { + ((DecimalProperty) p).setMaximum(new BigDecimal(f.constraints().decimalMax())); + ((DecimalProperty) p).setExclusiveMaximum(!f.constraints().decimalMaxInclusive()); + } + } else if (p instanceof ArrayProperty) { + if (f.constraints().minSize() != null && f.constraints().minSize() > 0) { + ((ArrayProperty) p).setMinItems(f.constraints().minSize()); + } + if (f.constraints().maxSize() != null && f.constraints().maxSize() < Integer.MAX_VALUE) { + ((ArrayProperty) p).setMaxItems(f.constraints().maxSize()); + } + } + } } return p; } - private static String getOperationId(ApiConfig apiConfig, ApiMethodConfig methodConfig) { - return FluentIterable.of(apiConfig.getName(), apiConfig.getVersion(), - apiConfig.getResource(), apiConfig.getApiClassConfig().getResource(), - methodConfig.getEndpointMethodName()).transform(CONVERTER).join(JOINER); + private Property inlineObjectProperty(SchemaReference schemaReference) { + Schema schema = schemaReference.get(); + Map properties = Maps + .transformValues(schema.fields(), this::convertToSwaggerProperty); + return new ObjectProperty(ImmutableMap.copyOf(properties)); + } + + private MapProperty inlineMapProperty(SchemaReference schemaReference) { + Schema schema = schemaReference.get(); + Field mapField = schema.mapValueSchema(); + if (SchemaRepository.isJsonMapSchema(schema) + || mapField == null) { //map field should not be null for non-JsonMap schema, handling anyway + return new MapProperty(FREE_FORM_PROPERTY); + } + return new MapProperty(convertToSwaggerProperty(mapField)); + } + + private static String getTagName(ApiConfig apiConfig, SwaggerContext context) { + return NamingContext.build(apiConfig, null).resolve(context.tagTemplate); + } + + private String getFullRef(RefType type, String name) { + return type.getInternalPrefix() + UrlEscapers.urlFormParameterEscaper().escape(name); + } + + private static String getOperationId(ApiConfig apiConfig, ApiMethodConfig methodConfig, SwaggerContext context) { + return NamingContext.build(apiConfig, methodConfig).resolve(context.operationIdTemplate); } private static Property getSwaggerArrayProperty(TypeToken typeToken) { @@ -469,7 +820,10 @@ private static Property getSwaggerArrayProperty(TypeToken typeToken) { } else if (type == Double.class || type == Double.TYPE) { return new DoubleProperty(); } else if (type == byte[].class) { - return new ByteArrayProperty(); + ByteArrayProperty property = new ByteArrayProperty(); + //this will add a base64 pattern to the property + return PropertyBuilder.build(property.getType(), property.getFormat(), + Collections.emptyMap()); } else if (type.isEnum()) { return new StringProperty(); } @@ -481,22 +835,25 @@ private Path getOrCreatePath(Swagger swagger, ApiMethodConfig methodConfig) { Path path = swagger.getPath(pathStr); if (path == null) { path = new Path(); + if (swagger.getPaths() == null) { + swagger.setPaths(new TreeMap<>()); + } swagger.path(pathStr, path); } return path; } private static List getEnumValues(TypeToken t) { - List values = Lists.newArrayList(); - for (Object value : t.getRawType().getEnumConstants()) { - values.add(value.toString()); + if (Types.isOptional(t)) { + t = Types.getTypeParameter(t, 0); } - return values; + return new ArrayList<>(Types.getEnumValuesAndDescriptions((TypeToken>) t).keySet()); } - private static SecuritySchemeDefinition toScheme( + private static OAuth2Definition toScheme( IssuerConfig issuerConfig, ImmutableSet audiences) { - OAuth2Definition tokenDef = new OAuth2Definition().implicit(""); + OAuth2Definition tokenDef = new OAuth2Definition() + .implicit(issuerConfig.getAuthorizationUrl()); tokenDef.setVendorExtension("x-google-issuer", issuerConfig.getIssuer()); if (!com.google.common.base.Strings.isNullOrEmpty(issuerConfig.getJwksUri())) { tokenDef.setVendorExtension("x-google-jwks_uri", issuerConfig.getJwksUri()); @@ -508,7 +865,7 @@ private static SecuritySchemeDefinition toScheme( private Map getOrCreateDefinitionMap(Swagger swagger) { Map definitions = swagger.getDefinitions(); if (definitions == null) { - definitions = new LinkedHashMap<>(); + definitions = new TreeMap<>(); swagger.setDefinitions(definitions); } return definitions; @@ -518,55 +875,70 @@ private static Map getOrCreateSecurityDefiniti Swagger swagger) { Map securityDefinitions = swagger.getSecurityDefinitions(); if (securityDefinitions == null) { - securityDefinitions = new LinkedHashMap<>(); + securityDefinitions = new TreeMap<>(); swagger.setSecurityDefinitions(securityDefinitions); } return securityDefinitions; } - private static String addNonConflictingSecurityDefinition( - Swagger swagger, IssuerConfig issuerConfig, ImmutableSet audiences) + private static String addNonConflictingSecurityDefinition(Swagger swagger, + IssuerConfig issuerConfig, ImmutableSet audiences, List scopes) throws ApiConfigException { Map securityDefinitions = getOrCreateSecurityDefinitionMap(swagger); String issuerPlusHash = String.format("%s-%x", issuerConfig.getName(), audiences.hashCode()); - SecuritySchemeDefinition existingDef = securityDefinitions.get(issuerConfig.getName()); - SecuritySchemeDefinition newDef = toScheme(issuerConfig, audiences); - if (existingDef != null && !existingDef.equals(newDef)) { - throw new ApiConfigException( - "Multiple conflicting definitions found for issuer " + issuerConfig.getName()); + OAuth2Definition newDef = toScheme(issuerConfig, audiences); + SecuritySchemeDefinition existingDef = securityDefinitions.get(issuerPlusHash); + if (existingDef != null) { + checkExistingDefinition(issuerConfig.getName(), newDef, existingDef); } - swagger.securityDefinition(issuerPlusHash, newDef); + OAuth2Definition def = existingDef != null ? (OAuth2Definition) existingDef : newDef; + scopes.forEach(scope -> def.addScope(scope, AuthScopeRepository.getDescription(scope))); + swagger.securityDefinition(issuerPlusHash, def); return issuerPlusHash; } + private static void checkExistingDefinition(String defName, OAuth2Definition newDef, + SecuritySchemeDefinition existingDef) throws ApiConfigException { + if (!(existingDef instanceof OAuth2Definition)) { + throw new ApiConfigException( + "Conflicting definition types found for issuer " + defName); + } + OAuth2Definition existingOAuth2Def = (OAuth2Definition) existingDef; + boolean propertiesMatchExceptScope = Stream.>of( + OAuth2Definition::getType, OAuth2Definition::getAuthorizationUrl, + OAuth2Definition::getFlow, OAuth2Definition::getTokenUrl, + OAuth2Definition::getDescription, OAuth2Definition::getVendorExtensions) + .allMatch(getter -> Objects.equals(getter.apply(existingOAuth2Def), getter.apply(newDef))); + if (!propertiesMatchExceptScope) { + throw new ApiConfigException( + "Conflicting OAuth2 definitions found for issuer " + defName); + } + } + public static class SwaggerContext { + public static final String DEFAULT_TAG_TEMPLATE = "${apiName}:${apiVersion}${.Resource}"; + public static final String DEFAULT_OPERATION_ID_TEMPLATE = "${apiName}:${apiVersion}${.Resource}${.method}"; + private Scheme scheme = Scheme.HTTPS; private String hostname = "myapi.appspot.com"; private String basePath = "/_ah/api"; private String docVersion = "1.0.0"; - - public SwaggerContext setApiRoot(String apiRoot) { - try { - URL url = new URL(apiRoot); - hostname = url.getHost(); - if (("http".equals(url.getProtocol()) && url.getPort() != 80 && url.getPort() != -1) - || ("https".equals(url.getProtocol()) && url.getPort() != 443 && url.getPort() != -1)) { - hostname += ":" + url.getPort(); - } - basePath = Strings.stripTrailingSlash(url.getPath()); - setScheme(url.getProtocol()); - return this; - } catch (MalformedURLException e) { - throw new IllegalArgumentException(e); - } - } + private String title; + private String description; + private String apiName; + private String tagTemplate = DEFAULT_TAG_TEMPLATE; + private String operationIdTemplate = DEFAULT_OPERATION_ID_TEMPLATE; + private boolean addGoogleJsonErrorAsDefaultResponse; + private boolean addErrorCodesForServiceExceptions; + private boolean extractCommonParametersAsRefs; + private boolean combineCommonParametersInSamePath; public SwaggerContext setScheme(String scheme) { this.scheme = "http".equals(scheme) ? Scheme.HTTP : Scheme.HTTPS; return this; } - + public SwaggerContext setHostname(String hostname) { this.hostname = hostname; return this; @@ -581,12 +953,110 @@ public SwaggerContext setDocVersion(String docVersion) { this.docVersion = docVersion; return this; } + + public SwaggerContext setTitle(String title) { + this.title = title; + return this; + } + + public SwaggerContext setDescription(String description) { + this.description = description; + return this; + } + + public SwaggerContext setApiName(String apiName) { + this.apiName = apiName; + return this; + } + + public SwaggerContext setTagTemplate(String tagTemplate) { + this.tagTemplate = tagTemplate; + return this; + } + + public SwaggerContext setOperationIdTemplate(String operationIdTemplate) { + this.operationIdTemplate = operationIdTemplate; + return this; + } + + public SwaggerContext setAddGoogleJsonErrorAsDefaultResponse(boolean addGoogleJsonErrorAsDefaultResponse) { + this.addGoogleJsonErrorAsDefaultResponse = addGoogleJsonErrorAsDefaultResponse; + return this; + } + + public SwaggerContext setAddErrorCodesForServiceExceptions(boolean addErrorCodesForServiceExceptions) { + this.addErrorCodesForServiceExceptions = addErrorCodesForServiceExceptions; + return this; + } + + public SwaggerContext setExtractCommonParametersAsRefs(boolean extractCommonParametersAsRefs) { + this.extractCommonParametersAsRefs = extractCommonParametersAsRefs; + return this; + } + + public SwaggerContext setCombineCommonParametersInSamePath(boolean combineCommonParametersInSamePath) { + this.combineCommonParametersInSamePath = combineCommonParametersInSamePath; + return this; + } } private static class GenerationContext { private final Map limitMetrics = new TreeMap<>(); private ApiConfigValidator validator; - private boolean writeInternal; private SchemaRepository schemata; } + + /** + * A template mechanism based on Apache Commons lang's StrSubstitutor (placeholder syntax is "${var}). + * + * The following variables are available on API and API method contexts: + * - apiName + * - apiVersion + * - resource (might be null for API or method context) + * - method (is null when working in a method context) + * + * Each variable comes with following variants: + * - Uppercased variants (if "${apiName}" is "myApi", "${ApiName}" will be "MyAPi" + * - Prefixed with "-",":" or "." (only chars that are safe for use in Swagger tags for Endpoints Portal) + * - Prefixed variants should be used for nullable vars: "${.resource}" will be empty if the resource var is null, but will be ".myResource" if resource is "myResource" + * - Prefixed variants also come in uppercased flavors ("${.Resource}" will be ".MyResource" if resource var is "myResource") + */ + private static class NamingContext { + + private static final Converter UPPER + = CaseFormat.LOWER_CAMEL.converterTo(CaseFormat.UPPER_CAMEL); + private final Map values = new HashMap<>(); + private final String prefixes; + + private static NamingContext build(ApiConfig apiConfig, ApiMethodConfig methodConfig) { + String resource = apiConfig.getApiClassConfig().getResource(); + String method = methodConfig != null ? methodConfig.getEndpointMethodName() : null; + return new NamingContext("-:.") + .put("apiName", apiConfig.getName()) + .put("apiVersion", apiConfig.getVersion()) + .put("resource", resource) + .put("method", method); + } + + NamingContext(String prefixes) { + this.prefixes = prefixes; + } + + NamingContext put(String key, String value) { + value = com.google.common.base.Strings.nullToEmpty(value); + values.put(key, value); + values.put(UPPER.convert(key), UPPER.convert(value)); + for (char c : prefixes.toCharArray()) { + values.put(c + key, value.isEmpty() ? "" : c + value); + values.put(c + UPPER.convert(key), value.isEmpty() ? "" : c + UPPER.convert(value)); + } + return this; + } + + String resolve(String template) { + return new StrSubstitutor(values).replace(template); + } + + } + } diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/swagger/UNSUPPORTED_FEATURES.md b/endpoints-framework/src/main/java/com/google/api/server/spi/swagger/UNSUPPORTED_FEATURES.md new file mode 100644 index 00000000..0aa6af0c --- /dev/null +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/swagger/UNSUPPORTED_FEATURES.md @@ -0,0 +1,11 @@ +- Info + - license, contact, terms of service +- Paths + - Configurable tag content and format +- Parameters + - other repeated param features (uniqueItems, default values) + - empty value parameters + - headers in params +- Responses + - use introspection or new annotation to describe usage of any subclasses of ServiceException + - headers in response \ No newline at end of file diff --git a/endpoints-framework/src/test/java/com/google/api/server/spi/BackendPropertiesTest.java b/endpoints-framework/src/test/java/com/google/api/server/spi/BackendPropertiesTest.java index 69b1e925..5fe7d65d 100644 --- a/endpoints-framework/src/test/java/com/google/api/server/spi/BackendPropertiesTest.java +++ b/endpoints-framework/src/test/java/com/google/api/server/spi/BackendPropertiesTest.java @@ -23,7 +23,7 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.Mockito; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; /** * Tests for {@link BackendProperties}. @@ -97,8 +97,6 @@ public void testGetApplicationId_appEngine() { @Test public void testGetApplicationId_flex() { System.clearProperty(BackendProperties.APP_ID_PROPERTY); - Mockito.when(envReader.getenv(BackendProperties.GCLOUD_PROJECT_PROPERTY)) - .thenReturn(APPLICATION_ID); assertNull(properties.getApplicationId()); } } diff --git a/endpoints-framework/src/test/java/com/google/api/server/spi/ConfiguredObjectMapperTest.java b/endpoints-framework/src/test/java/com/google/api/server/spi/ConfiguredObjectMapperTest.java index 1200fec0..7b3ac00d 100644 --- a/endpoints-framework/src/test/java/com/google/api/server/spi/ConfiguredObjectMapperTest.java +++ b/endpoints-framework/src/test/java/com/google/api/server/spi/ConfiguredObjectMapperTest.java @@ -19,7 +19,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertSame; -import static org.mockito.Matchers.any; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.when; @@ -38,7 +38,7 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.Mockito; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import java.util.Map; @@ -119,9 +119,9 @@ public void testBuildWithModules_oneWithAll() { doModuleSetup(moduleB, "moduleB"); doModuleSetup(moduleC, "moduleC"); builder.addRegisteredModules(ImmutableList.of(moduleA, moduleB, moduleC)).build(); - Mockito.verify(moduleA, atLeastOnce()).setupModule(any(SetupContext.class)); - Mockito.verify(moduleB, atLeastOnce()).setupModule(any(SetupContext.class)); - Mockito.verify(moduleC, atLeastOnce()).setupModule(any(SetupContext.class)); + Mockito.verify(moduleA, atLeastOnce()).setupModule(any()); + Mockito.verify(moduleB, atLeastOnce()).setupModule(any()); + Mockito.verify(moduleC, atLeastOnce()).setupModule(any()); assertEquals(1, cache.size()); } diff --git a/endpoints-framework/src/test/java/com/google/api/server/spi/EndpointsServletTest.java b/endpoints-framework/src/test/java/com/google/api/server/spi/EndpointsServletTest.java index 86b36fa6..2ce99d5c 100644 --- a/endpoints-framework/src/test/java/com/google/api/server/spi/EndpointsServletTest.java +++ b/endpoints-framework/src/test/java/com/google/api/server/spi/EndpointsServletTest.java @@ -20,11 +20,14 @@ import com.google.api.server.spi.config.Api; import com.google.api.server.spi.config.ApiMethod; import com.google.api.server.spi.config.ApiMethod.HttpMethod; +import com.google.api.server.spi.response.FoundRedirectException; +import com.google.api.server.spi.response.SeeOtherRedirectException; import com.google.common.base.Splitter; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; +import java.nio.charset.StandardCharsets; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -75,7 +78,23 @@ public void explorer() throws IOException { assertThat(resp.getStatus()).isEqualTo(HttpServletResponse.SC_FOUND); assertThat(resp.getHeader("Location")).isEqualTo( - "http://apis-explorer.appspot.com/apis-explorer/?base=" + API_ROOT + "&root=" + API_ROOT); + "https://apis-explorer.appspot.com/apis-explorer/?base=" + API_ROOT); + } + + @Test + public void customExplorer() throws IOException, ServletException { + MockServletConfig config = new MockServletConfig(); + config.addInitParameter("apiExplorerUrlTemplate", "http://mycustomapiexplorer.com/#${apiBase}"); + servlet.init(config); + + req.setRequestURI("/_ah/api/explorer/"); + req.setMethod("GET"); + + servlet.service(req, resp); + + assertThat(resp.getStatus()).isEqualTo(HttpServletResponse.SC_FOUND); + assertThat(resp.getHeader("Location")).isEqualTo( + "http://mycustomapiexplorer.com/#" + API_ROOT); } @Test @@ -97,12 +116,22 @@ public void empty() throws IOException { assertThat(resp.getStatus()).isEqualTo(HttpServletResponse.SC_NO_CONTENT); } + + @Test + public void nullResponse() throws IOException { + req.setRequestURI("/_ah/api/test/v2/null"); + req.setMethod("GET"); + + servlet.service(req, resp); + + assertThat(resp.getStatus()).isEqualTo(HttpServletResponse.SC_NO_CONTENT); + } @Test public void echo() throws IOException { req.setRequestURI("/_ah/api/test/v2/echo"); req.setMethod("POST"); - req.setParameter("x", "1"); + req.setContent("{\"x\":1}".getBytes(StandardCharsets.UTF_8)); servlet.service(req, resp); @@ -117,7 +146,7 @@ public void echo() throws IOException { public void contentLengthHeaderNull() throws IOException { req.setRequestURI("/_ah/api/test/v2/echo"); req.setMethod("POST"); - req.setParameter("x", "1"); + req.setContent("{\"x\":1}".getBytes(StandardCharsets.UTF_8)); servlet.service(req, resp); @@ -133,7 +162,7 @@ public void contentLengthHeaderPresent() throws IOException, ServletException { req.setRequestURI("/_ah/api/test/v2/echo"); req.setMethod("POST"); - req.setParameter("x", "1"); + req.setContent("{\"x\":1}".getBytes(StandardCharsets.UTF_8)); servlet.service(req, resp); @@ -145,7 +174,7 @@ public void methodOverride() throws IOException { req.setRequestURI("/_ah/api/test/v2/increment"); req.setMethod("POST"); req.addHeader("X-HTTP-Method-Override", "PATCH"); - req.setParameter("x", "1"); + req.setContent("{\"x\":1}".getBytes(StandardCharsets.UTF_8)); servlet.service(req, resp); @@ -186,6 +215,28 @@ public void cors() throws IOException { .containsExactly("HEAD", "DELETE", "GET", "PATCH", "POST", "PUT"); } + @Test + public void redirectionGet_relativeToRequest() throws IOException { + req.setRequestURI("/_ah/api/test/v2/redirect"); + req.setMethod("GET"); + + servlet.service(req, resp); + + assertThat(resp.getStatus()).isEqualTo(HttpServletResponse.SC_FOUND); + assertThat(resp.getHeader("Location")).isEqualTo("/_ah/api/test/v2/redirect/new/location"); + } + + @Test + public void redirectionPost_locationWithScheme() throws IOException { + req.setRequestURI("/_ah/api/test/v2/redirect"); + req.setMethod("POST"); + + servlet.service(req, resp); + + assertThat(resp.getStatus()).isEqualTo(HttpServletResponse.SC_SEE_OTHER); + assertThat(resp.getHeader("Location")).isEqualTo("https://example.com/other/resource"); + } + public static class TestResource { public int x; } @@ -200,6 +251,21 @@ public TestResource echo(TestResource r) { return r; } + @ApiMethod(httpMethod = HttpMethod.GET, path = "null") + public TestResource nullResponse() { + return null; + } + + @ApiMethod(httpMethod = HttpMethod.GET, path = "redirect") + public TestResource redirect() throws ServiceException { + throw new FoundRedirectException("redirecting", "new/location"); + } + + @ApiMethod(httpMethod = HttpMethod.POST, path = "redirect") + public TestResource redirectSeeOther() throws ServiceException { + throw new SeeOtherRedirectException("redirecting", "https://example.com/other/resource"); + } + @ApiMethod(httpMethod = "PATCH") public TestResource increment(TestResource r) { r.x = r.x + 1; diff --git a/endpoints-framework/src/test/java/com/google/api/server/spi/EnvUtilTest.java b/endpoints-framework/src/test/java/com/google/api/server/spi/EnvUtilTest.java index 38c0b6fe..f2a6db47 100644 --- a/endpoints-framework/src/test/java/com/google/api/server/spi/EnvUtilTest.java +++ b/endpoints-framework/src/test/java/com/google/api/server/spi/EnvUtilTest.java @@ -29,6 +29,7 @@ */ @RunWith(JUnit4.class) public class EnvUtilTest { + @Test public void testIsRunningOnAppEngine() { System.setProperty(EnvUtil.ENV_APPENGINE_RUNTIME, "Production"); assertTrue(EnvUtil.isRunningOnAppEngine()); @@ -45,4 +46,16 @@ public void testIsRunningOnAppEngineProd() { System.clearProperty(EnvUtil.ENV_APPENGINE_RUNTIME); assertFalse(EnvUtil.isRunningOnAppEngineProd()); } + + @Test + public void testHasForceAuthenticationEnabled() { + System.setProperty(EnvUtil.FORCE_AUTHENTICATION_ENABLED, "true"); + assertTrue(EnvUtil.hasForceAuthenticationEnabled()); + System.setProperty(EnvUtil.FORCE_AUTHENTICATION_ENABLED, "false"); + assertFalse(EnvUtil.hasForceAuthenticationEnabled()); + System.setProperty(EnvUtil.FORCE_AUTHENTICATION_ENABLED, "invalid"); + assertFalse(EnvUtil.hasForceAuthenticationEnabled()); + System.clearProperty(EnvUtil.FORCE_AUTHENTICATION_ENABLED); + assertFalse(EnvUtil.hasForceAuthenticationEnabled()); + } } diff --git a/endpoints-framework/src/test/java/com/google/api/server/spi/MethodHierarchyReaderTest.java b/endpoints-framework/src/test/java/com/google/api/server/spi/MethodHierarchyReaderTest.java index 998fd311..0a9d6cd1 100644 --- a/endpoints-framework/src/test/java/com/google/api/server/spi/MethodHierarchyReaderTest.java +++ b/endpoints-framework/src/test/java/com/google/api/server/spi/MethodHierarchyReaderTest.java @@ -35,6 +35,7 @@ import java.lang.reflect.Method; import java.util.Arrays; +import java.util.Collection; import java.util.Date; import java.util.List; import java.util.Map; @@ -84,7 +85,7 @@ private void verifySingleMethod(Method method) { } } - private void verifyOverrides(Method method, List overrides) { + private void verifyOverrides(Method method, Collection overrides) { TestEndpoint.ExpectedMethod expectedMethod = TestEndpoint.ExpectedMethod.fromName(method.getName()); @@ -92,7 +93,7 @@ private void verifyOverrides(Method method, List overrides) { assertEquals("Wrong number of overrides for method: " + method.getName(), 2, overrides.size()); assertEquals("Overridden " + method.getName() + " is wrong class.", - TestEndpointSuperclass.class, overrides.get(1).getMethod().getDeclaringClass()); + TestEndpointSuperclass.class, Iterables.get(overrides, 1).getMethod().getDeclaringClass()); } else { assertEquals("Wrong number of overrides for method: " + method.getName(), 1, overrides.size()); @@ -112,11 +113,11 @@ public void testGetLeafMethods() { @Test public void testGetEndpointOverrides() { - Iterable> methods = methodReader.getEndpointOverrides(); + Iterable> methods = methodReader.getEndpointOverrides(); TestHelper helper = new TestHelper(Iterables.size(methods)); - for (List overrides : methods) { - Method method = overrides.get(0).getMethod(); + for (Collection overrides : methods) { + Method method = overrides.iterator().next().getMethod(); helper.verifySingleMethod(method); verifyOverrides(method, overrides); } diff --git a/endpoints-framework/src/test/java/com/google/api/server/spi/ObjectMapperUtilTest.java b/endpoints-framework/src/test/java/com/google/api/server/spi/ObjectMapperUtilTest.java index 399899c6..f387791d 100644 --- a/endpoints-framework/src/test/java/com/google/api/server/spi/ObjectMapperUtilTest.java +++ b/endpoints-framework/src/test/java/com/google/api/server/spi/ObjectMapperUtilTest.java @@ -17,15 +17,26 @@ import static com.google.common.truth.Truth.assertThat; +import com.google.api.server.spi.config.model.EndpointsFlag; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import com.google.api.server.spi.config.model.EndpointsFlag; +import com.fasterxml.jackson.databind.ObjectReader; import org.junit.Test; +import java.lang.reflect.Constructor; +import java.util.Objects; +import java.util.Optional; +import java.util.OptionalLong; + /** * Tests for {@link ObjectMapperUtil} */ public class ObjectMapperUtilTest { + @Test public void createStandardObjectMapper_base64Variant() throws Exception { byte[] bytes = new byte[] {(byte) 0xff, (byte) 0xef}; @@ -51,6 +62,90 @@ public void createStandardObjectMapper_disableJacksonAnnotations() throws Except } } + @Test + public void createStandardObjectMapper_deserialize_optional() throws JsonProcessingException { + ObjectMapper mapper = ObjectMapperUtil.createStandardObjectMapper(); + ObjectReader reader = mapper.readerFor(TestOptionals.class); + assertThat(reader.readValue("{\"optionalString\":\"value\"}").getOptionalString()) + .isEqualTo(Optional.of("value")); + assertThat(reader.readValue("{\"optionalString\":\"\"}").getOptionalString()) + .isEqualTo(Optional.of("")); + assertThat(reader.readValue("{\"optionalString\":null}").getOptionalString()) + .isEqualTo(Optional.empty()); + assertThat(reader.readValue("{}").getOptionalString()) + .isEqualTo(null); + assertThat(reader.readValue("{\"optionalLong\":123}").getOptionalLong()) + .isEqualTo(OptionalLong.of(123)); + assertThat(reader.readValue("{\"optionalLong\":\"123\"}").getOptionalLong()) + .isEqualTo(OptionalLong.of(123)); + assertThat(reader.readValue("{\"optionalLong\":null}").getOptionalLong()) + .isEqualTo(OptionalLong.empty()); + assertThat(reader.readValue("{}").getOptionalLong()) + .isEqualTo(null); + } + + @Test + public void createStandardObjectMapper_serialize_optional() throws Exception { + testJava8Type(TestOptionals.class, + "{\"optionalString\":null,\"optionalLong\":null}", + "{\"optionalString\":null,\"optionalLong\":null}" + ); + testJava8Type(TestOptionalsNonAbsent.class, + "{}", + "{}" + ); + testJava8Type(TestOptionalsNonEmpty.class, + "{}", + "{}" + ); + testJava8Type(TestOptionalsNonNull.class, + "{\"optionalString\":null,\"optionalLong\":null}", + "{}" + ); + testJava8Type(TestOptionalsNonDefault.class, + "{\"optionalString\":null,\"optionalLong\":null}", + "{}" + ); + testJava8Type(TestOptionalsUseDefaults.class, + "{\"optionalString\":null,\"optionalLong\":null}", + "{\"optionalString\":null,\"optionalLong\":null}" + ); + testJava8Type(TestOptionalsAlways.class, + "{\"optionalString\":null,\"optionalLong\":null}", + "{\"optionalString\":null,\"optionalLong\":null}" + ); + } + + private void testJava8Type(Class type, String expectedForEmpty, + String expectedForNull) throws Exception { + ObjectMapper mapper = ObjectMapperUtil.createStandardObjectMapper(); + Constructor constructor = type.getConstructor(Optional.class, OptionalLong.class); + assertThat(mapper.writeValueAsString(constructor.newInstance(Optional.of("value"), OptionalLong.of(123)))) + .isEqualTo("{\"optionalString\":\"value\",\"optionalLong\":123}"); + assertThat(mapper.writeValueAsString(constructor.newInstance(Optional.of(""), OptionalLong.of(0)))) + .isEqualTo("{\"optionalString\":\"\",\"optionalLong\":0}"); + assertThat(mapper.writeValueAsString(constructor.newInstance(Optional.empty(), OptionalLong.empty()))) + .isEqualTo(expectedForEmpty); + assertThat(mapper.writeValueAsString(constructor.newInstance(null, null))) + .isEqualTo(expectedForNull); + } + + @Test + public void createStandardObjectMapper_parameterNames() throws Exception { + ObjectMapper mapper = ObjectMapperUtil.createStandardObjectMapper(); + TestParameterNames result = mapper.readerFor(TestParameterNames.class) + .readValue("{\"name\":\"Jerry\",\"surname\":\"Smith\"}"); + assertThat(result).isEqualTo(new TestParameterNames("Jerry", "Smith")); + } + + @Test + public void createStandardObjectMapper_parameterNames_misingRequiredField() throws Exception { + ObjectMapper mapper = ObjectMapperUtil.createStandardObjectMapper(); + TestParameterNames result = mapper.readerFor(TestParameterNames.class) + .readValue("{\"name\":\"Jerry\"}"); + assertThat(result).isEqualTo(new TestParameterNames("Jerry", null)); + } + private enum TestEnum { @JsonProperty("test") TEST } @@ -70,4 +165,129 @@ public TestEnum getTest() { return test; } } + + private static class TestOptionals { + Optional optionalString; + OptionalLong optionalLong; + + public TestOptionals() { + } + + public TestOptionals(Optional optionalString, OptionalLong optionalLong) { + this.optionalString = optionalString; + this.optionalLong = optionalLong; + } + + public Optional getOptionalString() { + return optionalString; + } + + public void setOptionalString(Optional optionalString) { + this.optionalString = optionalString; + } + + public OptionalLong getOptionalLong() { + return optionalLong; + } + + public TestOptionals setOptionalLong(OptionalLong optionalLong) { + this.optionalLong = optionalLong; + return this; + } + } + + @JsonInclude(Include.NON_ABSENT) + private static class TestOptionalsNonAbsent extends TestOptionals { + public TestOptionalsNonAbsent() { + } + public TestOptionalsNonAbsent(Optional optionalString, OptionalLong optionalLong) { + super(optionalString, optionalLong); + } + } + + @JsonInclude(Include.NON_EMPTY) + private static class TestOptionalsNonEmpty extends TestOptionals { + public TestOptionalsNonEmpty() { + } + public TestOptionalsNonEmpty(Optional optionalString, OptionalLong optionalLong) { + super(optionalString, optionalLong); + } + } + + @JsonInclude(Include.NON_NULL) + private static class TestOptionalsNonNull extends TestOptionals { + public TestOptionalsNonNull() { + } + public TestOptionalsNonNull(Optional optionalString, OptionalLong optionalLong) { + super(optionalString, optionalLong); + } + } + + @JsonInclude(Include.NON_DEFAULT) + private static class TestOptionalsNonDefault extends TestOptionals { + public TestOptionalsNonDefault() { + } + public TestOptionalsNonDefault(Optional optionalString, + OptionalLong optionalLong) { + super(optionalString, optionalLong); + } + } + + @JsonInclude(Include.USE_DEFAULTS) + private static class TestOptionalsUseDefaults extends TestOptionals { + public TestOptionalsUseDefaults() { + } + public TestOptionalsUseDefaults(Optional optionalString, + OptionalLong optionalLong) { + super(optionalString, optionalLong); + } + } + + @JsonInclude(Include.ALWAYS) + private static class TestOptionalsAlways extends TestOptionals { + public TestOptionalsAlways() { + } + public TestOptionalsAlways(Optional optionalString, OptionalLong optionalLong) { + super(optionalString, optionalLong); + } + } + + private static class TestParameterNames { + + private final String name; + private final String surname; + + //could not be deserialized without @JsonProperty("fieldName) if not compiled with -parameters + public TestParameterNames(String name, String surname) { + this.name = name; + this.surname = surname; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + TestParameterNames that = (TestParameterNames) o; + return Objects.equals(name, that.name) && + Objects.equals(surname, that.surname); + } + + @Override + public int hashCode() { + return Objects.hash(name, surname); + } + + @Override + public String toString() { + return "TestParameterNames{" + + "name='" + name + '\'' + + ", surname='" + surname + '\'' + + '}'; + } + } + } \ No newline at end of file diff --git a/endpoints-framework/src/test/java/com/google/api/server/spi/PeerAuthTest.java b/endpoints-framework/src/test/java/com/google/api/server/spi/PeerAuthTest.java deleted file mode 100644 index 94142efa..00000000 --- a/endpoints-framework/src/test/java/com/google/api/server/spi/PeerAuthTest.java +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Copyright 2016 Google Inc. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.api.server.spi; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotSame; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.when; - -import com.google.api.server.spi.auth.EndpointsPeerAuthenticator; -import com.google.api.server.spi.config.PeerAuthenticator; -import com.google.api.server.spi.config.model.ApiMethodConfig; -import com.google.api.server.spi.request.Attribute; -import com.google.api.server.spi.testing.FailPeerAuthenticator; -import com.google.api.server.spi.testing.PassPeerAuthenticator; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.Lists; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; -import org.springframework.mock.web.MockHttpServletRequest; - -import java.util.List; - -/** - * Test for PeerAuth. - */ -@RunWith(MockitoJUnitRunner.class) -public class PeerAuthTest { - @Mock private ApiMethodConfig config; - - private MockHttpServletRequest request; - private PeerAuth peerAuth; - private Attribute attr; - - @Before - public void setUp() throws Exception { - request = new MockHttpServletRequest(); - attr = Attribute.from(request); - attr.set(Attribute.RESTRICT_SERVLET, true); - attr.set(Attribute.API_METHOD_CONFIG, config); - peerAuth = PeerAuth.from(request); - } - - @Test - public void testGetPeerAuthenticatorInstances_default() throws Exception { - when(config.getPeerAuthenticators()).thenReturn(null); - List peerAuthenticators = - Lists.newArrayList(peerAuth.getPeerAuthenticatorInstances()); - assertEquals(1, peerAuthenticators.size()); - assertTrue(peerAuthenticators.get(0) instanceof EndpointsPeerAuthenticator); - } - - @Test - public void testGetPeerAuthenticatorInstances() throws Exception { - when(config.getPeerAuthenticators()).thenReturn( - ImmutableList.of(PassPeerAuthenticator.class, FailPeerAuthenticator.class)); - List peerAuthenticators = - Lists.newArrayList(peerAuth.getPeerAuthenticatorInstances()); - assertEquals(2, peerAuthenticators.size()); - assertTrue(peerAuthenticators.get(0) instanceof PassPeerAuthenticator); - assertTrue(peerAuthenticators.get(1) instanceof FailPeerAuthenticator); - } - - @Test - public void testGetPeerAuthenticatorInstances_singleton() throws Exception { - when(config.getPeerAuthenticators()).thenReturn(ImmutableList.of(PassPeerAuthenticator.class, - FailPeerAuthenticator.class, PassPeerAuthenticator.class)); - List peerAuthenticators = - Lists.newArrayList(peerAuth.getPeerAuthenticatorInstances()); - assertEquals(3, peerAuthenticators.size()); - assertTrue(peerAuthenticators.get(0) instanceof PassPeerAuthenticator); - assertTrue(peerAuthenticators.get(1) instanceof FailPeerAuthenticator); - assertTrue(peerAuthenticators.get(2) instanceof PassPeerAuthenticator); - assertSame(peerAuthenticators.get(0), peerAuthenticators.get(2)); - } - - @Test - public void testGetPeerAuthenticatorInstances_nonSingleton() throws Exception { - when(config.getPeerAuthenticators()).thenReturn(ImmutableList.of(PassPeerAuthenticator.class, - FailPeerAuthenticator.class, FailPeerAuthenticator.class)); - List peerAuthenticators = - Lists.newArrayList(peerAuth.getPeerAuthenticatorInstances()); - assertEquals(3, peerAuthenticators.size()); - assertTrue(peerAuthenticators.get(0) instanceof PassPeerAuthenticator); - assertTrue(peerAuthenticators.get(1) instanceof FailPeerAuthenticator); - assertTrue(peerAuthenticators.get(2) instanceof FailPeerAuthenticator); - assertNotSame(peerAuthenticators.get(1), peerAuthenticators.get(2)); - } - - @Test - public void testPeerAuthorize_nonRestricted() throws Exception { - attr.set(Attribute.RESTRICT_SERVLET, false); - assertTrue(peerAuth.authorizePeer()); - } - - @Test - public void testPeerAuthorize_pass() throws Exception { - when(config.getPeerAuthenticators()).thenReturn( - ImmutableList.>of(PassPeerAuthenticator.class)); - assertTrue(peerAuth.authorizePeer()); - } - - @Test - public void testPeerAuthorize_fail() throws Exception { - when(config.getPeerAuthenticators()).thenReturn( - ImmutableList.>of(FailPeerAuthenticator.class)); - assertFalse(peerAuth.authorizePeer()); - } - - @Test - public void testPeerAuthorize_passThenFail() throws Exception { - when(config.getPeerAuthenticators()).thenReturn( - ImmutableList.of(PassPeerAuthenticator.class, FailPeerAuthenticator.class)); - assertFalse(peerAuth.authorizePeer()); - } - - @Test - public void testPeerAuthorize_failThenPass() throws Exception { - when(config.getPeerAuthenticators()).thenReturn( - ImmutableList.of(FailPeerAuthenticator.class, PassPeerAuthenticator.class)); - assertFalse(peerAuth.authorizePeer()); - } -} diff --git a/endpoints-framework/src/test/java/com/google/api/server/spi/ServiceExceptionTest.java b/endpoints-framework/src/test/java/com/google/api/server/spi/ServiceExceptionTest.java index ccc19143..aea5f576 100644 --- a/endpoints-framework/src/test/java/com/google/api/server/spi/ServiceExceptionTest.java +++ b/endpoints-framework/src/test/java/com/google/api/server/spi/ServiceExceptionTest.java @@ -1,15 +1,23 @@ package com.google.api.server.spi; import static com.google.common.truth.Truth.assertThat; +import static java.lang.Boolean.TRUE; +import com.google.api.server.spi.response.BadRequestException; +import com.google.api.server.spi.response.ConflictException; import com.google.api.server.spi.response.UnauthorizedException; + +import java.util.Map; import java.util.logging.Level; + +import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @RunWith(JUnit4.class) public class ServiceExceptionTest { + @Test public void testWithLogLevel() { UnauthorizedException ex = new UnauthorizedException(""); @@ -17,4 +25,63 @@ public void testWithLogLevel() { assertThat(ServiceException.withLogLevel(ex, Level.WARNING).getLogLevel()) .isEqualTo(Level.WARNING); } -} \ No newline at end of file + + @Test + public void testExtraFields() { + UnauthorizedException ex = new UnauthorizedException(""); + ex.putExtraField("isAdmin", TRUE) + .putExtraField("userId", 12) + .putExtraField("userName", "John Doe"); + Map extraFields = ex.getExtraFields(); + assertThat(extraFields.size()).isEqualTo(3); + assertThat(extraFields.get("isAdmin")).isEqualTo(TRUE); + assertThat(extraFields.get("userId")).isEqualTo(12); + assertThat(extraFields.get("userName")).isEqualTo("John Doe"); + } + + @Test(expected = NullPointerException.class) + public void testExtraFields_nameNull() { + new BadRequestException("").putExtraField(null, "value not null"); + } + + @Test + public void testExtraFields_valueNull_allowed() { + UnauthorizedException ex = new UnauthorizedException(""); + ex.putExtraField("isAdmin", (String) null); + Map extraFields = ex.getExtraFields(); + assertThat(extraFields.size()).isEqualTo(1); + assertThat(extraFields.get("isAdmin")).isNull(); + } + + @Test + public void testExtraFields_overrideValue_keepLast() { + UnauthorizedException ex = new UnauthorizedException(""); + ex.putExtraField("isAdmin", "YES"); + ex.putExtraField("isAdmin", TRUE); + Map extraFields = ex.getExtraFields(); + assertThat(extraFields.size()).isEqualTo(1); + assertThat(extraFields.get("isAdmin")).isEqualTo(TRUE); + } + + @Test + public void testExtraFields_ReservedNameDomain_forbidden() { + assertExtraFields_ReservedName_forbidden("domain"); + } + + @Test + public void testExtraFields_ReservedNameMessage_forbidden() { + assertExtraFields_ReservedName_forbidden("message"); + } + + @Test + public void testExtraFields_ReservedNameReason_forbidden() { + assertExtraFields_ReservedName_forbidden("reason"); + } + + private void assertExtraFields_ReservedName_forbidden(String fieldName) { + IllegalArgumentException e = Assert.assertThrows(IllegalArgumentException.class, () -> + new ConflictException("Fails", "no extra " + fieldName).putExtraField(fieldName, "some other " + fieldName) + ); + assertThat(e.getMessage()).contains("The field name '" + fieldName + "' is reserved"); + } +} diff --git a/endpoints-framework/src/test/java/com/google/api/server/spi/ServletInitializationParametersTest.java b/endpoints-framework/src/test/java/com/google/api/server/spi/ServletInitializationParametersTest.java index b4cbb19a..cde4348c 100644 --- a/endpoints-framework/src/test/java/com/google/api/server/spi/ServletInitializationParametersTest.java +++ b/endpoints-framework/src/test/java/com/google/api/server/spi/ServletInitializationParametersTest.java @@ -43,50 +43,55 @@ public void testBuilder_defaults() { ServletInitializationParameters initParameters = ServletInitializationParameters.builder() .build(); assertThat(initParameters.getServiceClasses()).isEmpty(); - assertThat(initParameters.isServletRestricted()).isTrue(); assertThat(initParameters.isClientIdWhitelistEnabled()).isTrue(); assertThat(initParameters.isIllegalArgumentBackendError()).isFalse(); assertThat(initParameters.isExceptionCompatibilityEnabled()).isTrue(); assertThat(initParameters.isPrettyPrintEnabled()).isTrue(); assertThat(initParameters.isAddContentLength()).isFalse(); - verifyAsMap(initParameters, "", "true", "true", "false", "true", "true", "false"); + assertThat(initParameters.getApiExplorerUrlTemplate()).isNull(); + assertThat(initParameters.isParameterValidationEnabled()).isTrue(); + assertThat(initParameters.isContentTypeValidationEnabled()).isFalse(); + verifyAsMap(initParameters, "", "true", "false", "true", "true", "false", null, "true", "false"); } @Test public void testBuilder_emptySetsAndTrue() { ServletInitializationParameters initParameters = ServletInitializationParameters.builder() .setClientIdWhitelistEnabled(true) - .setRestricted(true) .addServiceClasses(ImmutableSet.>of()) .setIllegalArgumentBackendError(true) .setExceptionCompatibilityEnabled(true) .setPrettyPrintEnabled(true) .setAddContentLength(true) + .setApiExplorerUrlTemplate("apiExplorer") + .setParameterValidationEnabled(true) + .setContentTypeValidationEnabled(true) .build(); assertThat(initParameters.getServiceClasses()).isEmpty(); - assertThat(initParameters.isServletRestricted()).isTrue(); assertThat(initParameters.isClientIdWhitelistEnabled()).isTrue(); assertThat(initParameters.isIllegalArgumentBackendError()).isTrue(); assertThat(initParameters.isExceptionCompatibilityEnabled()).isTrue(); - verifyAsMap(initParameters, "", "true", "true", "true", "true", "true", "true"); + assertThat(initParameters.getApiExplorerUrlTemplate()).isEqualTo("apiExplorer"); + assertThat(initParameters.isParameterValidationEnabled()).isTrue(); + verifyAsMap(initParameters, "", "true", "true", "true", "true", "true", "apiExplorer", "true", "true"); } @Test public void testBuilder_oneEntrySetsAndFalse() { ServletInitializationParameters initParameters = ServletInitializationParameters.builder() - .setRestricted(false) .addServiceClass(String.class) .setClientIdWhitelistEnabled(false) .setIllegalArgumentBackendError(false) .setExceptionCompatibilityEnabled(false) .setPrettyPrintEnabled(false) .setAddContentLength(false) + .setParameterValidationEnabled(false) + .setContentTypeValidationEnabled(false) .build(); assertThat(initParameters.getServiceClasses()).containsExactly(String.class); - assertThat(initParameters.isServletRestricted()).isFalse(); assertThat(initParameters.isClientIdWhitelistEnabled()).isFalse(); verifyAsMap( - initParameters, String.class.getName(), "false", "false", "false", "false", "false","false"); + initParameters, String.class.getName(), "false", "false", "false", "false","false", null, "false", "false"); } @Test @@ -95,8 +100,8 @@ public void testBuilder_twoEntrySets() { .addServiceClasses(ImmutableSet.of(String.class, Integer.class)) .build(); assertThat(initParameters.getServiceClasses()).containsExactly(String.class, Integer.class); - verifyAsMap(initParameters, String.class.getName() + ',' + Integer.class.getName(), "true", - "true", "false", "true", "true", "false"); + verifyAsMap(initParameters, String.class.getName() + ',' + Integer.class.getName(), + "true", "false", "true", "true", "false", null, "true", "false"); } @Test @@ -104,65 +109,70 @@ public void testFromServletConfig_nullConfig() throws ServletException { ServletInitializationParameters initParameters = ServletInitializationParameters.fromServletConfig(null, getClass().getClassLoader()); assertThat(initParameters.getServiceClasses()).isEmpty(); - assertThat(initParameters.isServletRestricted()).isTrue(); assertThat(initParameters.isClientIdWhitelistEnabled()).isTrue(); } @Test public void testFromServletConfig_nullValues() throws ServletException { ServletInitializationParameters initParameters = - fromServletConfig(null, null, null, null, null, null, null); + fromServletConfig(null, null, null, null, null, null, null, null, null); assertThat(initParameters.getServiceClasses()).isEmpty(); - assertThat(initParameters.isServletRestricted()).isTrue(); assertThat(initParameters.isClientIdWhitelistEnabled()).isTrue(); assertThat(initParameters.isIllegalArgumentBackendError()).isFalse(); assertThat(initParameters.isExceptionCompatibilityEnabled()).isTrue(); assertThat(initParameters.isPrettyPrintEnabled()).isTrue(); + assertThat(initParameters.isParameterValidationEnabled()).isTrue(); + assertThat(initParameters.isContentTypeValidationEnabled()).isFalse(); + assertThat(initParameters.getApiExplorerUrlTemplate()).isNull(); } @Test public void testFromServletConfig_emptySetsAndFalse() throws ServletException { ServletInitializationParameters initParameters = - fromServletConfig("", "false", "false", "false", "false", "false", "false"); + fromServletConfig("", "false", "false", "false", "false", "false", null, "false", "false"); assertThat(initParameters.getServiceClasses()).isEmpty(); - assertThat(initParameters.isServletRestricted()).isFalse(); assertThat(initParameters.isClientIdWhitelistEnabled()).isFalse(); assertThat(initParameters.isIllegalArgumentBackendError()).isFalse(); assertThat(initParameters.isExceptionCompatibilityEnabled()).isFalse(); assertThat(initParameters.isPrettyPrintEnabled()).isFalse(); + assertThat(initParameters.isParameterValidationEnabled()).isFalse(); + assertThat(initParameters.isContentTypeValidationEnabled()).isFalse(); + assertThat(initParameters.getApiExplorerUrlTemplate()).isNull(); } @Test public void testFromServletConfig_oneEntrySetsAndTrue() throws ServletException { ServletInitializationParameters initParameters = - fromServletConfig(String.class.getName(), "true", "true", "true", "true", "true", "true"); + fromServletConfig(String.class.getName(), "true", "true", "true", "true", "true", null, "true", "false"); assertThat(initParameters.getServiceClasses()).containsExactly(String.class); - assertThat(initParameters.isServletRestricted()).isTrue(); assertThat(initParameters.isClientIdWhitelistEnabled()).isTrue(); assertThat(initParameters.isIllegalArgumentBackendError()).isTrue(); assertThat(initParameters.isExceptionCompatibilityEnabled()).isTrue(); assertThat(initParameters.isPrettyPrintEnabled()).isTrue(); + assertThat(initParameters.isParameterValidationEnabled()).isTrue(); + assertThat(initParameters.isContentTypeValidationEnabled()).isFalse(); + assertThat(initParameters.getApiExplorerUrlTemplate()).isNull(); } @Test public void testFromServletConfig_twoEntrySets() throws ServletException { ServletInitializationParameters initParameters = fromServletConfig( - String.class.getName() + ',' + Integer.class.getName(), null, null, null, null, null, null); + String.class.getName() + ',' + Integer.class.getName(), null, null, null, null, null, null, null, null); assertThat(initParameters.getServiceClasses()).containsExactly(String.class, Integer.class); } @Test public void testFromServletConfig_skipsEmptyElements() throws ServletException { ServletInitializationParameters initParameters = fromServletConfig( - ",," + String.class.getName() + ",,," + Integer.class.getName() + ",", null, null, null, - null, null, null); + ",," + String.class.getName() + ",,," + Integer.class.getName() + ",", null, null, + null, null, null, null, null, null); assertThat(initParameters.getServiceClasses()).containsExactly(String.class, Integer.class); } @Test - public void testFromServletConfig_invalidRestrictedThrows() throws ServletException { + public void testFromServletConfig_invalidBooleanThrows() throws ServletException { try { - fromServletConfig(null, "yes", null, null, null, null, null); + fromServletConfig(null, "yes", null, null, null, null, null, null, null); fail("Expected IllegalArgumentException"); } catch (IllegalArgumentException expected) { // expected @@ -171,29 +181,34 @@ public void testFromServletConfig_invalidRestrictedThrows() throws ServletExcept private void verifyAsMap( ServletInitializationParameters initParameters, String serviceClasses, - String isServletRestricted, String isClientIdWhitelistEnabled, + String isClientIdWhitelistEnabled, String isIllegalArgumentBackendError, String isExceptionCompatibilityEnabled, - String isPrettyPrintEnabled, String isAddContentLength) { + String isPrettyPrintEnabled, String isAddContentLength, String apiExplorerUrlTemplate, + String isParameterValidationEnabled, String isContentTypeValidationEnabled) { Map map = initParameters.asMap(); - assertThat(map).hasSize(7); + assertThat(map).hasSize(9); assertThat(map.get("services")).isEqualTo(serviceClasses); - assertThat(map.get("restricted")).isEqualTo(isServletRestricted); assertThat(map.get("clientIdWhitelistEnabled")).isEqualTo(isClientIdWhitelistEnabled); assertThat(map.get("illegalArgumentIsBackendError")).isEqualTo(isIllegalArgumentBackendError); assertThat(map.get("enableExceptionCompatibility")).isEqualTo(isExceptionCompatibilityEnabled); assertThat(map.get("prettyPrint")).isEqualTo(isPrettyPrintEnabled); assertThat(map.get("addContentLength")).isEqualTo(isAddContentLength); + assertThat(map.get("enableValidation")).isEqualTo(isParameterValidationEnabled); + assertThat(map.get("enableContentTypeValidation")).isEqualTo(isContentTypeValidationEnabled); + assertThat(map.get("apiExplorerUrlTemplate")).isEqualTo(apiExplorerUrlTemplate); } private ServletInitializationParameters fromServletConfig( - String serviceClasses, String isServletRestricted, + String serviceClasses, String isClientIdWhitelistEnabled, String isIllegalArgumentBackendError, String isExceptionCompatibilityEnabled, String isPrettyPrintEnabled, - String isAddContentLength) + String isAddContentLength, String apiExplorerUrlTemplate, + String isParameterValidationEnabled, String isContentTypeValidationEnabled) throws ServletException { ServletConfig servletConfig = new StubServletConfig(serviceClasses, - isServletRestricted, isClientIdWhitelistEnabled, isIllegalArgumentBackendError, - isExceptionCompatibilityEnabled, isPrettyPrintEnabled, isAddContentLength); + isClientIdWhitelistEnabled, isIllegalArgumentBackendError, + isExceptionCompatibilityEnabled, isPrettyPrintEnabled, isAddContentLength, + apiExplorerUrlTemplate, isParameterValidationEnabled, isContentTypeValidationEnabled); return ServletInitializationParameters.fromServletConfig( servletConfig, getClass().getClassLoader()); } @@ -202,17 +217,20 @@ private static class StubServletConfig implements ServletConfig { private final Map initParameters; public StubServletConfig( - String serviceClasses, String isServletRestricted, String isClientIdWhitelistEnabled, + String serviceClasses, String isClientIdWhitelistEnabled, String isIllegalArgumentBackendError, String isExceptionCompatibilityEnabled, - String isPrettyPrintEnabled, String isAddContentLength) { + String isPrettyPrintEnabled, String isAddContentLength, String apiExplorerUrlTemplate, + String isParameterValidationEnabled, String isContentTypeValidationEnabled) { initParameters = Maps.newHashMap(); initParameters.put("services", serviceClasses); - initParameters.put("restricted", isServletRestricted); initParameters.put("clientIdWhitelistEnabled", isClientIdWhitelistEnabled); initParameters.put("illegalArgumentIsBackendError", isIllegalArgumentBackendError); initParameters.put("enableExceptionCompatibility", isExceptionCompatibilityEnabled); initParameters.put("prettyPrint", isPrettyPrintEnabled); initParameters.put("addContentLength", isAddContentLength); + initParameters.put("apiExplorerUrlTemplate", apiExplorerUrlTemplate); + initParameters.put("enableValidation", isParameterValidationEnabled); + initParameters.put("enableContentTypeValidation", isContentTypeValidationEnabled); } @Override diff --git a/endpoints-framework/src/test/java/com/google/api/server/spi/auth/EndpointsAuthenticatorTest.java b/endpoints-framework/src/test/java/com/google/api/server/spi/auth/EndpointsAuthenticatorTest.java index f9ce9db5..ec22cc2a 100644 --- a/endpoints-framework/src/test/java/com/google/api/server/spi/auth/EndpointsAuthenticatorTest.java +++ b/endpoints-framework/src/test/java/com/google/api/server/spi/auth/EndpointsAuthenticatorTest.java @@ -27,7 +27,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import org.springframework.mock.web.MockHttpServletRequest; /** diff --git a/endpoints-framework/src/test/java/com/google/api/server/spi/auth/EndpointsPeerAuthenticatorTest.java b/endpoints-framework/src/test/java/com/google/api/server/spi/auth/EndpointsPeerAuthenticatorTest.java deleted file mode 100644 index e81d8785..00000000 --- a/endpoints-framework/src/test/java/com/google/api/server/spi/auth/EndpointsPeerAuthenticatorTest.java +++ /dev/null @@ -1,184 +0,0 @@ -/* - * Copyright 2016 Google Inc. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.api.server.spi.auth; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; -import static org.mockito.Mockito.when; - -import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken; -import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken.Payload; -import com.google.api.server.spi.EnvUtil; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; -import org.springframework.mock.web.MockHttpServletRequest; - -import java.net.InetAddress; - -/** - * Tests for {@code EndpointsPeerAuthenticator}. - */ -@RunWith(MockitoJUnitRunner.class) -public class EndpointsPeerAuthenticatorTest { - private static final String FAKE_TOKEN = "fakeToken"; - - @Mock private GoogleJwtAuthenticator jwtAuthenticator; - @Mock private GoogleIdToken token; - - private final Payload payload = new Payload(); - private MockHttpServletRequest request; - private EndpointsPeerAuthenticator authenticator; - - @Before - public void setUp() throws Exception { - System.clearProperty(EnvUtil.ENV_APPENGINE_RUNTIME); - authenticator = new EndpointsPeerAuthenticator(jwtAuthenticator); - request = new MockHttpServletRequest(); - request.setRemoteAddr("8.8.8.8"); - } - - @After - public void tearDown() throws Exception { - EnvUtil.recoverAppEngineRuntime(); - } - - @Test - public void testAuthenticate_localHost() throws Exception { - request.setRemoteAddr(InetAddress.getLocalHost().getHostAddress()); - assertTrue(authenticator.authenticate(request)); - } - - @Test - public void testAuthenticate_localHostIp() { - request.setRemoteAddr("127.0.0.1"); - assertTrue(authenticator.authenticate(request)); - } - - @Test - public void testAuthenticate_appEngineRunTimeNoXAppengineHeader() { - System.setProperty(EnvUtil.ENV_APPENGINE_RUNTIME, "Production"); - assertFalse(authenticator.authenticate(request)); - } - - @Test - public void testAuthenticate_appEngineRunTimeUnmatchedXAppengineHeader() { - System.setProperty(EnvUtil.ENV_APPENGINE_RUNTIME, "Production"); - request.addHeader(EndpointsPeerAuthenticator.APPENGINE_PEER, "invalid"); - assertFalse(authenticator.authenticate(request)); - } - - @Test - public void testAuthenticate_appEngineRunTimeSuccess() { - System.setProperty(EnvUtil.ENV_APPENGINE_RUNTIME, "Production"); - request.addHeader(EndpointsPeerAuthenticator.HEADER_APPENGINE_PEER, - EndpointsPeerAuthenticator.APPENGINE_PEER); - assertTrue(authenticator.authenticate(request)); - } - - @Test - public void testAuthenticate_noPeerAuthorizationHeader() { - assertFalse(authenticator.authenticate(request)); - } - - @Test - public void testAuthenticate_invalidHeader() { - request.addHeader(EndpointsPeerAuthenticator.HEADER_PEER_AUTHORIZATION, FAKE_TOKEN); - when(jwtAuthenticator.verifyToken(FAKE_TOKEN)).thenReturn(null); - assertFalse(authenticator.authenticate(request)); - } - - @Test - public void testAuthenticate_invalidEmail() { - payload.setEmail("invalid@gmail.com"); - request.addHeader(EndpointsPeerAuthenticator.HEADER_PEER_AUTHORIZATION, FAKE_TOKEN); - when(jwtAuthenticator.verifyToken(FAKE_TOKEN)).thenReturn(token); - when(token.getPayload()).thenReturn(payload); - assertFalse(authenticator.authenticate(request)); - } - - @Test - public void testAuthenticate_unmatchedHost() { - payload.setEmail(EndpointsPeerAuthenticator.SIGNER); - payload.setAudience("http://otherhost.com/api"); - request.addHeader(EndpointsPeerAuthenticator.HEADER_PEER_AUTHORIZATION, FAKE_TOKEN); - request.addHeader("Host", "myhost.com"); - when(jwtAuthenticator.verifyToken(FAKE_TOKEN)).thenReturn(token); - when(token.getPayload()).thenReturn(payload); - assertFalse(authenticator.authenticate(request)); - } - - @Test - public void testAuthenticate_unmatchedPortDefault() { - payload.setEmail(EndpointsPeerAuthenticator.SIGNER); - payload.setAudience("https://myhost.com/api"); - request.addHeader(EndpointsPeerAuthenticator.HEADER_PEER_AUTHORIZATION, FAKE_TOKEN); - request.addHeader("Host", "myhost.com"); - when(jwtAuthenticator.verifyToken(FAKE_TOKEN)).thenReturn(token); - when(token.getPayload()).thenReturn(payload); - assertFalse(authenticator.authenticate(request)); - } - - @Test - public void testAuthenticate_unmatchedPort() { - request = createRequest("myhost.com", 456, "", "", ""); - request.setRemoteAddr("8.8.8.8"); - payload.setEmail(EndpointsPeerAuthenticator.SIGNER); - payload.setAudience("http://otherhost.com:789/api"); - request.addHeader(EndpointsPeerAuthenticator.HEADER_PEER_AUTHORIZATION, FAKE_TOKEN); - when(jwtAuthenticator.verifyToken(FAKE_TOKEN)).thenReturn(token); - when(token.getPayload()).thenReturn(payload); - assertFalse(authenticator.authenticate(request)); - } - - @Test - public void testAuthenticate_success() { - request = createRequest("myhost.com", 456, "", "", ""); - request.setRemoteAddr("8.8.8.8"); - payload.setEmail(EndpointsPeerAuthenticator.SIGNER); - payload.setAudience("http://myhost.com:456/api"); - request.addHeader(EndpointsPeerAuthenticator.HEADER_PEER_AUTHORIZATION, FAKE_TOKEN); - when(jwtAuthenticator.verifyToken(FAKE_TOKEN)).thenReturn(token); - when(token.getPayload()).thenReturn(payload); - assertTrue(authenticator.authenticate(request)); - } - - @Test - public void testNewInstance() { - try { - authenticator = EndpointsPeerAuthenticator.class.newInstance(); - } catch (Exception e) { - fail("newInstance on EndpointsPeerAuthenticator.class failed"); - } - } - - private static MockHttpServletRequest createRequest( - String host, int port, String servletPath, String contextPath, String queryString) { - MockHttpServletRequest request = new MockHttpServletRequest(); - request.addHeader("Host", host); - request.setServerName(host); - request.setServerPort(port); - request.setServletPath(servletPath); - request.setQueryString(queryString); - request.setContextPath(contextPath); - return request; - } -} diff --git a/endpoints-framework/src/test/java/com/google/api/server/spi/auth/GoogleAppEngineAuthenticatorTest.java b/endpoints-framework/src/test/java/com/google/api/server/spi/auth/GoogleAppEngineAuthenticatorTest.java index 3265bb83..6eec4554 100644 --- a/endpoints-framework/src/test/java/com/google/api/server/spi/auth/GoogleAppEngineAuthenticatorTest.java +++ b/endpoints-framework/src/test/java/com/google/api/server/spi/auth/GoogleAppEngineAuthenticatorTest.java @@ -34,7 +34,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import org.springframework.mock.web.MockHttpServletRequest; import javax.servlet.http.HttpServletRequest; @@ -147,7 +147,6 @@ public void testGetOAuth2UserSkipClientIdCheck() throws Exception { when(config.getScopeExpression()).thenReturn(AuthScopeExpressions.interpret(SCOPES)); when(oauthService.getAuthorizedScopes(SCOPES)).thenReturn(SCOPES); when(oauthService.getClientId(SCOPES)).thenReturn(CLIENT_ID); - when(config.getClientIds()).thenReturn(ImmutableList.of("clienId2")); when(oauthService.getCurrentUser(SCOPES)).thenReturn(APP_ENGINE_USER); assertEquals(APP_ENGINE_USER, authenticator.getOAuth2User(request, config)); } diff --git a/endpoints-framework/src/test/java/com/google/api/server/spi/auth/GoogleJwtAuthenticatorTest.java b/endpoints-framework/src/test/java/com/google/api/server/spi/auth/GoogleJwtAuthenticatorTest.java index e67140d7..fcaa9822 100644 --- a/endpoints-framework/src/test/java/com/google/api/server/spi/auth/GoogleJwtAuthenticatorTest.java +++ b/endpoints-framework/src/test/java/com/google/api/server/spi/auth/GoogleJwtAuthenticatorTest.java @@ -21,7 +21,6 @@ import static org.mockito.Mockito.when; import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken; -import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken.Payload; import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier; import com.google.api.server.spi.auth.common.User; import com.google.api.server.spi.config.model.ApiMethodConfig; @@ -32,7 +31,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import org.springframework.mock.web.MockHttpServletRequest; import java.io.IOException; @@ -45,25 +44,27 @@ public class GoogleJwtAuthenticatorTest { private static final String TOKEN = "abcdefjh.abcdefjh.abcdefjh"; private static final String EMAIL = "dummy@gmail.com"; + private static final String PRIMARY_EMAIL = "primary@gmail.com"; private static final String CLIENT_ID = "clientId1"; private static final String AUDIENCE = "audience1"; private static final String USER_ID = "1234567"; - private Payload payload; + private GoogleCustomIdToken.Payload payload; private GoogleJwtAuthenticator authenticator; private MockHttpServletRequest request; private Attribute attr; - @Mock private GoogleIdTokenVerifier verifier; - @Mock private GoogleIdToken token; + @Mock private GoogleCustomIdTokenVerifier verifier; + @Mock private GoogleCustomIdToken token; @Mock protected ApiMethodConfig config; @Before public void setUp() throws Exception { - payload = new Payload(); + payload = new GoogleCustomIdToken.Payload(); payload.setAuthorizedParty(CLIENT_ID); payload.setAudience(AUDIENCE); payload.setEmail(EMAIL); + payload.setPrimaryEmail(PRIMARY_EMAIL); payload.setSubject(USER_ID); authenticator = new GoogleJwtAuthenticator(verifier); request = new MockHttpServletRequest(); @@ -129,7 +130,6 @@ public void testAuthenticate_audienceNotAllowed() throws Exception { public void testAuthenticate_skipClientIdCheck() throws Exception { request.removeAttribute(Attribute.ENABLE_CLIENT_ID_WHITELIST); when(verifier.verify(TOKEN)).thenReturn(token); - when(config.getClientIds()).thenReturn(ImmutableList.of("clientId2")); when(config.getAudiences()).thenReturn(ImmutableList.of(AUDIENCE)); User user = authenticator.authenticate(request); assertEquals(EMAIL, user.getEmail()); diff --git a/endpoints-framework/src/test/java/com/google/api/server/spi/auth/GoogleOAuth2AuthenticatorTest.java b/endpoints-framework/src/test/java/com/google/api/server/spi/auth/GoogleOAuth2AuthenticatorTest.java index b05839fa..a82420ad 100644 --- a/endpoints-framework/src/test/java/com/google/api/server/spi/auth/GoogleOAuth2AuthenticatorTest.java +++ b/endpoints-framework/src/test/java/com/google/api/server/spi/auth/GoogleOAuth2AuthenticatorTest.java @@ -32,7 +32,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import org.springframework.mock.web.MockHttpServletRequest; /** @@ -106,7 +106,6 @@ public void testAuthenticate_clientIdNotAllowed() throws ServiceUnavailableExcep public void testAuthenticate_skipClientIdCheck() throws ServiceUnavailableException { request.removeAttribute(Attribute.ENABLE_CLIENT_ID_WHITELIST); when(config.getScopeExpression()).thenReturn(AuthScopeExpressions.interpret("scope1")); - when(config.getClientIds()).thenReturn(ImmutableList.of("clientId2")); User user = authenticator.authenticate(request); assertEquals(EMAIL, user.getEmail()); assertEquals(USER_ID, user.getId()); diff --git a/endpoints-framework/src/test/java/com/google/api/server/spi/config/ApiConfigLoaderTest.java b/endpoints-framework/src/test/java/com/google/api/server/spi/config/ApiConfigLoaderTest.java index 700251c7..bb88ac90 100644 --- a/endpoints-framework/src/test/java/com/google/api/server/spi/config/ApiConfigLoaderTest.java +++ b/endpoints-framework/src/test/java/com/google/api/server/spi/config/ApiConfigLoaderTest.java @@ -34,7 +34,7 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.Mockito; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; /** * Tests for {@link ApiConfigLoader}. diff --git a/endpoints-framework/src/test/java/com/google/api/server/spi/config/annotationreader/ApiAnnotationConfigTest.java b/endpoints-framework/src/test/java/com/google/api/server/spi/config/annotationreader/ApiAnnotationConfigTest.java index 07bc8f23..38ed1854 100644 --- a/endpoints-framework/src/test/java/com/google/api/server/spi/config/annotationreader/ApiAnnotationConfigTest.java +++ b/endpoints-framework/src/test/java/com/google/api/server/spi/config/annotationreader/ApiAnnotationConfigTest.java @@ -26,13 +26,11 @@ import com.google.api.server.spi.config.Api; import com.google.api.server.spi.config.AuthLevel; import com.google.api.server.spi.config.Authenticator; -import com.google.api.server.spi.config.PeerAuthenticator; import com.google.api.server.spi.config.model.ApiConfig; import com.google.api.server.spi.config.model.ApiMethodConfig; import com.google.api.server.spi.config.scope.AuthScopeExpression; import com.google.api.server.spi.config.scope.AuthScopeExpressions; import com.google.api.server.spi.testing.PassAuthenticator; -import com.google.api.server.spi.testing.PassPeerAuthenticator; import com.google.api.server.spi.testing.TestEndpoint; import com.google.common.collect.ImmutableList; @@ -382,21 +380,6 @@ public void testSetAuthenticatorIfSpecified_unspecified() throws Exception { assertNull(config.getAuthenticators()); } - @Test - public void testSetPeerAuthenticatorIfSpecified() throws Exception { - annotationConfig.setPeerAuthenticatorsIfSpecified(PassPeerAuthenticator.testArray); - assertEquals(Arrays.asList(PassPeerAuthenticator.testArray), config.getPeerAuthenticators()); - } - - // Unchecked cast needed to get a generic array type. - @SuppressWarnings("unchecked") - public void testSetPeerAuthenticatorIfSpecified_unspecified() throws Exception { - Class[] peerAuthenticators = {PeerAuthenticator.class}; - annotationConfig.setPeerAuthenticatorsIfSpecified( - (Class[]) peerAuthenticators); - assertNull(config.getPeerAuthenticators()); - } - private EndpointMethod getResultNoParamsMethod() throws NoSuchMethodException, SecurityException { return getSimpleEndpointMethod(TestEndpoint.class.getMethod("getResultNoParams")); } diff --git a/endpoints-framework/src/test/java/com/google/api/server/spi/config/annotationreader/ApiAnnotationIntrospectorTest.java b/endpoints-framework/src/test/java/com/google/api/server/spi/config/annotationreader/ApiAnnotationIntrospectorTest.java index 05ded3fd..6b7d769d 100644 --- a/endpoints-framework/src/test/java/com/google/api/server/spi/config/annotationreader/ApiAnnotationIntrospectorTest.java +++ b/endpoints-framework/src/test/java/com/google/api/server/spi/config/annotationreader/ApiAnnotationIntrospectorTest.java @@ -593,7 +593,7 @@ public TestResourceWithCustomSerializer transformFrom(Map in) { try { obj = clazz.newInstance(); } catch (Exception e) { - Throwables.propagate(e); + throw new RuntimeException(e); } obj.point = String.format("%d,%d", MoreObjects.firstNonNull(in.get("x"), 0), diff --git a/endpoints-framework/src/test/java/com/google/api/server/spi/config/annotationreader/ApiClassAnnotationConfigTest.java b/endpoints-framework/src/test/java/com/google/api/server/spi/config/annotationreader/ApiClassAnnotationConfigTest.java index f5305aae..27709914 100644 --- a/endpoints-framework/src/test/java/com/google/api/server/spi/config/annotationreader/ApiClassAnnotationConfigTest.java +++ b/endpoints-framework/src/test/java/com/google/api/server/spi/config/annotationreader/ApiClassAnnotationConfigTest.java @@ -19,19 +19,17 @@ import com.google.api.server.spi.config.Api; import com.google.api.server.spi.config.AuthLevel; import com.google.api.server.spi.config.Authenticator; -import com.google.api.server.spi.config.PeerAuthenticator; import com.google.api.server.spi.config.model.ApiClassConfig; import com.google.api.server.spi.config.scope.AuthScopeExpression; import com.google.api.server.spi.config.scope.AuthScopeExpressions; import com.google.api.server.spi.testing.PassAuthenticator; -import com.google.api.server.spi.testing.PassPeerAuthenticator; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.Mockito; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import java.util.Arrays; @@ -58,7 +56,7 @@ public void testSetResourceIfNotEmpty() { @Test public void testSetResourceIfNotEmpty_empty() { annotationConfig.setResourceIfNotEmpty(""); - Mockito.verifyZeroInteractions(config); + Mockito.verifyNoInteractions(config); annotationConfig.setResourceIfNotEmpty("bleh"); annotationConfig.setResourceIfNotEmpty(""); @@ -80,7 +78,7 @@ public void testSetAuthLevelIfSpecified() throws Exception { @Test public void testSetAuthLevelIfSpecified_unspecified() throws Exception { annotationConfig.setAuthLevelIfSpecified(AuthLevel.UNSPECIFIED); - Mockito.verifyZeroInteractions(config); + Mockito.verifyNoInteractions(config); } @Test @@ -100,11 +98,11 @@ public void testSetScopesIfSpecified_empty() throws Exception { @Test public void testSetScopesIfSpecified_unspecified() throws Exception { annotationConfig.setScopesIfSpecified(null); - Mockito.verifyZeroInteractions(config); + Mockito.verifyNoInteractions(config); String[] unspecified = {Api.UNSPECIFIED_STRING_FOR_LIST}; annotationConfig.setScopesIfSpecified(unspecified); - Mockito.verifyZeroInteractions(config); + Mockito.verifyNoInteractions(config); String[] scopes = { "bleh", "more bleh" }; annotationConfig.setScopesIfSpecified(scopes); @@ -130,11 +128,11 @@ public void testSetAudiencesIfSpecified_empty() throws Exception { @Test public void testSetAudiencesIfSpecified_unspecified() throws Exception { annotationConfig.setAudiencesIfSpecified(null); - Mockito.verifyZeroInteractions(config); + Mockito.verifyNoInteractions(config); String[] unspecified = {Api.UNSPECIFIED_STRING_FOR_LIST}; annotationConfig.setAudiencesIfSpecified(unspecified); - Mockito.verifyZeroInteractions(config); + Mockito.verifyNoInteractions(config); String[] audiences = {"bleh", "more bleh"}; annotationConfig.setAudiencesIfSpecified(audiences); @@ -160,11 +158,11 @@ public void testSetClientIdsIfSpecified_empty() throws Exception { @Test public void testSetClientIdsIfSpecified_unspecified() throws Exception { annotationConfig.setClientIdsIfSpecified(null); - Mockito.verifyZeroInteractions(config); + Mockito.verifyNoInteractions(config); String[] unspecified = {Api.UNSPECIFIED_STRING_FOR_LIST}; annotationConfig.setClientIdsIfSpecified(unspecified); - Mockito.verifyZeroInteractions(config); + Mockito.verifyNoInteractions(config); String[] clientIds = {"bleh", "more bleh"}; annotationConfig.setClientIdsIfSpecified(clientIds); @@ -185,22 +183,7 @@ public void testSetAuthenticatorIfSpecified_unspecified() throws Exception { Class[] authenticators = {Authenticator.class}; annotationConfig.setAuthenticatorsIfSpecified( (Class[]) authenticators); - Mockito.verifyZeroInteractions(config); - } - - @Test - public void testSetPeerAuthenticatorIfSpecified() throws Exception { - annotationConfig.setPeerAuthenticatorsIfSpecified(PassPeerAuthenticator.testArray); - Mockito.verify(config).setPeerAuthenticators(Arrays.asList(PassPeerAuthenticator.testArray)); - } - - // Unchecked cast needed to get a generic array type. - @SuppressWarnings("unchecked") - public void testSetPeerAuthenticatorIfSpecified_unspecified() throws Exception { - Class[] peerAuthenticators = {PeerAuthenticator.class}; - annotationConfig.setPeerAuthenticatorsIfSpecified( - (Class[]) peerAuthenticators); - Mockito.verifyZeroInteractions(config); + Mockito.verifyNoInteractions(config); } @Test diff --git a/endpoints-framework/src/test/java/com/google/api/server/spi/config/annotationreader/ApiConfigAnnotationReaderTest.java b/endpoints-framework/src/test/java/com/google/api/server/spi/config/annotationreader/ApiConfigAnnotationReaderTest.java index a11087d9..1d2ff8ce 100644 --- a/endpoints-framework/src/test/java/com/google/api/server/spi/config/annotationreader/ApiConfigAnnotationReaderTest.java +++ b/endpoints-framework/src/test/java/com/google/api/server/spi/config/annotationreader/ApiConfigAnnotationReaderTest.java @@ -67,11 +67,9 @@ import com.google.api.server.spi.testing.Endpoint3; import com.google.api.server.spi.testing.Endpoint4; import com.google.api.server.spi.testing.FailAuthenticator; -import com.google.api.server.spi.testing.FailPeerAuthenticator; import com.google.api.server.spi.testing.Foo; import com.google.api.server.spi.testing.InterfaceReferenceEndpoint; import com.google.api.server.spi.testing.PassAuthenticator; -import com.google.api.server.spi.testing.PassPeerAuthenticator; import com.google.api.server.spi.testing.ReferenceOverridingEndpoint; import com.google.api.server.spi.testing.RestfulResourceEndpointBase; import com.google.api.server.spi.testing.SimpleBean; @@ -105,6 +103,7 @@ /** * Tests for {@link ApiConfigAnnotationReader}. + * TODO test deprecation */ @RunWith(JUnit4.class) public class ApiConfigAnnotationReaderTest { @@ -149,7 +148,6 @@ public void testBasicEndpoint() throws Exception { DEFAULT_SCOPES, DEFAULT_AUDIENCES, DEFAULT_CLIENTIDS, - null, null); } @@ -193,8 +191,7 @@ public void testEndpointWithInheritance() throws Exception { defaultScopes, defaultAudiences, defaultClientIds, - ImmutableList.of(PassAuthenticator.class), - ImmutableList.of(PassPeerAuthenticator.class)); + ImmutableList.of(PassAuthenticator.class)); assertEquals(1, getBar.getParameterConfigs().size()); validateParameter(getBar.getParameterConfigs().get(0), "id", false, null, String.class); } @@ -216,7 +213,6 @@ public void testEndpointWithBridgeMethods() throws Exception { DEFAULT_SCOPES, DEFAULT_AUDIENCES, DEFAULT_CLIENTIDS, - null, null); ApiMethodConfig fn2 = config.getApiClassConfig().getMethods().get(methodToEndpointMethod( BridgeInheritanceEndpoint.class.getSuperclass().getMethod("fn2"))); @@ -227,7 +223,6 @@ public void testEndpointWithBridgeMethods() throws Exception { DEFAULT_SCOPES, DEFAULT_AUDIENCES, DEFAULT_CLIENTIDS, - null, null); ApiMethodConfig bridge = config.getApiClassConfig().getMethods().get(methodToEndpointMethod( @@ -270,7 +265,6 @@ public void testSimpleOverrideEndpoint() throws Exception { DEFAULT_SCOPES, DEFAULT_AUDIENCES, DEFAULT_CLIENTIDS, - null, null); } @@ -504,8 +498,7 @@ public void testServiceWithOverridingInheritance() throws Exception { String[] expectedAudiences = { "a0", "a1" }; String[] expectedClientIds = { "c0", "c1" }; validateMethod(listFoos, "foos.list", "foos", ApiMethod.HttpMethod.GET, expectedScopes, - expectedAudiences, expectedClientIds, ImmutableList.of(FailAuthenticator.class), - ImmutableList.of(FailPeerAuthenticator.class)); + expectedAudiences, expectedClientIds, ImmutableList.of(FailAuthenticator.class)); assertEquals(0, listFoos.getParameterConfigs().size()); ApiMethodConfig getFoo = config.getApiClassConfig().getMethods().get(methodToEndpointMethod( @@ -517,8 +510,7 @@ public void testServiceWithOverridingInheritance() throws Exception { defaultScopes, defaultAudiences, defaultClientIds, - ImmutableList.of(PassAuthenticator.class), - ImmutableList.of(PassPeerAuthenticator.class)); + ImmutableList.of(PassAuthenticator.class)); assertEquals(1, getFoo.getParameterConfigs().size()); validateParameter(getFoo.getParameterConfigs().get(0), "id", false, null, String.class); @@ -531,8 +523,7 @@ public void testServiceWithOverridingInheritance() throws Exception { defaultScopes, defaultAudiences, defaultClientIds, - ImmutableList.of(PassAuthenticator.class), - ImmutableList.of(PassPeerAuthenticator.class)); + ImmutableList.of(PassAuthenticator.class)); assertEquals(1, insertFoo.getParameterConfigs().size()); validateParameter(insertFoo.getParameterConfigs().get(0), null, false, null, Foo.class); @@ -543,8 +534,7 @@ public void testServiceWithOverridingInheritance() throws Exception { defaultScopes, defaultAudiences, defaultClientIds, - ImmutableList.of(PassAuthenticator.class), - ImmutableList.of(PassPeerAuthenticator.class)); + ImmutableList.of(PassAuthenticator.class)); assertEquals(1, execute2.getParameterConfigs().size()); validateParameter(execute2.getParameterConfigs().get(0), "serialized", false, null, SimpleBean.class, DumbSerializer2.class, Integer.class); @@ -589,8 +579,7 @@ public void testServiceWithOverridingReference() throws Exception { defaultScopes, defaultAudiences, defaultClientIds, - ImmutableList.of(PassAuthenticator.class), - ImmutableList.of(PassPeerAuthenticator.class)); + ImmutableList.of(PassAuthenticator.class)); assertEquals(1, getFoo.getParameterConfigs().size()); validateParameter(getFoo.getParameterConfigs().get(0), "id", false, null, String.class); } @@ -804,7 +793,6 @@ public void method(@Named("serialized") TestBean tb) {} DEFAULT_SCOPES, DEFAULT_AUDIENCES, DEFAULT_CLIENTIDS, - null, null); validateParameter(methodConfig.getParameterConfigs().get(0), "serialized", false, null, TestBean.class, TestSerializer.class, String.class); @@ -1275,8 +1263,7 @@ private void validateEndpoint1(ApiConfig config, Class claz String[] expectedAudiences = { "a0", "a1" }; String[] expectedClientIds = { "c0", "c1" }; validateMethod(listFoos, "foos.list", "foos", ApiMethod.HttpMethod.GET, expectedScopes, - expectedAudiences, expectedClientIds, ImmutableList.of(FailAuthenticator.class), - ImmutableList.of(FailPeerAuthenticator.class)); + expectedAudiences, expectedClientIds, ImmutableList.of(FailAuthenticator.class)); assertEquals(0, listFoos.getParameterConfigs().size()); ApiMethodConfig getFoo = config.getApiClassConfig().getMethods().get(methodToEndpointMethod( @@ -1288,8 +1275,7 @@ private void validateEndpoint1(ApiConfig config, Class claz defaultScopes, defaultAudiences, defaultClientIds, - ImmutableList.of(PassAuthenticator.class), - ImmutableList.of(PassPeerAuthenticator.class)); + ImmutableList.of(PassAuthenticator.class)); assertEquals(1, getFoo.getParameterConfigs().size()); validateParameter(getFoo.getParameterConfigs().get(0), "id", false, null, String.class); @@ -1302,8 +1288,7 @@ private void validateEndpoint1(ApiConfig config, Class claz defaultScopes, defaultAudiences, defaultClientIds, - ImmutableList.of(PassAuthenticator.class), - ImmutableList.of(PassPeerAuthenticator.class)); + ImmutableList.of(PassAuthenticator.class)); assertEquals(1, insertFoo.getParameterConfigs().size()); validateParameter(insertFoo.getParameterConfigs().get(0), null, false, null, Foo.class); @@ -1316,8 +1301,7 @@ private void validateEndpoint1(ApiConfig config, Class claz defaultScopes, defaultAudiences, defaultClientIds, - ImmutableList.of(PassAuthenticator.class), - ImmutableList.of(PassPeerAuthenticator.class)); + ImmutableList.of(PassAuthenticator.class)); assertEquals(2, updateFoo.getParameterConfigs().size()); validateParameter(updateFoo.getParameterConfigs().get(0), "id", false, null, String.class); validateParameter(updateFoo.getParameterConfigs().get(1), null, false, null, Foo.class); @@ -1328,8 +1312,7 @@ private void validateEndpoint1(ApiConfig config, Class claz defaultScopes, defaultAudiences, defaultClientIds, - ImmutableList.of(PassAuthenticator.class), - ImmutableList.of(PassPeerAuthenticator.class)); + ImmutableList.of(PassAuthenticator.class)); assertEquals(1, removeFoo.getParameterConfigs().size()); validateParameter(removeFoo.getParameterConfigs().get(0), "id", false, null, String.class); @@ -1343,8 +1326,7 @@ private void validateEndpoint1(ApiConfig config, Class claz defaultScopes, defaultAudiences, defaultClientIds, - ImmutableList.of(PassAuthenticator.class), - ImmutableList.of(PassPeerAuthenticator.class)); + ImmutableList.of(PassAuthenticator.class)); assertEquals(9, execute0.getParameterConfigs().size()); validateParameter(execute0.getParameterConfigs().get(0), "id", false, null, String.class); validateParameter(execute0.getParameterConfigs().get(1), "i0", false, null, int.class); @@ -1363,8 +1345,7 @@ private void validateEndpoint1(ApiConfig config, Class claz defaultScopes, defaultAudiences, defaultClientIds, - ImmutableList.of(PassAuthenticator.class), - ImmutableList.of(PassPeerAuthenticator.class)); + ImmutableList.of(PassAuthenticator.class)); assertEquals(1, execute1.getParameterConfigs().size()); validateParameter(execute1.getParameterConfigs().get(0), null, false, null, Foo.class); @@ -1375,8 +1356,7 @@ private void validateEndpoint1(ApiConfig config, Class claz defaultScopes, defaultAudiences, defaultClientIds, - ImmutableList.of(PassAuthenticator.class), - ImmutableList.of(PassPeerAuthenticator.class)); + ImmutableList.of(PassAuthenticator.class)); assertEquals(1, execute2.getParameterConfigs().size()); validateParameter(execute2.getParameterConfigs().get(0), "serialized", false, null, SimpleBean.class, DumbSerializer1.class, String.class); @@ -1386,8 +1366,7 @@ private void validateMethod(ApiMethodConfig method, String name, String path, St String[] scopes, String[] audiences, String[] clientIds, - List authenticators, - List peerAuthenticators) { + List authenticators) { assertEquals(name, method.getName()); assertEquals(path, method.getPath()); assertEquals(httpMethod, method.getHttpMethod()); @@ -1395,7 +1374,6 @@ private void validateMethod(ApiMethodConfig method, String name, String path, St assertEquals(Arrays.asList(audiences), method.getAudiences()); assertEquals(Arrays.asList(clientIds), method.getClientIds()); assertEquals(authenticators, method.getAuthenticators()); - assertEquals(peerAuthenticators, method.getPeerAuthenticators()); } private void validateMethodForAuth(ApiMethodConfig method, String[] scopes, String[] audiences, diff --git a/endpoints-framework/src/test/java/com/google/api/server/spi/config/annotationreader/ApiMethodAnnotationConfigTest.java b/endpoints-framework/src/test/java/com/google/api/server/spi/config/annotationreader/ApiMethodAnnotationConfigTest.java index 1e9101cd..c7cfe30c 100644 --- a/endpoints-framework/src/test/java/com/google/api/server/spi/config/annotationreader/ApiMethodAnnotationConfigTest.java +++ b/endpoints-framework/src/test/java/com/google/api/server/spi/config/annotationreader/ApiMethodAnnotationConfigTest.java @@ -21,19 +21,17 @@ import com.google.api.server.spi.EndpointMethod; import com.google.api.server.spi.TypeLoader; import com.google.api.server.spi.auth.EndpointsAuthenticator; -import com.google.api.server.spi.auth.EndpointsPeerAuthenticator; import com.google.api.server.spi.config.Api; import com.google.api.server.spi.config.AuthLevel; import com.google.api.server.spi.config.Authenticator; -import com.google.api.server.spi.config.PeerAuthenticator; import com.google.api.server.spi.config.model.ApiClassConfig; import com.google.api.server.spi.config.model.ApiConfig; import com.google.api.server.spi.config.model.ApiMethodConfig; import com.google.api.server.spi.config.model.ApiSerializationConfig; +import com.google.api.server.spi.config.model.ApiValidationConstraints; import com.google.api.server.spi.config.scope.AuthScopeExpression; import com.google.api.server.spi.config.scope.AuthScopeExpressions; import com.google.api.server.spi.testing.PassAuthenticator; -import com.google.api.server.spi.testing.PassPeerAuthenticator; import com.google.api.server.spi.testing.TestEndpoint; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; @@ -44,7 +42,7 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.Mockito; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import java.util.Arrays; import java.util.Collections; @@ -67,8 +65,6 @@ public class ApiMethodAnnotationConfigTest { private static final List defaultClientIds = Lists.newArrayList("c1", "c2"); private static final List> defaultAuthenticators = ImmutableList.>of(EndpointsAuthenticator.class); - private static final List> defaultPeerAuthenticators = - ImmutableList.>of(EndpointsPeerAuthenticator.class); @Before public void setUp() throws Exception { @@ -81,8 +77,6 @@ public void setUp() throws Exception { Mockito.when(apiClassConfig.getClientIds()).thenReturn(defaultClientIds); Mockito.>>when(apiClassConfig.getAuthenticators()) .thenReturn(defaultAuthenticators); - Mockito.>>when(apiClassConfig.getPeerAuthenticators()) - .thenReturn(defaultPeerAuthenticators); Mockito.when(apiClassConfig.getApiClassJavaSimpleName()).thenReturn( TestEndpoint.class.getSimpleName()); Mockito.when(apiConfig.getSerializationConfig()).thenReturn(serializationConfig); @@ -104,14 +98,14 @@ public void testDefaults() { assertEquals(defaultAudiences, config.getAudiences()); assertEquals(defaultClientIds, config.getClientIds()); assertEquals(defaultAuthenticators, config.getAuthenticators()); - assertEquals(defaultPeerAuthenticators, config.getPeerAuthenticators()); } @Test public void testAddParameter() { assertEquals(0, config.getParameterConfigs().size()); - config.addParameter("bleh", "desc", false, null, TypeToken.of(String.class)); + ApiValidationConstraints validationConstraints = new ApiValidationConstraints("\\d{2}", 1L, 2L, "3.0", "4.0", true, false, 5, 6); + config.addParameter("bleh", "desc", false, null, TypeToken.of(String.class), validationConstraints); assertEquals(1, config.getParameterConfigs().size()); assertEquals("bleh", config.getParameterConfigs().get(0).getName()); @@ -121,18 +115,28 @@ public void testAddParameter() { assertEquals( TypeToken.of(String.class), config.getParameterConfigs().get(0).getSchemaBaseType()); assertEquals("overrideMethod1/{bleh}", config.getPath()); + ApiValidationConstraints actualValidationConstraints = config.getParameterConfigs().get(0).getValidationConstraints(); + assertEquals("\\d{2}", actualValidationConstraints.getPattern()); + assertEquals(new Long(1L), actualValidationConstraints.getMin()); + assertEquals(new Long(2L), actualValidationConstraints.getMax()); + assertEquals("3.0", actualValidationConstraints.getDecimalMin()); + assertEquals("4.0", actualValidationConstraints.getDecimalMax()); + assertEquals(Boolean.TRUE, actualValidationConstraints.getDecimalMinInclusive()); + assertEquals(Boolean.FALSE, actualValidationConstraints.getDecimalMaxInclusive()); + assertEquals(Integer.valueOf(5), actualValidationConstraints.getMinSize()); + assertEquals(Integer.valueOf(6), actualValidationConstraints.getMaxSize()); } @Test public void testAddParameter_nullableOrDefault() { assertEquals(0, config.getParameterConfigs().size()); - config.addParameter("bleh", null, true, null, TypeToken.of(String.class)); + config.addParameter("bleh", null, true, null, TypeToken.of(String.class), null); assertEquals(1, config.getParameterConfigs().size()); assertEquals("bleh", config.getParameterConfigs().get(0).getName()); assertEquals("overrideMethod1", config.getPath()); - config.addParameter("foo", null, false, "42", TypeToken.of(String.class)); + config.addParameter("foo", null, false, "42", TypeToken.of(String.class), null); assertEquals(2, config.getParameterConfigs().size()); assertEquals("foo", config.getParameterConfigs().get(1).getName()); assertEquals("overrideMethod1", config.getPath()); @@ -361,29 +365,6 @@ public void testSetAuthenticatorIfSpecified_unspecified() throws Exception { assertEquals(Arrays.asList(PassAuthenticator.testArray), config.getAuthenticators()); } - @Test - public void testPeerSetAuthenticatorIfSpecified() throws Exception { - annotationConfig.setPeerAuthenticatorsIfSpecified(PassPeerAuthenticator.testArray); - assertEquals(Arrays.asList(PassPeerAuthenticator.testArray), config.getPeerAuthenticators()); - } - - // Unchecked cast needed to get a generic array type. - @SuppressWarnings("unchecked") - public void testSetPeerAuthenticatorIfSpecified_unspecified() throws Exception { - Class[] unspecified = {PeerAuthenticator.class}; - Class[] unspecifiedConverted = - (Class[]) unspecified; - - testDefaults(); - - annotationConfig.setPeerAuthenticatorsIfSpecified(unspecifiedConverted); - testDefaults(); - - annotationConfig.setPeerAuthenticatorsIfSpecified(PassPeerAuthenticator.testArray); - annotationConfig.setPeerAuthenticatorsIfSpecified(unspecifiedConverted); - assertEquals(Arrays.asList(PassPeerAuthenticator.testArray), config.getPeerAuthenticators()); - } - private static AuthScopeExpression toScopeExpression(String... scopes) { return AuthScopeExpressions.interpret(scopes); } diff --git a/endpoints-framework/src/test/java/com/google/api/server/spi/config/jsonwriter/JacksonResourceSchemaProviderTest.java b/endpoints-framework/src/test/java/com/google/api/server/spi/config/jsonwriter/JacksonResourceSchemaProviderTest.java index 024e7093..b8519d09 100644 --- a/endpoints-framework/src/test/java/com/google/api/server/spi/config/jsonwriter/JacksonResourceSchemaProviderTest.java +++ b/endpoints-framework/src/test/java/com/google/api/server/spi/config/jsonwriter/JacksonResourceSchemaProviderTest.java @@ -18,6 +18,8 @@ import org.junit.runner.RunWith; import org.junit.runners.JUnit4; +import com.google.api.server.spi.TypeLoader; + /** * Tests for {@link JacksonResourceSchemaProvider}. */ @@ -25,6 +27,10 @@ public class JacksonResourceSchemaProviderTest extends ResourceSchemaProviderTest { @Override public ResourceSchemaProvider getResourceSchemaProvider() { - return new JacksonResourceSchemaProvider(); + try { + return new JacksonResourceSchemaProvider(new TypeLoader(JacksonResourceSchemaProviderTest.class.getClassLoader())); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } } } diff --git a/endpoints-framework/src/test/java/com/google/api/server/spi/config/model/ApiClassConfigTest.java b/endpoints-framework/src/test/java/com/google/api/server/spi/config/model/ApiClassConfigTest.java index be11d584..adbb3bd3 100644 --- a/endpoints-framework/src/test/java/com/google/api/server/spi/config/model/ApiClassConfigTest.java +++ b/endpoints-framework/src/test/java/com/google/api/server/spi/config/model/ApiClassConfigTest.java @@ -32,7 +32,7 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.Mockito; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import java.lang.reflect.Method; import java.util.Date; diff --git a/endpoints-framework/src/test/java/com/google/api/server/spi/config/model/ApiConfigTest.java b/endpoints-framework/src/test/java/com/google/api/server/spi/config/model/ApiConfigTest.java index 500e378f..3da29f0d 100644 --- a/endpoints-framework/src/test/java/com/google/api/server/spi/config/model/ApiConfigTest.java +++ b/endpoints-framework/src/test/java/com/google/api/server/spi/config/model/ApiConfigTest.java @@ -20,12 +20,10 @@ import com.google.api.server.spi.ServiceContext; import com.google.api.server.spi.TypeLoader; -import com.google.api.server.spi.auth.EndpointsPeerAuthenticator; import com.google.api.server.spi.auth.GoogleJwtAuthenticator; import com.google.api.server.spi.config.ApiConfigInconsistency; import com.google.api.server.spi.config.AuthLevel; import com.google.api.server.spi.config.Authenticator; -import com.google.api.server.spi.config.PeerAuthenticator; import com.google.api.server.spi.config.scope.AuthScopeExpressions; import com.google.api.server.spi.testing.DumbSerializer1; import com.google.api.server.spi.testing.FloatToStringSerializer; @@ -177,8 +175,6 @@ public void testCopyConstructor() { apiConfig.setClientIds(ImmutableList.of("clientid")); apiConfig.setAuthenticators( ImmutableList.>of(GoogleJwtAuthenticator.class)); - apiConfig.setPeerAuthenticators( - ImmutableList.>of(EndpointsPeerAuthenticator.class)); assertThat(apiConfig).isEqualTo(new ApiConfig(apiConfig)); } } diff --git a/endpoints-framework/src/test/java/com/google/api/server/spi/config/model/ApiIssuerConfigsTest.java b/endpoints-framework/src/test/java/com/google/api/server/spi/config/model/ApiIssuerConfigsTest.java index fdf5479c..f5ca03c5 100644 --- a/endpoints-framework/src/test/java/com/google/api/server/spi/config/model/ApiIssuerConfigsTest.java +++ b/endpoints-framework/src/test/java/com/google/api/server/spi/config/model/ApiIssuerConfigsTest.java @@ -23,19 +23,19 @@ public void isSpecified() { public void asMap() { assertThat(ApiIssuerConfigs.builder().build().asMap()).isEmpty(); ApiIssuerConfigs configs = ApiIssuerConfigs.builder() - .addIssuer(new IssuerConfig("issuerName", "issuer", "jwks")) + .addIssuer(new IssuerConfig("issuerName", "issuer", "jwks", "authUrl", true)) .build(); assertThat(configs.asMap()).containsExactly( - "issuerName", new IssuerConfig("issuerName", "issuer", "jwks")); + "issuerName", new IssuerConfig("issuerName", "issuer", "jwks", "authUrl", true)); } @Test public void equals() { ApiIssuerConfigs configs1 = ApiIssuerConfigs.builder() - .addIssuer(new IssuerConfig("issuerName", "issuer", "jwks")) + .addIssuer(new IssuerConfig("issuerName", "issuer", "jwks", "authUrl", true)) .build(); ApiIssuerConfigs configs2 = ApiIssuerConfigs.builder() - .addIssuer(new IssuerConfig("issuerName", "issuer", "jwks")) + .addIssuer(new IssuerConfig("issuerName", "issuer", "jwks", "authUrl", true)) .build(); assertThat(configs1).isEqualTo(configs2); assertThat(configs1).isNotEqualTo(ApiIssuerConfigs.UNSPECIFIED); diff --git a/endpoints-framework/src/test/java/com/google/api/server/spi/config/model/ApiMethodConfigTest.java b/endpoints-framework/src/test/java/com/google/api/server/spi/config/model/ApiMethodConfigTest.java index fc401d44..fcc04d42 100644 --- a/endpoints-framework/src/test/java/com/google/api/server/spi/config/model/ApiMethodConfigTest.java +++ b/endpoints-framework/src/test/java/com/google/api/server/spi/config/model/ApiMethodConfigTest.java @@ -32,7 +32,7 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.Mockito; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import java.util.List; @@ -58,6 +58,9 @@ public class ApiMethodConfigTest { private static final List defaultAudiences2 = Lists.newArrayList("a1"); private static final List defaultClientIds2 = Lists.newArrayList("c1", "c2", "c3"); + private static final TypeToken voidReturnType = TypeToken.of(Void.class); + private static final TypeToken stringReturnType = TypeToken.of(String.class); + @Before public void setUp() throws Exception { Mockito.when(apiConfig.getName()).thenReturn("testapi"); @@ -71,7 +74,6 @@ public void setUp() throws Exception { Mockito.when(apiClassConfig.getApiConfig()).thenReturn(apiConfig); Mockito.when(method.getMethod()).thenReturn(TestEndpoint.class.getMethod("getResultNoParams")); - Mockito.doReturn(TestEndpoint.class).when(method).getEndpointClass(); methodConfig = new ApiMethodConfig(method, new TypeLoader(), apiClassConfig); } @@ -108,22 +110,38 @@ public void testMethodNameNoResource() { assertEquals("className.getResultNoParams", methodConfig.getName()); } + @Test + public void testMethodResponseStatusEffectiveStatus_returnValue_statusOK() throws Exception { + Mockito.when(method.getReturnType()).thenReturn(stringReturnType); + methodConfig = new ApiMethodConfig(method, new TypeLoader(), apiClassConfig); + + assertEquals(200, methodConfig.getEffectiveResponseStatus()); + } + + @Test + public void testMethodResponseStatusEffectiveStatus_returnVoid_statusNO_CONTENT() throws Exception { + Mockito.when(method.getReturnType()).thenReturn(voidReturnType); + methodConfig = new ApiMethodConfig(method, new TypeLoader(), apiClassConfig); + + assertEquals(204, methodConfig.getEffectiveResponseStatus()); + } + @Test public void addInjectedParameter_notInPath() { - methodConfig.addParameter("alt", null, false, null, TypeToken.of(String.class)); + methodConfig.addParameter("alt", null, false, null, TypeToken.of(String.class), null); assertThat(methodConfig.getPath()).doesNotContain("{alt}"); } @Test public void addPathParameter_appendsToCanonicalPath() { - methodConfig.addParameter("test", null, false, null, TypeToken.of(String.class)); + methodConfig.addParameter("test", null, false, null, TypeToken.of(String.class), null); assertThat(methodConfig.getCanonicalPath()).contains("{test}"); } @Test public void addPathParameter_doesNotAppendIfInPathAlready() { methodConfig.setPath("test/{test}"); - methodConfig.addParameter("test", null, false, null, TypeToken.of(String.class)); + methodConfig.addParameter("test", null, false, null, TypeToken.of(String.class), null); assertThat(methodConfig.getPath()).isEqualTo("test/{test}"); } } diff --git a/endpoints-framework/src/test/java/com/google/api/server/spi/config/model/ApiParameterConfigTest.java b/endpoints-framework/src/test/java/com/google/api/server/spi/config/model/ApiParameterConfigTest.java index be1d1344..6649c2a5 100644 --- a/endpoints-framework/src/test/java/com/google/api/server/spi/config/model/ApiParameterConfigTest.java +++ b/endpoints-framework/src/test/java/com/google/api/server/spi/config/model/ApiParameterConfigTest.java @@ -31,7 +31,7 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.Mockito; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import java.util.Collections; @@ -87,9 +87,9 @@ public void setUp() throws Exception { Mockito.when(apiConfig.getSerializationConfig()).thenReturn(serializationConfig); config = new ApiParameterConfig( - apiMethodConfig, "bleh", null, false, null, TypeToken.of(String.class), typeLoader); + apiMethodConfig, "bleh", null, false, null, TypeToken.of(String.class), typeLoader, null); configWithArray = new ApiParameterConfig( - apiMethodConfig, "bleh", null, false, null, TypeToken.of(Boolean[].class), typeLoader); + apiMethodConfig, "bleh", null, false, null, TypeToken.of(Boolean[].class), typeLoader, null); } @Test @@ -157,6 +157,6 @@ public void standardParametersAreInjected() { private ApiParameterConfig createStandardParameter(String name) { return new ApiParameterConfig( - apiMethodConfig, "alt", null, false, null, TypeToken.of(String.class), typeLoader); + apiMethodConfig, "alt", null, false, null, TypeToken.of(String.class), typeLoader, null); } } diff --git a/endpoints-framework/src/test/java/com/google/api/server/spi/config/model/SchemaRepositoryTest.java b/endpoints-framework/src/test/java/com/google/api/server/spi/config/model/SchemaRepositoryTest.java index f62d6d6a..8bdff31f 100644 --- a/endpoints-framework/src/test/java/com/google/api/server/spi/config/model/SchemaRepositoryTest.java +++ b/endpoints-framework/src/test/java/com/google/api/server/spi/config/model/SchemaRepositoryTest.java @@ -9,8 +9,10 @@ import com.google.api.server.spi.EndpointMethod; import com.google.api.server.spi.ServiceContext; import com.google.api.server.spi.TypeLoader; +import com.google.api.server.spi.config.AnnotationBoolean; import com.google.api.server.spi.config.Api; import com.google.api.server.spi.config.ApiConfigLoader; +import com.google.api.server.spi.config.ApiResourceProperty; import com.google.api.server.spi.config.Transformer; import com.google.api.server.spi.config.annotationreader.ApiConfigAnnotationReader; import com.google.api.server.spi.config.model.ApiParameterConfig.Classification; @@ -23,11 +25,15 @@ import com.google.common.reflect.TypeParameter; import com.google.common.reflect.TypeToken; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import org.junit.Before; import org.junit.Test; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.Optional; /** * Tests for {@link SchemaRepository}. @@ -97,6 +103,7 @@ public void getOrAdd_mapType() throws Exception { Schema schema = repo.getOrAdd(methodConfig.getReturnType(), config); assertThat(schema).isEqualTo(Schema.builder() .setName("Map_String_TestEnum") + .setDescription("A collection of name / TestEnum pairs") .setType("object") .setMapValueSchema(Field.builder() .setName(SchemaRepository.MAP_UNUSED_MSG) @@ -138,6 +145,7 @@ public void getOrAdd_NestedMap() throws Exception { Schema expectedSchema = Schema.builder() .setName("Map_String_Map_String_String") .setType("object") + .setDescription("A collection of name / Map_String_String pairs") .setMapValueSchema(Field.builder() .setName(SchemaRepository.MAP_UNUSED_MSG) .setType(FieldType.OBJECT) @@ -209,14 +217,9 @@ public void getOrAdd_transformer() throws Exception { repo.getOrAdd(methodConfig.getReturnType(), config), String.class); } - @Test + @Test(expected = IllegalArgumentException.class) public void getOrAdd_primitiveReturn() throws Exception { - try { - repo.getOrAdd(TypeToken.of(int.class), config); - fail("expected IllegalArgumentException"); - } catch (IllegalArgumentException expected) { - // expected - } + repo.getOrAdd(TypeToken.of(int.class), config); } @Test @@ -226,12 +229,69 @@ public void getOrAdd_enum() throws Exception { .setName("TestEnum") .setType("string") .addEnumValue("VALUE1") - .addEnumValue("VALUE2") + .addEnumValue("value_2") .addEnumDescription("") .addEnumDescription("") .build()); } + @Test + public void getOrAdd_enum_disableJacksonAnnotations() throws Exception { + System.setProperty(EndpointsFlag.JSON_USE_JACKSON_ANNOTATIONS.systemPropertyName, "false"); + try { + assertThat(repo.getOrAdd(TypeToken.of(TestEnum.class), config)) + .isEqualTo(Schema.builder() + .setName("TestEnum") + .setType("string") + .addEnumValue("VALUE1") + .addEnumValue("VALUE2") + .addEnumDescription("") + .addEnumDescription("") + .build()); + } finally { + System.clearProperty(EndpointsFlag.JSON_USE_JACKSON_ANNOTATIONS.systemPropertyName); + } + } + + @Test + public void getOrAdd_optional_foo() throws Exception { + checkRequiredProperties(new TypeToken>() {}); + } + + @Test + public void getOrAdd_optional_enum() throws Exception { + assertThat(repo.getOrAdd(new TypeToken>() {}, config)) + .isEqualTo(Schema.builder() + .setName("TestEnum") + .setType("string") + .addEnumValue("VALUE1") + .addEnumValue("value_2") + .addEnumDescription("") + .addEnumDescription("") + .build()); + } + + @Test(expected = IllegalArgumentException.class) + public void getOrAdd_optional_list() throws Exception { + repo.getOrAdd(new TypeToken>>() {}, config); + } + + @Test(expected = IllegalArgumentException.class) + public void getOrAdd_optional_map() throws Exception { + repo.getOrAdd(new TypeToken>>() {}, config); + + } + + @Test(expected = IllegalArgumentException.class) + public void getOrAdd_optional_object() throws Exception { + repo.getOrAdd(new TypeToken>() {}, config); + } + + @Test(expected = IllegalArgumentException.class) + public void getOrAdd_optional_optional() throws Exception { + repo.getOrAdd(new TypeToken>>() {}, config); + } + @Test public void getOrAdd_recursiveSchema() throws Exception { TypeToken type = TypeToken.of(SelfReferencingObject.class); @@ -251,6 +311,14 @@ public void getOrAdd_recursiveSchema() throws Exception { .build()); } + @Test + public void getOrAdd_requiredProperties() throws Exception { + TypeToken type = TypeToken.of(RequiredProperties.class); + // This test checks the combinations of annotation that determine the "required" marker for + // resource properties. + checkRequiredProperties(type); + } + @Test public void get() { TypeToken> type = new TypeToken>() {}; @@ -270,7 +338,7 @@ public void getOrAdd_multipleApis() throws Exception { .setName("TestEnum") .setType("string") .addEnumValue("VALUE1") - .addEnumValue("VALUE2") + .addEnumValue("value_2") .addEnumDescription("") .addEnumDescription("") .build(), @@ -381,6 +449,44 @@ public Parameterized transformFrom(Parameterized in) { } } + private static class RequiredProperties { + public String getUndefined() { + return null; + } + @ApiResourceProperty + public String apiResourceProperty_undefined() { + return null; + } + @ApiResourceProperty(required = AnnotationBoolean.TRUE) + public String apiResourceProperty_required() { + return ""; + } + @ApiResourceProperty(required = AnnotationBoolean.FALSE) + public String apiResourceProperty_not_required() { + return null; + } + @Nullable + public String getNullable() { + return null; + } + @Nonnull + public String getNonnull() { + return ""; + } + @ApiResourceProperty(required = AnnotationBoolean.TRUE) @Nullable + public String getPriority1() { + return ""; + } + @Nonnull @Nullable + public String getPriority2() { + return ""; + } + @ApiResourceProperty(required = AnnotationBoolean.FALSE) @Nonnull + public String getPriority3() { + return null; + } + } + private static class SelfReferencingObject { public SelfReferencingObject getFoo() { return null; @@ -461,4 +567,55 @@ private static void checkIntegerCollection(Schema schema) { .build()) .build()); } + + private void checkRequiredProperties(TypeToken type) { + assertThat(repo.getOrAdd(type, config)) + .isEqualTo(Schema.builder() + .setName("RequiredProperties") + .setType("object") + .addField("undefined", Field.builder() + .setName("undefined") + .setType(FieldType.STRING) + .build()) + .addField("apiResourceProperty_undefined", Field.builder() + .setName("apiResourceProperty_undefined") + .setType(FieldType.STRING) + .build()) + .addField("apiResourceProperty_required", Field.builder() + .setName("apiResourceProperty_required") + .setRequired(true) + .setType(FieldType.STRING) + .build()) + .addField("apiResourceProperty_not_required", Field.builder() + .setName("apiResourceProperty_not_required") + .setRequired(false) + .setType(FieldType.STRING) + .build()) + .addField("nullable", Field.builder() + .setName("nullable") + .setRequired(false) + .setType(FieldType.STRING) + .build()) + .addField("nonnull", Field.builder() + .setName("nonnull") + .setRequired(true) + .setType(FieldType.STRING) + .build()) + .addField("priority1", Field.builder() + .setName("priority1") + .setRequired(true) + .setType(FieldType.STRING) + .build()) + .addField("priority2", Field.builder() + .setName("priority2") + .setRequired(true) + .setType(FieldType.STRING) + .build()) + .addField("priority3", Field.builder() + .setName("priority3") + .setRequired(false) + .setType(FieldType.STRING) + .build()) + .build()); + } } diff --git a/endpoints-framework/src/test/java/com/google/api/server/spi/config/validation/ApiConfigValidatorTest.java b/endpoints-framework/src/test/java/com/google/api/server/spi/config/validation/ApiConfigValidatorTest.java index db671bb3..84762cf6 100644 --- a/endpoints-framework/src/test/java/com/google/api/server/spi/config/validation/ApiConfigValidatorTest.java +++ b/endpoints-framework/src/test/java/com/google/api/server/spi/config/validation/ApiConfigValidatorTest.java @@ -34,14 +34,13 @@ import com.google.api.server.spi.config.ApiIssuerAudience; import com.google.api.server.spi.config.ApiMethod; import com.google.api.server.spi.config.Authenticator; -import com.google.api.server.spi.config.PeerAuthenticator; import com.google.api.server.spi.config.Transformer; import com.google.api.server.spi.config.model.ApiConfig; import com.google.api.server.spi.config.model.SchemaRepository; +import com.google.api.server.spi.config.model.Serializers; import com.google.api.server.spi.testing.DefaultValueSerializer; import com.google.api.server.spi.testing.DuplicateMethodEndpoint; import com.google.api.server.spi.testing.PassAuthenticator; -import com.google.api.server.spi.testing.PassPeerAuthenticator; import com.google.api.server.spi.testing.TestEndpoint; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; @@ -208,7 +207,7 @@ final class TestSerializer extends DefaultValueSerializer {} config.getApiClassConfig().getMethods() .get(methodToEndpointMethod(TestEndpoint.class.getMethod("getResultNoParams"))) - .addParameter("param", null, false, null, TypeToken.of(Integer.class)) + .addParameter("param", null, false, null, TypeToken.of(Integer.class), null) .setSerializer(TestSerializer.class); try { @@ -224,7 +223,7 @@ final class TestSerializer extends DefaultValueSerializer {} config.getApiClassConfig().getMethods() .get(methodToEndpointMethod(TestEndpoint.class.getMethod("getResultNoParams"))) - .addParameter("param", null, false, null, TypeToken.of(Integer[].class)) + .addParameter("param", null, false, null, TypeToken.of(Integer[].class), null) .setRepeatedItemSerializer(TestSerializer.class); try { @@ -238,12 +237,15 @@ final class TestSerializer extends DefaultValueSerializer {} public void testMultipleSerializersInstalled() throws Exception { // TODO: The generic component of Comparable causes validation to miss certain error // cases like this. - @SuppressWarnings("rawtypes") final class ComparableSerializer extends DefaultValueSerializer, Integer> {} final class CharSequenceSerializer extends DefaultValueSerializer {} config.getSerializationConfig().addSerializationConfig(ComparableSerializer.class); config.getSerializationConfig().addSerializationConfig(CharSequenceSerializer.class); + List>> serializerClasses = Serializers + .getSerializerClasses(TypeToken.of(String.class), config.getSerializationConfig()); + assertThat(serializerClasses.size()).isEqualTo(2); + try { validator.validate(config); fail("Expected MultipleTransformersException."); @@ -269,7 +271,7 @@ final class TestSerializer {} config.getApiClassConfig().getMethods() .get(methodToEndpointMethod(TestEndpoint.class.getMethod("getResultNoParams"))) - .addParameter("param", null, false, null, TypeToken.of(Integer.class)) + .addParameter("param", null, false, null, TypeToken.of(Integer.class), null) .setSerializer((Class>) (Class) TestSerializer.class); try { @@ -285,7 +287,7 @@ final class TestSerializer {} config.getApiClassConfig().getMethods() .get(methodToEndpointMethod(TestEndpoint.class.getMethod("getResultNoParams"))) - .addParameter("param", null, false, null, TypeToken.of(Integer[].class)) + .addParameter("param", null, false, null, TypeToken.of(Integer[].class), null) .setRepeatedItemSerializer( (Class>) (Class) TestSerializer.class); @@ -306,7 +308,7 @@ public void foo(List l) {} .get(methodToEndpointMethod(TestEndpoint.class.getMethod("getResultNoParams"))) .addParameter("param", null, false, null, TypeToken.of( - Foo.class.getDeclaredMethod("foo", List.class).getGenericParameterTypes()[0])); + Foo.class.getDeclaredMethod("foo", List.class).getGenericParameterTypes()[0]), null); try { validator.validate(config); @@ -325,7 +327,7 @@ public void foo(List[] l) {} .get(methodToEndpointMethod(TestEndpoint.class.getMethod("getResultNoParams"))) .addParameter("param", null, false, null, TypeToken.of( - Foo.class.getDeclaredMethod("foo", List[].class).getGenericParameterTypes()[0])); + Foo.class.getDeclaredMethod("foo", List[].class).getGenericParameterTypes()[0]), null); try { validator.validate(config); @@ -344,7 +346,7 @@ public void foo(List> l) {} .get(methodToEndpointMethod(TestEndpoint.class.getMethod("getResultNoParams"))) .addParameter("param", null, false, null, TypeToken.of( - Foo.class.getDeclaredMethod("foo", List.class).getGenericParameterTypes()[0])); + Foo.class.getDeclaredMethod("foo", List.class).getGenericParameterTypes()[0]), null); try { validator.validate(config); @@ -357,7 +359,7 @@ public void foo(List> l) {} public void testArrayOfArraysParameter() throws Exception { config.getApiClassConfig().getMethods() .get(methodToEndpointMethod(TestEndpoint.class.getMethod("getResultNoParams"))) - .addParameter("param", null, false, null, TypeToken.of(Integer[][].class)); + .addParameter("param", null, false, null, TypeToken.of(Integer[][].class), null); try { validator.validate(config); @@ -377,7 +379,7 @@ public void foo(T t) {} config.getApiClassConfig().getMethods() .get(methodToEndpointMethod(TestEndpoint.class.getMethod("getResultNoParams"))) - .addParameter("param", null, false, null, unknownType); + .addParameter("param", null, false, null, unknownType, null); try { validator.validate(config); @@ -502,6 +504,57 @@ public void test() { } } + @Test + public void testApiMethodConfigWithApiMethodResponseStatus1xx() throws Exception { + @Api(name = "testApi", version = "v1", resource = "bar") + final class Test { + @ApiMethod(responseStatus = 103) + public void test() { + } + } + + ApiConfig config = configLoader.loadConfiguration(ServiceContext.create(), Test.class); + + try { + validator.validate(config); + fail("Expected InvalidResponseStatusException."); + } catch (InvalidResponseStatusException expected) { + assertThat(expected.getMessage()).contains("103"); + } + } + + @Test + public void testApiMethodConfigWithApiMethodResponseStatus3xx() throws Exception { + @Api(name = "testApi", version = "v1", resource = "bar") + final class Test { + @ApiMethod(responseStatus = 300) + public void test() { + } + } + + ApiConfig config = configLoader.loadConfiguration(ServiceContext.create(), Test.class); + + try { + validator.validate(config); + fail("Expected InvalidResponseStatusException."); + } catch (InvalidResponseStatusException expected) { + assertThat(expected.getMessage()).contains("300"); + } + } + + @Test + public void testApiMethodConfigWithApiMethodResponseStatusCreated() throws Exception { + @Api(name = "testApi", version = "v1", resource = "bar") + final class Test { + @ApiMethod(responseStatus = 201) + public void test() { + } + } + + ApiConfig config = configLoader.loadConfiguration(ServiceContext.create(), Test.class); + validator.validate(config); + } + @Test public void testValidateAuthenticator() throws Exception { config.getApiClassConfig().getMethods() @@ -568,72 +621,6 @@ public User authenticate(HttpServletRequest request) { } } - @Test - public void testValidatePeerAuthenticator() throws Exception { - config.getApiClassConfig().getMethods() - .get(methodToEndpointMethod(TestEndpoint.class.getMethod("getResultNoParams"))) - .setPeerAuthenticators( - ImmutableList.>of(PassPeerAuthenticator.class)); - - validator.validate(config); - } - - @Test - public void testValidatePeerAuthenticator_noNullary() throws Exception { - final class InvalidPeerAuthenticator implements PeerAuthenticator { - @SuppressWarnings("unused") - public InvalidPeerAuthenticator(int x) {} - - @SuppressWarnings("unused") - @Override - public boolean authenticate(HttpServletRequest request) { - return false; - } - } - - config.getApiClassConfig().getMethods() - .get(methodToEndpointMethod(TestEndpoint.class.getMethod("getResultNoParams"))) - .setPeerAuthenticators( - ImmutableList.>of(InvalidPeerAuthenticator.class)); - - try { - validator.validate(config); - fail(); - } catch (InvalidConstructorException expected) { - assertTrue(expected.getMessage().contains("Invalid custom peer authenticator")); - assertTrue(expected.getMessage().endsWith( - "InvalidPeerAuthenticator. It must have a public nullary constructor.")); - } - } - - @Test - public void testValidatePeerAuthenticator_privateNullary() throws Exception { - final class InvalidPeerAuthenticator implements PeerAuthenticator { - @SuppressWarnings("unused") - private InvalidPeerAuthenticator() {} - - @SuppressWarnings("unused") - @Override - public boolean authenticate(HttpServletRequest request) { - return false; - } - } - - config.getApiClassConfig().getMethods() - .get(methodToEndpointMethod(TestEndpoint.class.getMethod("getResultNoParams"))) - .setPeerAuthenticators( - ImmutableList.>of(InvalidPeerAuthenticator.class)); - - try { - validator.validate(config); - fail(); - } catch (InvalidConstructorException expected) { - assertTrue(expected.getMessage().contains("Invalid custom peer authenticator")); - assertTrue(expected.getMessage().endsWith( - "InvalidPeerAuthenticator. It must have a public nullary constructor.")); - } - } - @Test public void testValidateMethods_ignoredMethod() throws Exception { final class Bean { diff --git a/endpoints-framework/src/test/java/com/google/api/server/spi/discovery/CachingDiscoveryProviderTest.java b/endpoints-framework/src/test/java/com/google/api/server/spi/discovery/CachingDiscoveryProviderTest.java index 61b8b66c..26151eab 100644 --- a/endpoints-framework/src/test/java/com/google/api/server/spi/discovery/CachingDiscoveryProviderTest.java +++ b/endpoints-framework/src/test/java/com/google/api/server/spi/discovery/CachingDiscoveryProviderTest.java @@ -25,13 +25,12 @@ import com.google.api.server.spi.response.NotFoundException; import com.google.api.services.discovery.model.DirectoryList; import com.google.api.services.discovery.model.RestDescription; -import com.google.api.services.discovery.model.RpcDescription; import com.google.common.collect.ImmutableList; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import java.util.concurrent.TimeUnit; @@ -46,9 +45,6 @@ public class CachingDiscoveryProviderTest { private static final RestDescription REST_DOC = new RestDescription() .setName(NAME) .setVersion(VERSION); - private static final RpcDescription RPC_DOC = new RpcDescription() - .setName(NAME) - .setVersion(VERSION); private static final DirectoryList DIRECTORY = new DirectoryList() .setItems(ImmutableList.of(new DirectoryList.Items() .setName(NAME) @@ -122,72 +118,6 @@ public void getRestDocument_runtimeException() throws Exception { } } - @Test - public void getRpcDocument() throws Exception { - CachingDiscoveryProvider provider = createNonExpiringProvider(); - setupNormalMockDelegate(); - - // Make the same call twice and ensure that the delegate is only called once. - assertThat(provider.getRpcDocument(ROOT, NAME, VERSION)).isEqualTo(RPC_DOC); - assertThat(provider.getRpcDocument(ROOT, NAME, VERSION)).isEqualTo(RPC_DOC); - verify(delegate, times(1)).getRpcDocument(ROOT, NAME, VERSION); - } - - @Test - public void getRpcDocument_cacheExpiry() throws Exception { - CachingDiscoveryProvider provider = createShortExpiringProvider(); - setupNormalMockDelegate(); - - assertThat(provider.getRpcDocument(ROOT, NAME, VERSION)).isEqualTo(RPC_DOC); - - Thread.sleep(1000); - provider.cleanUp(); - - assertThat(provider.getRpcDocument(ROOT, NAME, VERSION)).isEqualTo(RPC_DOC); - verify(delegate, times(2)).getRpcDocument(ROOT, NAME, VERSION); - } - - @Test - public void getRpcDocument_notFound() throws Exception { - CachingDiscoveryProvider provider = createNonExpiringProvider(); - when(delegate.getRpcDocument(ROOT, NAME, VERSION)).thenThrow(new NotFoundException("")); - - try { - provider.getRpcDocument(ROOT, NAME, VERSION); - fail("expected NotFoundException"); - } catch (NotFoundException e) { - // expected - } - } - - @Test - public void getRpcDocument_internalServerError() throws Exception { - CachingDiscoveryProvider provider = createNonExpiringProvider(); - when(delegate.getRpcDocument(ROOT, NAME, VERSION)) - .thenThrow(new InternalServerErrorException("")); - - try { - provider.getRpcDocument(ROOT, NAME, VERSION); - fail("expected InternalServerErrorException"); - } catch (InternalServerErrorException e) { - // expected - } - } - - @Test - public void getRpcDocument_runtimeException() throws Exception { - CachingDiscoveryProvider provider = createNonExpiringProvider(); - when(delegate.getRpcDocument(ROOT, NAME, VERSION)) - .thenThrow(new RuntimeException("")); - - try { - provider.getRpcDocument(ROOT, NAME, VERSION); - fail("expected InternalServerErrorException"); - } catch (InternalServerErrorException e) { - // expected - } - } - @Test public void getDirectory() throws Exception { CachingDiscoveryProvider provider = createNonExpiringProvider(); @@ -253,7 +183,6 @@ private CachingDiscoveryProvider createProvider(long cacheExpiry, TimeUnit cache private void setupNormalMockDelegate() throws Exception { when(delegate.getRestDocument(ROOT, NAME, VERSION)).thenReturn(REST_DOC); - when(delegate.getRpcDocument(ROOT, NAME, VERSION)).thenReturn(RPC_DOC); when(delegate.getDirectory(ROOT)).thenReturn(DIRECTORY); } } diff --git a/endpoints-framework/src/test/java/com/google/api/server/spi/discovery/DiscoveryGeneratorTest.java b/endpoints-framework/src/test/java/com/google/api/server/spi/discovery/DiscoveryGeneratorTest.java index b4de96b0..481a82f9 100644 --- a/endpoints-framework/src/test/java/com/google/api/server/spi/discovery/DiscoveryGeneratorTest.java +++ b/endpoints-framework/src/test/java/com/google/api/server/spi/discovery/DiscoveryGeneratorTest.java @@ -22,7 +22,6 @@ import com.google.api.client.json.jackson2.JacksonFactory; import com.google.api.server.spi.IoUtil; -import com.google.api.server.spi.ObjectMapperUtil; import com.google.api.server.spi.ServiceContext; import com.google.api.server.spi.TypeLoader; import com.google.api.server.spi.config.ApiConfigLoader; @@ -40,17 +39,19 @@ import com.google.api.server.spi.testing.FooEndpoint; import com.google.api.server.spi.testing.MapEndpoint; import com.google.api.server.spi.testing.MapEndpointInvalid; +import com.google.api.server.spi.testing.MapEndpoints; import com.google.api.server.spi.testing.MultipleParameterEndpoint; import com.google.api.server.spi.testing.NamespaceEndpoint; import com.google.api.server.spi.testing.NonDiscoverableEndpoint; +import com.google.api.server.spi.testing.OptionalEndpoint; import com.google.api.server.spi.testing.PrimitiveEndpoint; +import com.google.api.server.spi.testing.RequiredPropertiesEndpoint; +import com.google.api.server.spi.testing.ValidationEndpoint; import com.google.api.services.discovery.model.DirectoryList; import com.google.api.services.discovery.model.RestDescription; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; -import com.fasterxml.jackson.databind.ObjectMapper; - import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -64,7 +65,6 @@ public class DiscoveryGeneratorTest { private final DiscoveryContext context = new DiscoveryContext() .setApiRoot("https://discovery-test.appspot.com/api"); - private final ObjectMapper mapper = ObjectMapperUtil.createStandardObjectMapper(); private DiscoveryGenerator generator; private ApiConfigLoader configLoader; private SchemaRepository schemaRepository; @@ -166,6 +166,21 @@ public void testWriteDiscovery_MapEndpoint_WithArrayValue() throws Exception { System.clearProperty(MAP_SCHEMA_SUPPORT_ARRAYS_VALUES.systemPropertyName); } } + + @Test + public void testWriteDiscovery_MapEndpoint_CacheArrayValues() throws Exception { + System.setProperty(MAP_SCHEMA_SUPPORT_ARRAYS_VALUES.systemPropertyName, "yes"); + try { + RestDescription api1 = getDiscovery(new DiscoveryContext(), MapEndpoints.Api1.class); + assertThat(api1.getSchemas()).containsKey("TestEnum"); + //second generation has the schema for Resource cached, checks the + //array value of its mapOfEnum field is present in description + RestDescription api2 = getDiscovery(new DiscoveryContext(), MapEndpoints.Api2.class); + assertThat(api2.getSchemas()).containsKey("TestEnum"); + } finally { + System.clearProperty(MAP_SCHEMA_SUPPORT_ARRAYS_VALUES.systemPropertyName); + } + } @Test public void testWriteDiscovery_namespace() throws Exception { @@ -219,6 +234,13 @@ public void testWriteDiscovery_FooEndpointWithDescription() throws Exception { compareDiscovery(expected, doc); } + @Test + public void testWriteDiscovery_RequiredPropertiesEndpoint() throws Exception { + RestDescription doc = getDiscovery(context, RequiredPropertiesEndpoint.class); + RestDescription expected = readExpectedAsDiscovery("required_parameters_endpoint.json"); + compareDiscovery(expected, doc); + } + @Test public void testWriteDiscovery_multipleApisWithSharedSchema() throws Exception { // Read in an API that uses a resource with fields that have their own schema, then read in @@ -265,7 +287,14 @@ public void testWriteDiscovery_nonDiscoverableEndpoint() throws Exception { assertThat(result.discoveryDocs()).isEmpty(); assertThat(result.directory().getItems()).isEmpty(); } - + + @Test + public void testWriteDiscovery_OptionalEndpoint() throws Exception { + RestDescription doc = getDiscovery(new DiscoveryContext(), OptionalEndpoint.class); + RestDescription expected = readExpectedAsDiscovery("optional_endpoint.json"); + compareDiscovery(expected, doc); + } + @Test public void testDirectoryIsCloneable() throws Exception { ApiConfig config = configLoader.loadConfiguration(ServiceContext.create(), FooEndpoint.class); @@ -273,6 +302,13 @@ public void testDirectoryIsCloneable() throws Exception { result.directory().clone(); } + @Test + public void testWriteDiscovery_ValidationEndpoint() throws Exception { + RestDescription doc = getDiscovery(new DiscoveryContext(), ValidationEndpoint.class); + RestDescription expected = readExpectedAsDiscovery("validation_endpoint.json"); + compareDiscovery(expected, doc); + } + private RestDescription getDiscovery(DiscoveryContext context, Class serviceClass) throws Exception { ApiConfig config = configLoader.loadConfiguration(ServiceContext.create(), serviceClass); @@ -296,8 +332,6 @@ private DirectoryList readExpectedAsDirectory(String file) throws Exception { } private void compareDiscovery(RestDescription expected, RestDescription actual) throws Exception { - System.out.println("Actual: " + mapper.writeValueAsString(actual)); - System.out.println("Expected: " + mapper.writeValueAsString(expected)); - assertThat(actual).isEqualTo(expected); + DiscoverySubject.assertThat(actual).isSameAs(expected); } } diff --git a/endpoints-framework/src/test/java/com/google/api/server/spi/discovery/DiscoverySubject.java b/endpoints-framework/src/test/java/com/google/api/server/spi/discovery/DiscoverySubject.java new file mode 100644 index 00000000..dde856b7 --- /dev/null +++ b/endpoints-framework/src/test/java/com/google/api/server/spi/discovery/DiscoverySubject.java @@ -0,0 +1,54 @@ +package com.google.api.server.spi.discovery; + +import static com.google.common.truth.Truth.assertAbout; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectWriter; +import com.google.api.services.discovery.model.RestDescription; +import com.google.common.truth.FailureMetadata; +import com.google.common.truth.Subject; +import io.swagger.util.Json; +import java.util.Objects; +import javax.annotation.Nullable; +import org.checkerframework.checker.nullness.compatqual.NullableDecl; +import org.junit.ComparisonFailure; + +public final class DiscoverySubject extends Subject { + + private final ObjectWriter writer = Json.mapper().writerWithDefaultPrettyPrinter(); + + private final RestDescription actual; + + public static DiscoverySubject assertThat(@NullableDecl RestDescription swagger) { + return assertAbout(discoveries()).that(swagger); + } + + private static Factory discoveries() { + return DiscoverySubject::new; + } + + private DiscoverySubject(FailureMetadata failureStrategy, @Nullable Object actual) { + super(failureStrategy, actual); + this.actual = actual instanceof RestDescription ? (RestDescription) actual : null; + } + + void isSameAs(RestDescription expected) { + checkEquality(expected); + } + + private void checkEquality(RestDescription expected) { + if (!Objects.equals(actual, expected)) { + throw new ComparisonFailure("Discovery specs don't match", + toString(expected), toString(actual)); + } + } + + private String toString(RestDescription expected) { + try { + return writer.writeValueAsString(expected); + } catch (JsonProcessingException e) { + throw new AssertionError("Cannot create String representation for specs", e); + } + } + +} diff --git a/endpoints-framework/src/test/java/com/google/api/server/spi/discovery/LocalDiscoveryProviderTest.java b/endpoints-framework/src/test/java/com/google/api/server/spi/discovery/LocalDiscoveryProviderTest.java index 1b05ccd4..0e067796 100644 --- a/endpoints-framework/src/test/java/com/google/api/server/spi/discovery/LocalDiscoveryProviderTest.java +++ b/endpoints-framework/src/test/java/com/google/api/server/spi/discovery/LocalDiscoveryProviderTest.java @@ -2,9 +2,9 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyListOf; -import static org.mockito.Matchers.eq; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; import com.google.api.server.spi.config.model.ApiConfig; @@ -24,7 +24,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; /** * Tests for {@link LocalDiscoveryProvider}. @@ -43,7 +43,7 @@ public class LocalDiscoveryProviderTest { public void setUp() { provider = new LocalDiscoveryProvider(ImmutableList.of(), generator, repository); when(generator.writeDiscovery( - anyListOf(ApiConfig.class), any(DiscoveryContext.class), eq(repository))) + anyList(), any(), eq(repository))) .thenReturn(Result.builder().setDiscoveryDocs( ImmutableMap.of(new ApiKey(NAME, VERSION, null /* root */), getPlaceholderDoc())) .setDirectory(getPlaceholderDirectory()) @@ -67,16 +67,6 @@ public void getRestDocument_NotFoundException() { } } - @Test - public void getRpcDocument() { - try { - provider.getRpcDocument(ROOT, NAME, VERSION); - fail("expected NotFoundException"); - } catch (NotFoundException expected) { - // expected - } - } - @Test public void getDirectory() throws Exception { DirectoryList directory = provider.getDirectory(ROOT); diff --git a/endpoints-framework/src/test/java/com/google/api/server/spi/discovery/ProxyingDiscoveryProviderTest.java b/endpoints-framework/src/test/java/com/google/api/server/spi/discovery/ProxyingDiscoveryProviderTest.java index aec77a9e..4cd968c8 100644 --- a/endpoints-framework/src/test/java/com/google/api/server/spi/discovery/ProxyingDiscoveryProviderTest.java +++ b/endpoints-framework/src/test/java/com/google/api/server/spi/discovery/ProxyingDiscoveryProviderTest.java @@ -17,8 +17,8 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.argThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -34,11 +34,9 @@ import com.google.api.services.discovery.Discovery.Apis; import com.google.api.services.discovery.Discovery.Apis.GenerateDirectory; import com.google.api.services.discovery.Discovery.Apis.GenerateRest; -import com.google.api.services.discovery.Discovery.Apis.GenerateRpc; import com.google.api.services.discovery.model.ApiConfigs; import com.google.api.services.discovery.model.DirectoryList; import com.google.api.services.discovery.model.RestDescription; -import com.google.api.services.discovery.model.RpcDescription; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Sets; @@ -48,7 +46,7 @@ import org.junit.runner.RunWith; import org.mockito.ArgumentMatcher; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import java.io.IOException; import java.util.Set; @@ -66,9 +64,6 @@ public class ProxyingDiscoveryProviderTest { private static final RestDescription REST_DOC = new RestDescription() .setName(NAME) .setVersion(V1); - private static final RpcDescription RPC_DOC = new RpcDescription() - .setName(NAME) - .setVersion(V1); private static final DirectoryList DIRECTORY = new DirectoryList() .setItems(ImmutableList.of(new DirectoryList.Items() .setName(NAME) @@ -81,7 +76,6 @@ public class ProxyingDiscoveryProviderTest { @Mock private Discovery discovery; @Mock private Apis apis; @Mock private GenerateRest restRequest; - @Mock private GenerateRpc rpcRequest; @Mock private GenerateDirectory directoryRequest; @Mock private ApiConfigWriter configWriter; @@ -104,11 +98,9 @@ public void setUp() throws Exception { // Setup standard mocks on our discovery API. when(discovery.apis()).thenReturn(apis); - when(apis.generateRest(any(com.google.api.services.discovery.model.ApiConfig.class))) + when(apis.generateRest(any())) .thenReturn(restRequest); - when(apis.generateRpc(any(com.google.api.services.discovery.model.ApiConfig.class))) - .thenReturn(rpcRequest); - when(apis.generateDirectory(any(ApiConfigs.class))) + when(apis.generateDirectory(any())) .thenReturn(directoryRequest); // Used by individual document tests when(configWriter.writeConfig(withConfigs(rewrittenApiConfig1, rewrittenApiConfig2))) @@ -156,39 +148,6 @@ public void getRestDocument_internalServerError() throws Exception { } } - @Test - public void getRpcDocument() throws Exception { - when(rpcRequest.execute()).thenReturn(RPC_DOC); - - RpcDescription actual = provider.getRpcDocument(REWRITTEN_ROOT, NAME, V1); - - assertThat(actual).isEqualTo(RPC_DOC); - verify(apis).generateRpc( - new com.google.api.services.discovery.model.ApiConfig().setConfig(V1_JSON_API_CONFIG)); - } - - @Test - public void getRpcDocument_notFound() throws Exception { - try { - provider.getRpcDocument(REWRITTEN_ROOT, WRONG_NAME, V1); - fail("expected NotFoundException"); - } catch (NotFoundException e) { - // expected - } - } - - @Test - public void getRpcDocument_internalServerError() throws Exception { - when(rpcRequest.execute()).thenThrow(new IOException()); - - try { - provider.getRpcDocument(REWRITTEN_ROOT, NAME, V1); - fail("expected InternalServerErrorException"); - } catch (InternalServerErrorException e) { - // expected - } - } - @Test public void getDirectory() throws Exception { when(directoryRequest.execute()).thenReturn(DIRECTORY); @@ -215,18 +174,17 @@ private static Iterable withConfigs(ApiConfig... configs) { return argThat(new ConfigMatcher(Sets.newHashSet(configs))); } - private static class ConfigMatcher extends ArgumentMatcher> { + private static class ConfigMatcher implements ArgumentMatcher> { private final Set configs; ConfigMatcher(Set configs) { this.configs = configs; } - @SuppressWarnings("unchecked") @Override - public boolean matches(Object argument) { - return argument instanceof Iterable - && configs.equals(Sets.newHashSet((Iterable) argument)); + public boolean matches(Iterable argument) { + return argument != null + && configs.equals(Sets.newHashSet(argument)); } } @@ -234,7 +192,7 @@ private static ApiConfigs withConfigs(String... jsonConfigs) { return argThat(new ApiConfigsMatcher(Sets.newHashSet(jsonConfigs))); } - private static class ApiConfigsMatcher extends ArgumentMatcher { + private static class ApiConfigsMatcher implements ArgumentMatcher { private final Set configs; ApiConfigsMatcher(Set configs) { @@ -242,9 +200,9 @@ private static class ApiConfigsMatcher extends ArgumentMatcher { } @Override - public boolean matches(Object argument) { - return argument instanceof ApiConfigs - && configs.equals(Sets.newHashSet(((ApiConfigs) argument).getConfigs())); + public boolean matches(ApiConfigs argument) { + return argument != null + && configs.equals(Sets.newHashSet(argument.getConfigs())); } } diff --git a/endpoints-framework/src/test/java/com/google/api/server/spi/discovery/ProxyingDiscoveryServiceTest.java b/endpoints-framework/src/test/java/com/google/api/server/spi/discovery/ProxyingDiscoveryServiceTest.java index 058c7537..0a3be544 100644 --- a/endpoints-framework/src/test/java/com/google/api/server/spi/discovery/ProxyingDiscoveryServiceTest.java +++ b/endpoints-framework/src/test/java/com/google/api/server/spi/discovery/ProxyingDiscoveryServiceTest.java @@ -23,13 +23,12 @@ import com.google.api.server.spi.response.NotFoundException; import com.google.api.services.discovery.model.DirectoryList; import com.google.api.services.discovery.model.RestDescription; -import com.google.api.services.discovery.model.RpcDescription; import com.google.common.collect.ImmutableList; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import org.springframework.mock.web.MockHttpServletRequest; /** @@ -46,9 +45,6 @@ public class ProxyingDiscoveryServiceTest { private static final RestDescription REST_DOC = new RestDescription() .setName(API_NAME) .setVersion(API_VERSION); - private static final RpcDescription RPC_DOC = new RpcDescription() - .setName(API_NAME) - .setVersion(API_VERSION); private static final DirectoryList DIRECTORY = new DirectoryList() .setItems(ImmutableList.of(new DirectoryList.Items() .setName(API_NAME) @@ -108,58 +104,6 @@ public void getRestDocument_uninitialized() throws Exception { } } - @Test - public void getRpcDocument() throws Exception { - ProxyingDiscoveryService discoveryService = createDiscoveryService(true); - when(provider.getRpcDocument(SERVER_ROOT, API_NAME, API_VERSION)).thenReturn(RPC_DOC); - - RpcDescription actual = discoveryService.getRpcDocument( - createRequest("discovery/v1/apis/tictactoe/v1/rpc"), API_NAME, API_VERSION); - - assertThat(actual).isEqualTo(RPC_DOC); - } - - @Test - public void getRpcDocument_notFound() throws Exception { - ProxyingDiscoveryService discoveryService = createDiscoveryService(true); - when(provider.getRpcDocument(SERVER_ROOT, API_NAME, API_VERSION)) - .thenThrow(new NotFoundException("")); - - try { - discoveryService.getRpcDocument( - createRequest("discovery/v1/apis/tictactoe/v1/rpc"), API_NAME, API_VERSION); - fail("expected NotFoundException"); - } catch (NotFoundException e) { - // expected - } - } - - @Test - public void getRpcDocument_internalServerError() throws Exception { - ProxyingDiscoveryService discoveryService = createDiscoveryService(true); - when(provider.getRpcDocument(SERVER_ROOT, API_NAME, API_VERSION)) - .thenThrow(new InternalServerErrorException("")); - - try { - discoveryService.getRpcDocument( - createRequest("discovery/v1/apis/tictactoe/v1/rest"), API_NAME, API_VERSION); - fail("expected InternalServerErrorException"); - } catch (InternalServerErrorException e) { - // expected - } - } - - @Test - public void getRpcDocument_uninitialized() throws Exception { - try { - ProxyingDiscoveryService discoveryService = createDiscoveryService(false); - discoveryService.getRpcDocument(null /* request */, null /* name */, null /* verson */); - fail("expected InternalServerErrorException"); - } catch (InternalServerErrorException e) { - // expected - } - } - @Test public void getApiList() throws Exception { ProxyingDiscoveryService discoveryService = createDiscoveryService(true); diff --git a/endpoints-framework/src/test/java/com/google/api/server/spi/dispatcher/PathDispatcherTest.java b/endpoints-framework/src/test/java/com/google/api/server/spi/dispatcher/PathDispatcherTest.java index 90562830..de293842 100644 --- a/endpoints-framework/src/test/java/com/google/api/server/spi/dispatcher/PathDispatcherTest.java +++ b/endpoints-framework/src/test/java/com/google/api/server/spi/dispatcher/PathDispatcherTest.java @@ -26,7 +26,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import java.io.IOException; diff --git a/endpoints-framework/src/test/java/com/google/api/server/spi/dispatcher/PathTrieTest.java b/endpoints-framework/src/test/java/com/google/api/server/spi/dispatcher/PathTrieTest.java index cb1110b2..55318c34 100644 --- a/endpoints-framework/src/test/java/com/google/api/server/spi/dispatcher/PathTrieTest.java +++ b/endpoints-framework/src/test/java/com/google/api/server/spi/dispatcher/PathTrieTest.java @@ -16,6 +16,8 @@ package com.google.api.server.spi.dispatcher; import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.assertThrows; import static org.junit.Assert.fail; import com.google.api.server.spi.dispatcher.PathTrie.Result; @@ -71,6 +73,202 @@ public void prefix() { assertFailedGetResolution(trie, ""); } + @Test + public void customMethod() { + PathTrie trie = PathTrie.builder() + .add(HttpMethod.GET, "discovery/rest:batchGet", 1234) + .build(); + + assertSuccessfulGetResolution(trie, "discovery/rest:batchGet", 1234); + assertFailedGetResolution(trie, "discovery/rest"); + } + + @Test + public void customMethodWithEquivalentStandardPath() { + PathTrie trie = PathTrie.builder() + .add(HttpMethod.GET, "discovery/rest:batchGet", 1234) + .add(HttpMethod.GET, "discovery/rest/batchGet", 4321) + .build(); + + assertSuccessfulGetResolution(trie, "discovery/rest:batchGet", 1234); + assertSuccessfulGetResolution(trie, "discovery/rest/batchGet", 4321); + } + + @Test + public void paramInCustomMethodNotAllowed() { + assertThrows(IllegalArgumentException.class, + () -> PathTrie.builder() + .add(HttpMethod.GET, "discovery/prefix:{version}", 1234) + .build()); + assertThrows(IllegalArgumentException.class, + () -> PathTrie.builder() + .add(HttpMethod.GET, "discovery/{major}:{minor}", 1234) + .build()); + } + + @Test + public void lastParameterHasUnescapedColon() { + PathTrie trie = PathTrie.builder() + .add(HttpMethod.GET, "discovery/{version}", 1234) + .add(HttpMethod.GET, "discovery/{version}/suffix", 1234) + .add(HttpMethod.PUT, "discovery/{version}", 4321) + .build(); + + assertSuccessfulGetResolution( + trie, "discovery/v1:batchGet", 1234, ImmutableMap.of("version", "v1:batchGet")); + assertSuccessfulGetResolution( + trie, "discovery/v1:first:second", 1234, ImmutableMap.of("version", "v1:first:second")); + assertSuccessfulGetResolution( + trie, "discovery/v1:batchGet/suffix", 1234, ImmutableMap.of("version", "v1:batchGet")); + assertSuccessfulGetResolution( + trie, "discovery/v1:first:second/suffix", 1234, ImmutableMap.of("version", "v1:first:second")); + assertSuccessfulResolution( + trie, HttpMethod.PUT, "discovery/v1:batchGet", 4321, + ImmutableMap.of("version", "v1:batchGet")); + } + + @Test + public void lastParameterHasUnescapedColon_mixed() { + PathTrie trie = PathTrie.builder() + .add(HttpMethod.GET, "discovery/{version}", 1234) + .add(HttpMethod.GET, "discovery/{version}:batchGet", 4321) + .add(HttpMethod.GET, "discovery/rest", 1) + .add(HttpMethod.GET, "discovery/rest:batchGet", 2) + .build(); + + assertSuccessfulGetResolution( + trie, "discovery/v1:batchGet", 4321, ImmutableMap.of("version", "v1")); + assertSuccessfulGetResolution( + trie, "discovery/v1:notMethod", 1234, ImmutableMap.of("version", "v1:notMethod")); + assertSuccessfulGetResolution(trie, "discovery/rest:batchGet", 2); + assertSuccessfulGetResolution(trie, "discovery/rest", 1); + assertSuccessfulGetResolution( + trie, "discovery/rest:notMethod", 1234, ImmutableMap.of("version", "rest:notMethod")); + + } + + @Test + public void emptyCustomMethod() { + PathTrie trie = PathTrie.builder() + .add(HttpMethod.GET, "discovery/{version}/rest:", 1234) + .build(); + + assertFailedGetResolution(trie, "discovery/v1/rest"); + //this could be forbidden, but no harm in letting it work + assertSuccessfulGetResolution( + trie, "discovery/v1/rest:", 1234, ImmutableMap.of("version", "v1")); + } + + @Test + public void twoCustomMethod() { + assertThrows(IllegalArgumentException.class, + () -> PathTrie.builder() + .add(HttpMethod.GET, "discovery/{version}/rest:batchGet:batchGet", 1234) + .build() + ); + } + + @Test + public void intermediateCustomMethod() { + assertThrows(IllegalArgumentException.class, + () -> PathTrie.builder() + .add(HttpMethod.GET, "discovery/{version}:batchGet/rest", 1234) + .build() + ); + } + + @Test + public void customMethodWithNumericPrefix() { + //there's no specific restriction on the syntax of custom methods in Google's API guide + // (https://cloud.google.com/apis/design/custom_methods) so we should accept this + PathTrie trie = PathTrie.builder() + .add(HttpMethod.GET, "discovery/{version}/rest:12invalidMethod", 1234) + .build(); + + assertSuccessfulGetResolution( + trie, "discovery/v1/rest:12invalidMethod", 1234, ImmutableMap.of("version", "v1")); + } + + @Test + public void colonParameterAndCustomMethodInSamePath() { + //This is a known and documented limitation (see Javadoc on PathTrie) + PathTrie trie = PathTrie.builder() + .add(HttpMethod.GET, "discovery/{version}/rest:batchGet", 1234) + .build(); + + assertFailedGetResolution(trie, "discovery/v1:suffix/rest:batchGet"); + } + + @Test + public void customMethodParameter() { + PathTrie trie = PathTrie.builder() + .add(HttpMethod.GET, "discovery/{version}/rest:batchGet", 1234) + .build(); + + assertSuccessfulGetResolution( + trie, "discovery/v1/rest:batchGet", 1234, ImmutableMap.of("version", "v1")); + } + + @Test + public void customMethodMultipleParameters() { + PathTrie trie = PathTrie.builder() + .add(HttpMethod.GET, "discovery/{discovery_version}/apis/{api}/{format}:batchGet", 1234) + .build(); + + assertSuccessfulGetResolution(trie, "discovery/v1/apis/test/rest:batchGet", 1234, + ImmutableMap.of("discovery_version", "v1", "api", "test", "format", "rest")); + assertFailedGetResolution(trie, "discovery/v1/apis/test/rest"); + } + + @Test + public void customMethodSharedParameters() { + PathTrie trie = PathTrie.builder() + .add(HttpMethod.GET, "discovery/{version}/rest:batchGet", 1234) + .add(HttpMethod.GET, "discovery/{version}/rpc", 4321) + .build(); + + assertSuccessfulGetResolution( + trie, "discovery/v1/rest:batchGet", 1234, ImmutableMap.of("version", "v1")); + assertSuccessfulGetResolution( + trie, "discovery/v1/rpc", 4321, ImmutableMap.of("version", "v1")); + } + + @Test + public void parametersOnMixedMethods() { + PathTrie trie = PathTrie.builder() + .add(HttpMethod.GET, "discovery/{version}/rest:batchGet", 1234) + .add(HttpMethod.GET, "discovery/{version}/rest", 4321) + .build(); + + assertSuccessfulGetResolution( + trie, "discovery/v1/rest:batchGet", 1234, ImmutableMap.of("version", "v1")); + assertSuccessfulGetResolution( + trie, "discovery/v1/rest", 4321, ImmutableMap.of("version", "v1")); + assertFailedGetResolution(trie, "discovery/v1/rest:unknownMethod"); + } + + @Test + public void customMethodSamePathDifferentMethod() { + PathTrie trie = PathTrie.builder() + .add(HttpMethod.GET, "discovery/{version}/rest", 1324) + .add(HttpMethod.PUT, "discovery/{version}/rest", 4123) + .add(HttpMethod.GET, "discovery/{version}/rest:batchGet", 1234) + .add(HttpMethod.PUT, "discovery/{version}/rest:batchGet", 2134) + .add(HttpMethod.GET, "discovery/{version}/rest:customList", 4321) + .build(); + + assertSuccessfulGetResolution( + trie, "discovery/v1/rest", 1324, ImmutableMap.of("version", "v1")); + assertSuccessfulResolution( + trie, HttpMethod.PUT, "discovery/v1/rest", 4123, ImmutableMap.of("version", "v1")); + assertSuccessfulGetResolution( + trie, "discovery/v1/rest:batchGet", 1234, ImmutableMap.of("version", "v1")); + assertSuccessfulResolution( + trie, HttpMethod.PUT, "discovery/v1/rest:batchGet", 2134, ImmutableMap.of("version", "v1")); + assertSuccessfulGetResolution( + trie, "discovery/v2/rest:customList", 4321, ImmutableMap.of("version", "v2")); + } + @Test public void parameter() { PathTrie trie = PathTrie.builder() @@ -223,7 +421,17 @@ public void invalidParameterName() { @Test public void invalidPathParameterSyntax() { try { - PathTrie. builder().add(HttpMethod.GET, "bad/{test", 1234); + PathTrie.builder().add(HttpMethod.GET, "bad/{test", 1234); + fail("expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + // expected + } + } + + @Test + public void parameterPathWithUnexpectedPart() { + try { + PathTrie.builder().add(HttpMethod.GET, "bad/{test}unexpected/afterBad", 1234); fail("expected IllegalArgumentException"); } catch (IllegalArgumentException e) { // expected @@ -235,7 +443,7 @@ public void invalidParameterSegment() { String invalids = "?#[]{}"; for (char c : invalids.toCharArray()) { try { - PathTrie. builder().add(HttpMethod.GET, "bad/" + c, 1234); + PathTrie.builder().add(HttpMethod.GET, "bad/" + c, 1234); fail("expected IllegalArgumentException"); } catch (IllegalArgumentException e) { // expected diff --git a/endpoints-framework/src/test/java/com/google/api/server/spi/handlers/EndpointsMethodHandlerRedirectLocationTest.java b/endpoints-framework/src/test/java/com/google/api/server/spi/handlers/EndpointsMethodHandlerRedirectLocationTest.java new file mode 100644 index 00000000..923b2b56 --- /dev/null +++ b/endpoints-framework/src/test/java/com/google/api/server/spi/handlers/EndpointsMethodHandlerRedirectLocationTest.java @@ -0,0 +1,59 @@ +package com.google.api.server.spi.handlers; + +import static com.google.api.server.spi.handlers.EndpointsMethodHandler.getRedirectLocation; + +import javax.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; + +import com.google.api.server.spi.EndpointsContext; + +/** + * Tests for redirect location in {@link EndpointsMethodHandler}. + */ +@RunWith(JUnit4.class) +public class EndpointsMethodHandlerRedirectLocationTest { + + private EndpointsContext context; + private MockHttpServletRequest request; + + @Before + public void setUp() throws Exception { + request = new MockHttpServletRequest(); + request.setRequestURI("/_ah/api/resource"); + HttpServletResponse response = new MockHttpServletResponse(); + context = new EndpointsContext("", "", request, response, false); + } + + @Test + public void httpsUrl() { + Assert.assertEquals("https://example.com/resource", getRedirectLocation(context, "https://example.com/resource")); + } + + @Test + public void httpsUrlWithPort() { + Assert.assertEquals("https://example.com:8443/resource", getRedirectLocation(context, "https://example.com:8443/resource")); + } + + @Test + public void relativeToServer() { + Assert.assertEquals("/redirected/other", getRedirectLocation(context, "/redirected/other")); + } + + @Test + public void relativeToRequest() { + Assert.assertEquals("/_ah/api/resource/redirect", getRedirectLocation(context, "redirect")); + } + + @Test + public void relativeToRequest_trailingSlashInRequest() { + request.setRequestURI("/_ah/api/resource/"); + Assert.assertEquals("/_ah/api/resource/redirect", getRedirectLocation(context, "redirect")); + } +} diff --git a/endpoints-framework/src/test/java/com/google/api/server/spi/handlers/EndpointsMethodHandlerTest.java b/endpoints-framework/src/test/java/com/google/api/server/spi/handlers/EndpointsMethodHandlerTest.java index 634ab850..1fbfc47e 100644 --- a/endpoints-framework/src/test/java/com/google/api/server/spi/handlers/EndpointsMethodHandlerTest.java +++ b/endpoints-framework/src/test/java/com/google/api/server/spi/handlers/EndpointsMethodHandlerTest.java @@ -152,7 +152,7 @@ public TestMethodHandler( @Override @VisibleForTesting protected ParamReader createRestParamReader(EndpointsContext context, - ApiSerializationConfig serializationConfig) { + ApiSerializationConfig serializationConfig, Object apiService) { return new FakeParamReader(params); } diff --git a/endpoints-framework/src/test/java/com/google/api/server/spi/handlers/ExplorerHandlerTest.java b/endpoints-framework/src/test/java/com/google/api/server/spi/handlers/ExplorerHandlerTest.java index 47d59d36..5b2f0add 100644 --- a/endpoints-framework/src/test/java/com/google/api/server/spi/handlers/ExplorerHandlerTest.java +++ b/endpoints-framework/src/test/java/com/google/api/server/spi/handlers/ExplorerHandlerTest.java @@ -25,36 +25,47 @@ import javax.servlet.http.HttpServletResponse; +import java.util.Collections; +import java.util.Map; + /** * Tests for {@link ExplorerHandler}. */ public class ExplorerHandlerTest { @Test public void testHandle() throws Exception { - testHandle("http", 8080, "http://apis-explorer.appspot.com/apis-explorer/" - + "?base=http://localhost:8080/_ah/api&root=http://localhost:8080/_ah/api"); + testHandle("http", 8080, "https://apis-explorer.appspot.com/apis-explorer/" + + "?base=http://localhost:8080/_ah/api", Collections.emptyMap()); } @Test public void testHandle_explicitHttpPort() throws Exception { - testHandle("http", 80, "http://apis-explorer.appspot.com/apis-explorer/" - + "?base=http://localhost/_ah/api&root=http://localhost/_ah/api"); + testHandle("http", 80, "https://apis-explorer.appspot.com/apis-explorer/" + + "?base=http://localhost/_ah/api", Collections.emptyMap()); } @Test public void testHandle_explicitHttpsPort() throws Exception { - testHandle("https", 443, "http://apis-explorer.appspot.com/apis-explorer/" - + "?base=https://localhost/_ah/api&root=https://localhost/_ah/api"); + testHandle("https", 443, "https://apis-explorer.appspot.com/apis-explorer/" + + "?base=https://localhost/_ah/api", Collections.emptyMap()); + } + + @Test + public void testHandle_forwardedRequest() throws Exception { + testHandle("http", 80, "https://apis-explorer.appspot.com/apis-explorer/" + + "?base=https://localhost/_ah/api", Collections.singletonMap("X-Forwarded-Proto", "https")); } - private void testHandle(String scheme, int port, String expectedLocation) throws Exception { + private void testHandle(String scheme, Integer port, String expectedLocation, + Map headers) throws Exception { MockHttpServletRequest request = new MockHttpServletRequest(); request.setScheme(scheme); request.setServerName("localhost"); request.setServerPort(port); request.setRequestURI("/_ah/api/explorer/"); + headers.forEach(request::addHeader); MockHttpServletResponse response = new MockHttpServletResponse(); - ExplorerHandler handler = new ExplorerHandler(); + ExplorerHandler handler = new ExplorerHandler(null); EndpointsContext context = new EndpointsContext("GET", "explorer", request, response, true); handler.handle(context); diff --git a/endpoints-framework/src/test/java/com/google/api/server/spi/request/AttributeTest.java b/endpoints-framework/src/test/java/com/google/api/server/spi/request/AttributeTest.java index bd6f69ce..44a441fd 100644 --- a/endpoints-framework/src/test/java/com/google/api/server/spi/request/AttributeTest.java +++ b/endpoints-framework/src/test/java/com/google/api/server/spi/request/AttributeTest.java @@ -29,7 +29,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import org.springframework.mock.web.MockHttpServletRequest; import javax.servlet.ServletConfig; @@ -52,31 +52,17 @@ public void setUp() { request = new MockHttpServletRequest(); } - @Test - public void bindStandardRequestAttributes_restricted() throws Exception { - when(methodConfig.getClientIds()).thenReturn(null); - - ServletInitializationParameters initParams = - createInitParams(true /* restricted */, true /* clientIdWhitelistEnabled */); - Attribute attr = Attribute.bindStandardRequestAttributes(request, methodConfig, initParams); - assertTrue(attr.isEnabled(Attribute.RESTRICT_SERVLET)); - - initParams = createInitParams(false /* restricted */, true /* clientIdWhitelistEnabled */); - attr = Attribute.bindStandardRequestAttributes(request, methodConfig, initParams); - assertFalse(attr.isEnabled(Attribute.RESTRICT_SERVLET)); - } - @Test public void bindStandardRequestAttributes_clientIdWhitelist() throws Exception { when(methodConfig.getClientIds()).thenReturn(null); ServletInitializationParameters initParams = - createInitParams(true /* restricted */, true /* clientIdWhitelistEnabled */); + createInitParams(true /* clientIdWhitelistEnabled */); Attribute attr = Attribute.bindStandardRequestAttributes(request, methodConfig, initParams); assertTrue(attr.isEnabled(Attribute.ENABLE_CLIENT_ID_WHITELIST)); attr.remove(Attribute.ENABLE_CLIENT_ID_WHITELIST); - initParams = createInitParams(true /* restricted */, false /* clientIdWhitelistEnabled */); + initParams = createInitParams(false /* clientIdWhitelistEnabled */); attr = Attribute.bindStandardRequestAttributes(request, methodConfig, initParams); assertFalse(attr.isEnabled(Attribute.ENABLE_CLIENT_ID_WHITELIST)); } @@ -84,20 +70,19 @@ public void bindStandardRequestAttributes_clientIdWhitelist() throws Exception { @Test public void bindStandardRequestAttributes_skipTokenAuth() throws Exception { ServletInitializationParameters initParams = - createInitParams(true /* restricted */, true /* clientIdWhitelistEnabled */); + createInitParams(true /* clientIdWhitelistEnabled */); when(methodConfig.getClientIds()).thenReturn(null); Attribute attr = Attribute.bindStandardRequestAttributes(request, methodConfig, initParams); assertTrue(attr.isEnabled(Attribute.SKIP_TOKEN_AUTH)); attr.remove(Attribute.SKIP_TOKEN_AUTH); - initParams = createInitParams(true /* restricted */, true /* clientIdWhitelistEnabled */); + initParams = createInitParams(true /* clientIdWhitelistEnabled */); when(methodConfig.getClientIds()).thenReturn(ImmutableList.of("clientId")); attr = Attribute.bindStandardRequestAttributes(request, methodConfig, initParams); assertFalse(attr.isEnabled(Attribute.SKIP_TOKEN_AUTH)); attr.remove(Attribute.SKIP_TOKEN_AUTH); - initParams = createInitParams(true /* restricted */, false /* clientIdWhitelistEnabled */); - when(methodConfig.getClientIds()).thenReturn(ImmutableList.of("clientId")); + initParams = createInitParams(false /* clientIdWhitelistEnabled */); attr = Attribute.bindStandardRequestAttributes(request, methodConfig, initParams); assertFalse(attr.isEnabled(Attribute.SKIP_TOKEN_AUTH)); } @@ -106,7 +91,7 @@ public void bindStandardRequestAttributes_skipTokenAuth() throws Exception { public void bindStandardRequestAttributes_apiMethodConfig() throws Exception { when(methodConfig.getClientIds()).thenReturn(null); ServletInitializationParameters initParams = - createInitParams(true /* restricted */, true /* clientIdWhitelistEnabled */); + createInitParams(true /* clientIdWhitelistEnabled */); Attribute attr = Attribute.bindStandardRequestAttributes(request, methodConfig, initParams); assertEquals(attr.get(Attribute.API_METHOD_CONFIG), methodConfig); } @@ -117,10 +102,8 @@ public void bindStandardRequestAttributes_apiMethodConfig() throws Exception { * * @throws ServletException */ - protected ServletInitializationParameters createInitParams(boolean restricted, - boolean clientIdWhitelistEnabled) throws Exception { + protected ServletInitializationParameters createInitParams(boolean clientIdWhitelistEnabled) throws Exception { return ServletInitializationParameters.builder() - .setRestricted(restricted) .setClientIdWhitelistEnabled(clientIdWhitelistEnabled) .build(); } diff --git a/endpoints-framework/src/test/java/com/google/api/server/spi/request/AuthTest.java b/endpoints-framework/src/test/java/com/google/api/server/spi/request/AuthTest.java index e1741447..4aecc39d 100644 --- a/endpoints-framework/src/test/java/com/google/api/server/spi/request/AuthTest.java +++ b/endpoints-framework/src/test/java/com/google/api/server/spi/request/AuthTest.java @@ -37,7 +37,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import org.springframework.mock.web.MockHttpServletRequest; import java.util.List; @@ -155,4 +155,22 @@ public void testAuthenticate_appEngineUser_CustomAuth() throws Exception { assertNull(attr.get(Attribute.AUTHENTICATED_APPENGINE_USER)); assertEquals(AppEngineAuthenticator.APP_ENGINE_USER, auth.authenticateAppEngineUser()); } + + @Test + public void testAuthenticateAppEngineUser_notOnAppEngine_forceAuthDisabled() throws Exception { + System.clearProperty(EnvUtil.ENV_APPENGINE_RUNTIME); + System.clearProperty(EnvUtil.FORCE_AUTHENTICATION_ENABLED); + + assertNull(auth.authenticateAppEngineUser()); + } + + @Test + public void testAuthenticateAppEngineUser_notOnAppEngine_forceAuthEnabled() throws Exception { + System.clearProperty(EnvUtil.ENV_APPENGINE_RUNTIME); + System.setProperty(EnvUtil.FORCE_AUTHENTICATION_ENABLED, "true"); + when(config.getAuthenticators()) + .thenReturn(ImmutableList.of(PassAuthenticator.class)); + assertEquals(AppEngineAuthenticator.APP_ENGINE_USER, auth.authenticateAppEngineUser()); + System.clearProperty(EnvUtil.FORCE_AUTHENTICATION_ENABLED); + } } diff --git a/endpoints-framework/src/test/java/com/google/api/server/spi/request/RestServletRequestParamReaderTest.java b/endpoints-framework/src/test/java/com/google/api/server/spi/request/RestServletRequestParamReaderTest.java index e55ebc15..2fa056ba 100644 --- a/endpoints-framework/src/test/java/com/google/api/server/spi/request/RestServletRequestParamReaderTest.java +++ b/endpoints-framework/src/test/java/com/google/api/server/spi/request/RestServletRequestParamReaderTest.java @@ -16,11 +16,15 @@ package com.google.api.server.spi.request; import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; import com.google.api.server.spi.EndpointMethod; import com.google.api.server.spi.EndpointsContext; import com.google.api.server.spi.ServiceContext; +import com.google.api.server.spi.ServiceException; +import com.google.api.server.spi.ServletInitializationParameters; import com.google.api.server.spi.TypeLoader; import com.google.api.server.spi.config.Api; import com.google.api.server.spi.config.ApiMethod; @@ -38,6 +42,7 @@ import com.google.common.collect.ImmutableMap; import java.util.ArrayList; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -48,6 +53,8 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.Date; import java.util.List; import java.util.Map; import java.util.Objects; @@ -62,17 +69,19 @@ public class RestServletRequestParamReaderTest { public static final SimpleDate NOV_2 = new SimpleDate(2015, 11, 2); public static final SimpleDate NOV_1 = new SimpleDate(2015, 11, 1); + private TestApi service; private EndpointMethod endpointMethod; private MockHttpServletRequest request; private ApiSerializationConfig serializationConfig; private ApiConfig apiConfig; private ApiMethodConfig methodConfig; - + @Before public void setUp() throws Exception { + service = new TestApi(); endpointMethod = EndpointMethod.create(TestApi.class, - TestApi.class.getMethod("test", Long.TYPE, List.class, SimpleDate.class, - TestResource.class)); + TestApi.class.getMethod("test", Long.TYPE, List.class, TestEnum.class, + SimpleDate.class, TestResource.class)); request = new MockHttpServletRequest(); ServiceContext serviceContext = ServiceContext.create(); serializationConfig = new ApiSerializationConfig(); @@ -99,6 +108,7 @@ public void repeatedQueryParameter() throws Exception { .containsExactly( 1234L, ImmutableList.of(NOV_1, NOV_2), + null, NOV_2, new TestResource()) .inOrder(); @@ -115,6 +125,7 @@ public void defaultValue() throws Exception { .containsExactly( 1234L, null, + null, JAN_1, new TestResource()); } @@ -134,6 +145,7 @@ public void resourceOverridesQuery() throws Exception { .containsExactly( 1234L, null, + null, NOV_2, new TestResource(NOV_2)) .inOrder(); @@ -152,6 +164,7 @@ public void queryOverridesPath() throws Exception { .containsExactly( 4321L, null, + null, NOV_2, new TestResource()) .inOrder(); @@ -169,7 +182,130 @@ public void nonObjectRequest() throws Exception { // expected } } + + @Test + public void parseIntegerError() throws ServiceException { + checkContentParseError("{\"objInt\":\"invalid\"}", "field 'objInt'", "int", "invalid number"); + } + + @Test + public void parseIntError() throws ServiceException { + checkContentParseError("{\"simpleInt\":\"invalid\"}", "field 'simpleInt'", "int", + "invalid number value \"invalid\""); + } + + @Test + public void parseLongError() throws ServiceException { + checkContentParseError("{\"objLong\":\"invalid\"}", "field 'objLong'", "long", "invalid number"); + } + + @Test + public void parsePrimitiveLongError() throws ServiceException { + checkContentParseError("{\"simpleLong\":\"invalid\"}", "field 'simpleLong'", "long", + "invalid number value \"invalid\""); + } + + @Test + public void parseFloatError() throws ServiceException { + checkContentParseError("{\"objFloat\":\"invalid\"}", "field 'objFloat'", "Float", "invalid number"); + } + + @Test + public void parsePrimitiveFloatError() throws ServiceException { + checkContentParseError("{\"simpleFloat\":\"invalid\"}", "field 'simpleFloat'", "float", + "invalid number value \"invalid\""); + } + + @Test + public void parseDoubleError() throws ServiceException { + checkContentParseError("{\"objDouble\":\"invalid\"}", "field 'objDouble'", "Double", "invalid number"); + } + + @Test + public void parsePrimitiveDoubleError() throws ServiceException { + checkContentParseError("{\"simpleDouble\":\"invalid\"}", "field 'simpleDouble'", "double", + "invalid number value \"invalid\""); + } + + @Test + public void parseBooleanError() throws ServiceException { + checkContentParseError("{\"objBoolean\":\"invalid\"}", "field 'objBoolean'", "Boolean", + "invalid boolean value"); + } + + @Test + public void parsePrimitiveBooleanError() throws ServiceException { + checkContentParseError("{\"simpleBoolean\":\"invalid\"}", "field 'simpleBoolean'", "boolean", + "invalid boolean value \"invalid\""); + } + + @Test + public void parseEnumError() throws ServiceException { + checkContentParseError("{\"simpleEnum\":\"invalidEnum\"}", "field 'simpleEnum'", "TestEnum", + "invalid enum value \"invalidEnum\". Valid values are [One, Two, Three]"); + } + + @Test + public void parseDateError() throws ServiceException { + checkContentParseError("{\"objDate\":\"invalidDate\"}", "field 'objDate'", "Date", + "invalid date value \"invalidDate\"."); + } + + @Test + public void parseNestedError() throws ServiceException { + checkContentParseError("{\"nested\":{\"simpleInt\": \"abc\"}}", "field 'nested.simpleInt'", "int", + "invalid number value \"abc\"."); + } + + @Test + public void parseArrayError() throws ServiceException { + checkContentParseError("{\"array\":123}", "field 'array'", "int[]", ""); + } + + @Test + public void parseArrayElementError() throws ServiceException { + checkContentParseError("{\"array\":[123, \"abc\"]}", "field 'array[1]'", "int", + "invalid number value \"abc\"."); + } + + @Test + public void parseNestedArrayError() throws ServiceException { + checkContentParseError("{\"nestedArray\":[{\"simpleInt\": 123},123]}", "field 'nestedArray[1]'", "NestedResource", ""); + } + + @Test + public void parseNestedArrayElementError() throws ServiceException { + checkContentParseError("{\"nestedArray\":[{\"simpleInt\": 123},{\"simpleInt\": \"abc\"}]}", "field 'nestedArray[1].simpleInt'", "int", + "invalid number value \"abc\"."); + } + + @Test + public void parseIntAsParamError() throws ServiceException { + RestServletRequestParamReader reader = createReader(ImmutableMap.of("path", "abcd")); + checkParseError("a parameter", "long", "invalid number value \"abcd\"", reader); + } + + @Test + public void parseEnumAsParamError() throws ServiceException { + request.addParameter("enum", "invalidEnum"); + RestServletRequestParamReader reader = createReader(ImmutableMap.of("path", "1234")); + + checkParseError("a parameter", "TestEnum", + "invalid enum value \"invalidEnum\". Valid values are [One, Two, Three]", reader); + } + + @Test + public void parseError() throws ServiceException { + request.setContent("{\"field\": \"this is an invalid json".getBytes(StandardCharsets.UTF_8)); + RestServletRequestParamReader reader = createReader(ImmutableMap.of("path", "1234")); + + BadRequestException e = Assert.assertThrows(BadRequestException.class, + reader::read + ); + Assert.assertEquals("Parse error", e.getMessage()); + } + @Test public void gzippedRequest() throws Exception { request.addParameter("path", "1234"); @@ -185,6 +321,7 @@ public void gzippedRequest() throws Exception { .containsExactly( 1234L, null, + null, JAN_1, new TestResource(NOV_2)) .inOrder(); @@ -204,6 +341,51 @@ public void arrayPathParam() throws Exception { .containsExactly(ImmutableList.of("4", "3", "2", "1")); } + @Test + public void contentTypeValidationEnabled_valid() throws Exception { + request.setContent("{\"objInt\":42}".getBytes(StandardCharsets.UTF_8)); + request.setContentType("application/json"); + endpointMethod = EndpointMethod.create(TestApi.class, TestApi.class.getMethod("testContentType", TestResource.class)); + RestServletRequestParamReader reader = createReader(Collections.emptyMap(), ServletInitializationParameters.builder().setContentTypeValidationEnabled(true).build()); + + Object[] params = reader.read(); + assertThat(params).hasLength(endpointMethod.getParameterClasses().length); + TestResource resource = (TestResource) params[0]; + assertEquals(new Integer(42), resource.objInt); + } + + @Test + public void contentTypeValidationEnabled_invalid() throws Exception { + try { + request.setContent("{\"objInt\":42}".getBytes(StandardCharsets.UTF_8)); + request.setContentType("application/xml"); + endpointMethod = EndpointMethod.create(TestApi.class, TestApi.class.getMethod("testContentType", TestResource.class)); + RestServletRequestParamReader reader = createReader(Collections.emptyMap(), ServletInitializationParameters.builder().setContentTypeValidationEnabled(true).build()); + + reader.read(); + fail("expected ServiceException"); + } catch (ServiceException e) { + assertEquals(406, e.getStatusCode()); + assertThat(e.getMessage()).contains("Expecting application/json"); + } + } + + @Test + public void contentTypeValidationEnabled_empty() throws Exception { + try { + request.setContent("{\"objInt\":42}".getBytes(StandardCharsets.UTF_8)); + request.setContentType(null); + endpointMethod = EndpointMethod.create(TestApi.class, TestApi.class.getMethod("testContentType", TestResource.class)); + RestServletRequestParamReader reader = createReader(Collections.emptyMap(), ServletInitializationParameters.builder().setContentTypeValidationEnabled(true).build()); + + reader.read(); + fail("expected ServiceException"); + } catch (ServiceException e) { + assertEquals(406, e.getStatusCode()); + assertThat(e.getMessage()).contains("Expecting application/json"); + } + } + @Test public void multipartFormData() throws Exception { endpointMethod = EndpointMethod.create(TestApi.class, @@ -228,18 +410,67 @@ public void multipartFormData() throws Exception { assertThat(params).asList() .containsExactly("test", 1234); } - + private RestServletRequestParamReader createReader(Map rawPathParameters) { + return createReader(rawPathParameters, ServletInitializationParameters.builder().build()); + } + + private RestServletRequestParamReader createReader(Map rawPathParameters, ServletInitializationParameters initializationParameters) { EndpointsContext endpointsContext = new EndpointsContext("GET", "/", request, new MockHttpServletResponse(), true); endpointsContext.setRawPathParameters(rawPathParameters); - return new RestServletRequestParamReader(endpointMethod, endpointsContext, null, - serializationConfig, methodConfig); + return new RestServletRequestParamReader(service, endpointMethod, endpointsContext, null, + serializationConfig, methodConfig, initializationParameters); } + private void checkContentParseError(String content, String location, String type, String details) + throws ServiceException { + request.setContent(content.getBytes(StandardCharsets.UTF_8)); + RestServletRequestParamReader reader = createReader(ImmutableMap.of("path", "1234")); + + checkParseError(location, type, details, reader); + } + + private void checkParseError(String location, String type, String details, + RestServletRequestParamReader reader) throws ServiceException { + BadRequestException e = Assert.assertThrows(BadRequestException.class, + reader::read + ); + assertThat(e.getMessage()).contains("for " + location); + assertThat(e.getMessage()).contains("of type '" + type + "'"); + assertThat(e.getMessage()).contains(details); + } + + public enum TestEnum { + One, Two, Three + } + + public static class NestedResource { + public int simpleInt; + } + public static class TestResource { public SimpleDate query; - + + public int simpleInt; + public long simpleLong; + public float simpleFloat; + public double simpleDouble; + public boolean simpleBoolean; + + public Integer objInt; + public Long objLong; + public Float objFloat; + public Double objDouble; + public Boolean objBoolean; + + public Date objDate; + + public TestEnum simpleEnum; + public NestedResource nested; + public int[] array; + public NestedResource[] nestedArray; + public TestResource() {} public TestResource(SimpleDate query) { @@ -258,6 +489,7 @@ public static class TestApi { public void test( @Nullable @Named("path") long path, @Nullable @Named("dates") List dates, + @Nullable @Named("enum") TestEnum enumValue, @Named("defaultvalue") @DefaultValue("2015-01-01") SimpleDate defaultValue, TestResource resource) { } @@ -268,7 +500,14 @@ public void test( path = "testArrayPathParam/{values}") public void testArrayPathParam(@Named("values") ArrayList values) { } - + + @ApiMethod( + name = "testContentType", + httpMethod = HttpMethod.POST, + path = "testContentType}") + public void testContentType(@Nullable TestResource input) { + } + @ApiMethod( name = "testFormData", httpMethod = HttpMethod.POST, diff --git a/endpoints-framework/src/test/java/com/google/api/server/spi/request/ServletRequestParamReaderTest.java b/endpoints-framework/src/test/java/com/google/api/server/spi/request/ServletRequestParamReaderTest.java index 5061f134..dff17fca 100644 --- a/endpoints-framework/src/test/java/com/google/api/server/spi/request/ServletRequestParamReaderTest.java +++ b/endpoints-framework/src/test/java/com/google/api/server/spi/request/ServletRequestParamReaderTest.java @@ -25,6 +25,7 @@ import com.google.api.server.spi.EndpointMethod; import com.google.api.server.spi.EndpointsContext; +import com.google.api.server.spi.ServletInitializationParameters; import com.google.api.server.spi.auth.common.User; import com.google.api.server.spi.config.AuthLevel; import com.google.api.server.spi.config.Named; @@ -47,22 +48,29 @@ import java.util.Arrays; import java.util.Calendar; import java.util.Collection; +import java.util.Collections; import java.util.Date; import java.util.GregorianCalendar; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.TimeZone; import javax.servlet.ServletContext; import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.Mockito; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; /** * Tests for {@link ServletRequestParamReader}. @@ -110,9 +118,9 @@ public void testRead() throws Exception { .put(TestEndpoint.NAME_LONG_OBJECT, String.valueOf(VALUE_LONG)) .put(TestEndpoint.NAME_FLOAT_OBJECT, String.valueOf(VALUE_FLOAT)) .put(TestEndpoint.NAME_DOUBLE_OBJECT, String.valueOf(VALUE_DOUBLE)) - .put("stringValue", "321") - .put("integerValue", "321") - .put("more", "999").build()); + .put("more", "999").build(), ImmutableMap.of( + "stringValue", "321", + "integerValue", "321")); assertEquals(VALUE_STRING, params[0]); assertEquals(VALUE_BOOLEAN, params[1]); @@ -136,7 +144,7 @@ public void testRead() throws Exception { public void testReadDate() throws Exception { Method method = TestEndpoint.class.getDeclaredMethod("getDate", Date.class); Object[] params = - readParameters("{" + TestEndpoint.NAME_DATE + ":\"1970-01-01T00:00:00Z\"}", method); + readParameters("{" + TestEndpoint.NAME_DATE + ":\"1970-01-01T00:00:00Z\"}", method, new TestEndpoint()); assertEquals(1, params.length); assertEquals(new Date(0), params[0]); @@ -156,8 +164,7 @@ public void testReadMissingParameters() throws Exception { .put(TestEndpoint.NAME_INTEGER_OBJECT, String.valueOf(VALUE_INTEGER)) .put(TestEndpoint.NAME_FLOAT, String.valueOf(VALUE_FLOAT)) .put(TestEndpoint.NAME_FLOAT_OBJECT, String.valueOf(VALUE_FLOAT)) - .put("stringValue", "321") - .put("more", "999").build()); + .put("more", "999").build(), ImmutableMap.of("stringValue", "321")); assertEquals(VALUE_STRING, params[0]); assertEquals(VALUE_BOOLEAN, params[1]); @@ -177,7 +184,7 @@ public void testReadMissingParameters() throws Exception { assertEquals(request, params[14]); } - private Object[] readExecuteMethod(ImmutableMap parameters) throws Exception { + private Object[] readExecuteMethod(ImmutableMap parameters, ImmutableMap resource) throws Exception { Method method = TestEndpoint.class.getDeclaredMethod("succeed", String.class, boolean.class, int.class, long.class, float.class, double.class, Boolean.class, Integer.class, Long.class, Float.class, Double.class, @@ -187,8 +194,16 @@ private Object[] readExecuteMethod(ImmutableMap parameters) thro for (Map.Entry entry : parameters.entrySet()) { builder.append(String.format("\"%s\":%s,", entry.getKey(), entry.getValue())); } - builder.replace(builder.length() - 1, builder.length(), "}"); - Object[] params = readParameters(builder.toString(), method); + if (!resource.isEmpty()) { + builder.append("\"resource\":{"); + for (Map.Entry entry : resource.entrySet()) { + builder.append(String.format("\"%s\":%s,", entry.getKey(), entry.getValue())); + } + builder.replace(builder.length() - 1, builder.length(), "}}"); + } else { + builder.replace(builder.length() - 1, builder.length(), "}"); + } + Object[] params = readParameters(builder.toString(), method, new TestEndpoint()); assertEquals(15, params.length); return params; } @@ -199,7 +214,7 @@ public void testReadDateAndTime() throws Exception { String dateAndTimeString = "2002-10-02T10:00:00-05:00"; Object[] params = readParameters( - "{" + TestEndpoint.NAME_DATE_AND_TIME + ":\"" + dateAndTimeString + "\"}", method); + "{" + TestEndpoint.NAME_DATE_AND_TIME + ":\"" + dateAndTimeString + "\"}", method, new TestEndpoint()); assertEquals(1, params.length); assertEquals(DateAndTime.parseRfc3339String(dateAndTimeString), params[0]); @@ -211,7 +226,7 @@ public void testReadSimpleDate_success() throws Exception { Method method = TestEndpoint.class.getDeclaredMethod("getSimpleDate", SimpleDate.class); Object[] params = null; params = readParameters( - "{" + TestEndpoint.NAME_DATE_AND_TIME + ":\"2002-10-02\"}", method); + "{" + TestEndpoint.NAME_DATE_AND_TIME + ":\"2002-10-02\"}", method, new TestEndpoint()); assertThat(Arrays.asList(params)).containsExactly(new SimpleDate(2002, 10, 2)).inOrder(); } @@ -233,7 +248,7 @@ public void testReadSimpleDate_invalidFormat() throws Exception { @Test public void testReadNoParameters() throws Exception { Method method = TestEndpoint.class.getDeclaredMethod("getResultNoParams"); - Object[] params = readParameters("", method); + Object[] params = readParameters("", method, new TestEndpoint()); assertEquals(0, params.length); } @@ -241,7 +256,7 @@ public void testReadNoParameters() throws Exception { public void testReadByteArrayParameter() throws Exception { Method method = TestEndpoint.class.getDeclaredMethod("doSomething", byte[].class); - Object[] params = readParameters("{\"bytes\":\"AQIDBA==\"}", method); + Object[] params = readParameters("{\"bytes\":\"AQIDBA==\"}", method, new TestEndpoint()); assertEquals(1, params.length); assertThat((byte[]) params[0]).isEqualTo(new byte[]{1, 2, 3, 4}); @@ -251,7 +266,7 @@ public void testReadByteArrayParameter() throws Exception { public void testReadBlobParameter() throws Exception { Method method = TestEndpoint.class.getDeclaredMethod("doBlob", Blob.class); - Object[] params = readParameters("{\"blob\":\"AQIDBA==\"}", method); + Object[] params = readParameters("{\"blob\":\"AQIDBA==\"}", method, new TestEndpoint()); assertEquals(1, params.length); assertThat(((Blob) params[0]).getBytes()).isEqualTo(new byte[]{1, 2, 3, 4}); @@ -260,7 +275,7 @@ public void testReadBlobParameter() throws Exception { @Test public void testReadEnumParameter() throws Exception { Method method = TestEndpoint.class.getDeclaredMethod("doEnum", TestEndpoint.TestEnum.class); - Object[] params = readParameters("{" + TestEndpoint.NAME_ENUM + ":\"TEST1\"}", method); + Object[] params = readParameters("{" + TestEndpoint.NAME_ENUM + ":\"TEST1\"}", method, new TestEndpoint()); assertEquals(1, params.length); assertEquals(TestEndpoint.TestEnum.TEST1, params[0]); @@ -274,7 +289,7 @@ public void collection(@Nullable @Named("list") List integers) {} } Method method = Test.class.getDeclaredMethod("collection", List.class); - Object[] params = readParameters("{}", method); + Object[] params = readParameters("{}", method, new Test()); assertEquals(1, params.length); @SuppressWarnings("unchecked") List integers = (List) params[0]; @@ -289,7 +304,7 @@ public void collection(@Nullable @Named("integer") Integer[] integers) {} } Method method = Test.class.getDeclaredMethod("collection", Integer[].class); - Object[] params = readParameters("{}", method); + Object[] params = readParameters("{}", method, new Test()); assertEquals(1, params.length); @SuppressWarnings("unchecked") Integer[] integers = (Integer[]) params[0]; @@ -304,7 +319,7 @@ public void collection(@Named("collection") Collection integers) {} } Method method = Test.class.getDeclaredMethod("collection", Collection.class); doTestCollectionParameter( - "collection", EndpointMethod.create(Test.class, method)); + "collection", EndpointMethod.create(Test.class, method), new Test()); } @Test @@ -316,7 +331,7 @@ public void collection(@Named("collection") Collection integers) {} class Test extends TestGeneric {} doTestCollectionParameter("collection", EndpointMethod.create( Test.class, Test.class.getMethod("collection", Collection.class), - TypeToken.of(Test.class).getSupertype(TestGeneric.class))); + TypeToken.of(Test.class).getSupertype(TestGeneric.class)), new Test()); } @Test @@ -327,7 +342,7 @@ public void collection(@Named("list") List integers) {} } Method method = Test.class.getDeclaredMethod("collection", List.class); doTestCollectionParameter( - "list", EndpointMethod.create(Test.class, method)); + "list", EndpointMethod.create(Test.class, method), new Test()); } @Test @@ -338,7 +353,7 @@ public void collection(@Named("set") Set integers) {} } Method method = Test.class.getDeclaredMethod("collection", Set.class); doTestSetParameter( - "set", EndpointMethod.create(Test.class, method)); + "set", EndpointMethod.create(Test.class, method), new Test()); } @Test @@ -349,11 +364,11 @@ public void array(@Named("array") Integer[] integers) {} } Method method = Test.class.getDeclaredMethod("array", Integer[].class); doTestReadArrayParameter( - "array", EndpointMethod.create(Test.class, method)); + "array", EndpointMethod.create(Test.class, method), new Test()); } - private void doTestCollectionParameter(String name, EndpointMethod method) throws Exception { - Object[] params = readParameters("{\"" + name + "\":[1,2,3]}", method); + private void doTestCollectionParameter(String name, EndpointMethod method, Object service) throws Exception { + Object[] params = readParameters("{\"" + name + "\":[1,2,3]}", method, service); assertEquals(1, params.length); @SuppressWarnings("unchecked") @@ -374,11 +389,11 @@ public void array(@Named("array") T[] integers) {} class Test extends TestGeneric {} doTestReadArrayParameter("array", EndpointMethod.create( Test.class, Test.class.getMethod("array", Object[].class), - TypeToken.of(Test.class).getSupertype(TestGeneric.class))); + TypeToken.of(Test.class).getSupertype(TestGeneric.class)), new Test()); } - private void doTestSetParameter(String name, EndpointMethod method) throws Exception { - Object[] params = readParameters("{\"" + name + "\":[1,2,1]}", method); + private void doTestSetParameter(String name, EndpointMethod method, Object service) throws Exception { + Object[] params = readParameters("{\"" + name + "\":[1,2,1]}", method, service); assertEquals(1, params.length); @SuppressWarnings("unchecked") @@ -387,8 +402,8 @@ private void doTestSetParameter(String name, EndpointMethod method) throws Excep assertTrue(integers.contains(1)); } - private void doTestReadArrayParameter(String name, EndpointMethod method) throws Exception { - Object[] params = readParameters("{\"" + name + "\":[1,2,3]}", method); + private void doTestReadArrayParameter(String name, EndpointMethod method, Object service) throws Exception { + Object[] params = readParameters("{\"" + name + "\":[1,2,3]}", method, service); assertEquals(1, params.length); Integer[] integers = (Integer[]) params[0]; @@ -406,7 +421,7 @@ public void collection(@Named("collection") Collection dates) {} } Method method = Test.class.getDeclaredMethod("collection", Collection.class); doTestReadCollectionDateParameter( - "collection", EndpointMethod.create(Test.class, method)); + "collection", EndpointMethod.create(Test.class, method), new Test()); } @Test @@ -418,13 +433,13 @@ public void collection(@Named("collection") Collection dates) {} class Test extends TestGeneric {} doTestReadCollectionDateParameter("collection", EndpointMethod.create( Test.class, Test.class.getMethod("collection", Collection.class), - TypeToken.of(Test.class).getSupertype(TestGeneric.class))); + TypeToken.of(Test.class).getSupertype(TestGeneric.class)), new Test()); } private void doTestReadCollectionDateParameter( - String name, EndpointMethod method) throws Exception { + String name, EndpointMethod method, Object service) throws Exception { Object[] params = readParameters( - "{\"" + name + "\":[\"2002-10-01\",\"2002-10-02\",\"2002-10-03\"]}", method); + "{\"" + name + "\":[\"2002-10-01\",\"2002-10-02\",\"2002-10-03\"]}", method, service); assertEquals(1, params.length); @SuppressWarnings("unchecked") @@ -444,7 +459,7 @@ public void array(@Named("array") Date[] dates) {} } Method method = Test.class.getDeclaredMethod("array", Date[].class); doTestReadArrayDateParameter( - "array", EndpointMethod.create(Test.class, method)); + "array", EndpointMethod.create(Test.class, method), new Test()); } @Test @@ -456,12 +471,12 @@ public void array(@Named("array") T[] dates) {} class Test extends TestGeneric {} doTestReadArrayDateParameter("array", EndpointMethod.create( Test.class, Test.class.getMethod("array", Object[].class), - TypeToken.of(Test.class).getSupertype(TestGeneric.class))); + TypeToken.of(Test.class).getSupertype(TestGeneric.class)), new Test()); } - private void doTestReadArrayDateParameter(String name, EndpointMethod method) throws Exception { + private void doTestReadArrayDateParameter(String name, EndpointMethod method, Object service) throws Exception { Object[] params = readParameters( - "{\"" + name + "\":[\"2002-10-01\",\"2002-10-02\",\"2002-10-03\"]}", method); + "{\"" + name + "\":[\"2002-10-01\",\"2002-10-02\",\"2002-10-03\"]}", method, service); assertEquals(1, params.length); Date[] dates = (Date[]) params[0]; @@ -483,7 +498,7 @@ public void collection(@Named("collection") Collection outcomes) {} } Method method = Test.class.getDeclaredMethod("collection", Collection.class); doTestReadCollectionEnumParameter( - "collection", EndpointMethod.create(Test.class, method)); + "collection", EndpointMethod.create(Test.class, method), new Test()); } @Test @@ -495,12 +510,12 @@ public void collection(@Named("collection") Collection outcomes) {} class Test extends TestGeneric {} doTestReadCollectionEnumParameter("collection", EndpointMethod.create( Test.class, Test.class.getMethod("collection", Collection.class), - TypeToken.of(Test.class).getSupertype(TestGeneric.class))); + TypeToken.of(Test.class).getSupertype(TestGeneric.class)), new Test()); } private void doTestReadCollectionEnumParameter( - String name, EndpointMethod method) throws Exception { - Object[] params = readParameters("{\"" + name + "\":[\"WON\",\"LOST\",\"TIE\"]}", method); + String name, EndpointMethod method, Object service) throws Exception { + Object[] params = readParameters("{\"" + name + "\":[\"WON\",\"LOST\",\"TIE\"]}", method, service); assertEquals(1, params.length); @SuppressWarnings("unchecked") @@ -519,7 +534,7 @@ class Test { public void array(@Named("array") Outcome[] outcomes) {} } Method method = Test.class.getDeclaredMethod("array", Outcome[].class); - doTestReadArrayEnumParameter("array", EndpointMethod.create(Test.class, method)); + doTestReadArrayEnumParameter("array", EndpointMethod.create(Test.class, method), new Test()); } @Test @@ -531,11 +546,11 @@ public void array(@Named("array") T[] outcomes) {} class Test extends TestGeneric {} doTestReadArrayEnumParameter("array", EndpointMethod.create( Test.class, Test.class.getMethod("array", Object[].class), - TypeToken.of(Test.class).getSupertype(TestGeneric.class))); + TypeToken.of(Test.class).getSupertype(TestGeneric.class)), new Test()); } - private void doTestReadArrayEnumParameter(String name, EndpointMethod method) throws Exception { - Object[] params = readParameters("{\"" + name + "\":[\"WON\",\"LOST\",\"TIE\"]}", method); + private void doTestReadArrayEnumParameter(String name, EndpointMethod method, Object service) throws Exception { + Object[] params = readParameters("{\"" + name + "\":[\"WON\",\"LOST\",\"TIE\"]}", method, service); assertEquals(1, params.length); Outcome[] outcomes = (Outcome[]) params[0]; @@ -556,12 +571,12 @@ public void foo(@Named("str") String string, } String requestString = "{\"str\":\"hello\",\"" + TestEndpoint.NAME_STRING + "\":\"" + VALUE_STRING + "\",\"" + TestEndpoint.NAME_INTEGER + "\":" + VALUE_INTEGER - + ",\"integer_array\":[1,2,3]," + "\"integer_collection\":[4,5,6], \"stringValue\":" - + "\"321\", \"integerValue\":321}"; + + ",\"integer_array\":[1,2,3]," + "\"integer_collection\":[4,5,6],\"resource\":{\"stringValue\":" + + "\"321\", \"integerValue\":321}}"; Method method = TestMultipleResources.class.getDeclaredMethod("foo", String.class, Integer[].class, Collection.class, Request.class); - Object[] params = readParameters(requestString, method); + Object[] params = readParameters(requestString, method, new TestMultipleResources()); assertEquals(4, params.length); String string = (String) params[0]; @@ -594,7 +609,7 @@ public void foo( String requestString = "{\"str\":\"hello\"}"; Method method = Test.class.getDeclaredMethod("foo", String.class, Integer.class); - Object[] params = readParameters(requestString, method); + Object[] params = readParameters(requestString, method, new Test()); assertEquals(2, params.length); assertEquals("hello", params[0]); @@ -615,7 +630,7 @@ public void foo(@Named("foo1") String f1, @Nullable @Named("foo2") String f2, String requestString = "{\"name1\":\"v1\", \"foo2\":\"v2\", \"name3\":\"v3\"}"; - Object[] params = readParameters(requestString, endpointMethod); + Object[] params = readParameters(requestString, endpointMethod, new Test()); assertEquals(3, params.length); assertEquals("v1", params[0]); @@ -636,7 +651,7 @@ public void foo( Method method = Test.class.getDeclaredMethod("foo", String.class, String.class, String.class); EndpointMethod endpointMethod = EndpointMethod.create(method.getDeclaringClass(), method); - readParameters("{}", endpointMethod); + readParameters("{}", endpointMethod, new Test()); List parameterNames = endpointMethod.getParameterNames(); assertEquals(3, parameterNames.size()); @@ -660,8 +675,8 @@ public void user(TestUser user) {} final TestUser user = new TestUser("test"); Method method = TestUserEndpoint.class.getDeclaredMethod("user", TestUser.class); ParamReader reader = new ServletRequestParamReader( - EndpointMethod.create(method.getDeclaringClass(), method), endpointsContext, context, null, - null) { + new TestUserEndpoint(), EndpointMethod.create(method.getDeclaringClass(), method), endpointsContext, context, null, + null, ServletInitializationParameters.builder().build()) { @Override User getUser() { return user; @@ -686,7 +701,7 @@ public void userIp(@Named("userIp") String userIp) {} String ip = "9.8.7.6"; when(request.getRemoteAddr()).thenReturn(ip); Object[] params = readParameters("{}", - TestUserIp.class.getDeclaredMethod("userIp", String.class)); + TestUserIp.class.getDeclaredMethod("userIp", String.class), new TestUserIp()); assertEquals(1, params.length); assertEquals(ip, params[0]); } @@ -698,7 +713,7 @@ class TestAlt { public void alt(@Named("alt") String alt) {} } Object[] params = readParameters( - "{\"alt\":\"test\"}", TestAlt.class.getDeclaredMethod("alt", String.class)); + "{\"alt\":\"test\"}", TestAlt.class.getDeclaredMethod("alt", String.class), new TestAlt()); assertEquals(1, params.length); assertEquals("test", params[0]); } @@ -709,7 +724,7 @@ class TestAlt { @SuppressWarnings("unused") public void alt(@Named("alt") String alt) {} } - Object[] params = readParameters("{}", TestAlt.class.getDeclaredMethod("alt", String.class)); + Object[] params = readParameters("{}", TestAlt.class.getDeclaredMethod("alt", String.class), new TestAlt()); assertEquals(1, params.length); assertEquals("json", params[0]); } @@ -721,7 +736,7 @@ class TestFields { public void fields(@Named("fields") String fields) {} } Object[] params = readParameters( - "{\"fields\":\"test\"}", TestFields.class.getDeclaredMethod("fields", String.class)); + "{\"fields\":\"test\"}", TestFields.class.getDeclaredMethod("fields", String.class), new TestFields()); assertEquals(1, params.length); assertEquals("test", params[0]); } @@ -733,7 +748,7 @@ class TestKey { public void key(@Named("key") String key) {} } Object[] params = readParameters( - "{\"key\":\"test\"}", TestKey.class.getDeclaredMethod("key", String.class)); + "{\"key\":\"test\"}", TestKey.class.getDeclaredMethod("key", String.class), new TestKey()); assertEquals(1, params.length); assertEquals("test", params[0]); } @@ -746,7 +761,7 @@ public void oAuthToken(@Named("oauth_token") String token) {} } Object[] params = readParameters( "{\"oauth_token\":\"test\"}", - TestOAuthToken.class.getDeclaredMethod("oAuthToken", String.class)); + TestOAuthToken.class.getDeclaredMethod("oAuthToken", String.class), new TestOAuthToken()); assertEquals(1, params.length); assertEquals("test", params[0]); } @@ -759,7 +774,7 @@ public void quotaUser(@Named("quotaUser") String quotaUser) {} } Object[] params = readParameters( "{\"quotaUser\":\"test\"}", - TestQuotaUser.class.getDeclaredMethod("quotaUser", String.class)); + TestQuotaUser.class.getDeclaredMethod("quotaUser", String.class), new TestQuotaUser()); assertEquals(1, params.length); assertEquals("test", params[0]); } @@ -772,7 +787,7 @@ public void prettyPrint(@Named("prettyPrint") String prettyPrint) {} } when(request.getParameter("prettyPrint")).thenReturn("false"); Object[] params = - readParameters("{}", TestPrettyPrint.class.getDeclaredMethod("prettyPrint", String.class)); + readParameters("{}", TestPrettyPrint.class.getDeclaredMethod("prettyPrint", String.class), new TestPrettyPrint()); assertEquals(1, params.length); assertEquals(false, params[0]); } @@ -784,11 +799,46 @@ class TestPrettyPrint { public void prettyPrint(@Named("prettyPrint") String prettyPrint) {} } Object[] params = - readParameters("{}", TestPrettyPrint.class.getDeclaredMethod("prettyPrint", String.class)); + readParameters("{}", TestPrettyPrint.class.getDeclaredMethod("prettyPrint", String.class), new TestPrettyPrint()); assertEquals(1, params.length); assertEquals(true, params[0]); } + @Test + public void testNameInParamsAndResource() throws Exception { + class TestNameInParamsAndResource { + @SuppressWarnings("unused") + public void test(@Named("stringValue") List string, + @Nullable @Named("integerValue") List integer, Request resource) {} + } + Object[] params = readParameters( + "{\"stringValue\": [\"fromParams\"], \"integerValue\": [1,2,3], " + + "\"resource\": {\"stringValue\": \"abc\", \"integerValue\": 42}}", + TestNameInParamsAndResource.class + .getDeclaredMethod("test", List.class, List.class, Request.class), new TestNameInParamsAndResource()); + assertEquals(3, params.length); + assertEquals(Collections.singletonList("fromParams"), params[0]); + assertEquals(ImmutableList.of(1,2,3), params[1]); + assertEquals(new Request("abc", 42), params[2]); + } + + @Test + public void testTypeMismatch() throws Exception { + class TesTypeMismatch { + @SuppressWarnings("unused") + public void test(Request request) {} + } + try { + readParameters( + "{\"resource\": {\"integerValue\": [42]}}", + TesTypeMismatch.class + .getDeclaredMethod("test", Request.class), new TesTypeMismatch()); + fail("expected bad request exception"); + } catch (BadRequestException e) { + assertEquals("Parse error for field 'integerValue' of type 'int'", e.getMessage()); + } + } + @Test public void testUserInjectionThrowsExceptionIfRequired() throws Exception { @SuppressWarnings("unused") @@ -805,7 +855,8 @@ public void getUser(User user) { } "{}", EndpointMethod.create(method.getDeclaringClass(), method), methodConfig, null, - null); + null, + new TestUser()); fail("expected unauthorized method exception"); } catch (UnauthorizedException ex) { // expected @@ -830,7 +881,8 @@ public void getUser(com.google.appengine.api.users.User user) { } EndpointMethod.create(method.getDeclaringClass(), method), methodConfig, null, - null); + null, + new TestUser()); fail("expected unauthorized method exception"); } catch (UnauthorizedException ex) { // expected @@ -846,27 +898,139 @@ public void test(@Named("testParam") String testParam) {} try { Object[] params = readParameters("{}", - TestNullValueForRequiredParam.class.getDeclaredMethod("test", String.class)); + TestNullValueForRequiredParam.class.getDeclaredMethod("test", String.class), new TestNullValueForRequiredParam()); fail("expected bad request exception"); } catch (BadRequestException ex) { // expected } } - private Object[] readParameters(String input, Method method) throws Exception { - return readParameters(input, EndpointMethod.create(method.getDeclaringClass(), method)); + @Test + public void testPatternAnnotation_noMatch() throws Exception { + class TestPatternAnnotation { + @SuppressWarnings("unused") + public void test(@Named("testParam") @Pattern(regexp = "^\\d{2}$") String testParam) {} + } + try { + readParameters("{\"testParam\":\"123\"}", + TestPatternAnnotation.class.getDeclaredMethod("test", String.class), new TestPatternAnnotation()); + fail("expected bad request exception"); + } catch (BadRequestException ex) { + assertTrue("failed for unexpected reason: " + ex.getMessage(), ex.getMessage().contains("testParam must match")); + } + } + + @Test + public void testPatternAnnotation_match() throws Exception { + class TestPatternAnnotation { + @SuppressWarnings("unused") + public void test(@Named("testParam") @Pattern(regexp = "^\\d{2}$") String testParam) {} + } + Object[] params = readParameters("{\"testParam\":\"42\"}", + TestPatternAnnotation.class.getDeclaredMethod("test", String.class), new TestPatternAnnotation()); + assertEquals(1, params.length); + assertEquals("42", params[0]); + } + + @Test + public void testPatternAnnotation_customError() throws Exception { + class TestPatternAnnotation { + @SuppressWarnings("unused") + public void test(@Named("testParam") @Pattern(regexp = "^\\d{2}$", message="custom error message") String testParam) {} + } + try { + readParameters("{\"testParam\":\"invalidValue\"}", + TestPatternAnnotation.class.getDeclaredMethod("test", String.class), new TestPatternAnnotation()); + fail("expected bad request exception"); + } catch (BadRequestException ex) { + assertTrue("failed for unexpected reason: " + ex.getMessage(), ex.getMessage().contains("testParam custom error message")); + } + } + + static class TestPatternAnnotationInResourceRequest { + + @Min(value = 3) + private Integer integerValue; + + public TestPatternAnnotationInResourceRequest() { + } + + public TestPatternAnnotationInResourceRequest(Integer stringValue) { + this.integerValue = stringValue; + } + + public void setIntegerValue(Integer integerValue) { + this.integerValue = integerValue; + } + + public Integer getIntegerValue() { + return integerValue; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + TestPatternAnnotationInResourceRequest request = (TestPatternAnnotationInResourceRequest) o; + return Objects.equals(integerValue, request.integerValue); + } + + @Override + public int hashCode() { + return Objects.hash(integerValue); + } + } + + @Test + public void testValidAnnotationInResource() throws Exception { + + class TestPatternAnnotationInResource { + @SuppressWarnings("unused") + public void test(@Valid TestPatternAnnotationInResourceRequest resource) {} + } + try { + readParameters("{\"resource\":{\"integerValue\":2}}", + TestPatternAnnotationInResource.class.getDeclaredMethod("test", TestPatternAnnotationInResourceRequest.class), new TestPatternAnnotationInResource()); + + fail("expected bad request exception"); + } catch (BadRequestException ex) { + assertTrue("failed for unexpected reason: " + ex.getMessage(), ex.getMessage().contains("resource.integerValue must be greater than")); + } + } + + @Test + public void testSizeAnnotation_invalid() throws Exception { + class TestPatternAnnotation { + @SuppressWarnings("unused") + public void test(@Named("testParam") @Size(min = 3, max = 5) String testParam) {} + } + try { + readParameters("{\"testParam\":\"too long string\"}", + TestPatternAnnotation.class.getDeclaredMethod("test", String.class), new TestPatternAnnotation()); + fail("expected bad request exception"); + } catch (BadRequestException ex) { + assertTrue("failed for unexpected reason: " + ex.getMessage(), ex.getMessage().contains("testParam size must be between 3 and 5")); + } + } + + private Object[] readParameters(String input, Method method, Object service) throws Exception { + return readParameters(input, EndpointMethod.create(method.getDeclaringClass(), method), service); } - private Object[] readParameters(final String input, EndpointMethod method) throws Exception { - return readParameters(input, method, null, USER, APP_ENGINE_USER); + private Object[] readParameters(final String input, EndpointMethod method, Object service) throws Exception { + return readParameters(input, method, null, USER, APP_ENGINE_USER, service); } private Object[] readParameters(final String input, EndpointMethod method, ApiMethodConfig methodConfig, final User user, - final com.google.appengine.api.users.User appEngineUser) + final com.google.appengine.api.users.User appEngineUser, Object service) throws Exception { - ParamReader reader = new ServletRequestParamReader(method, endpointsContext, context, null, - methodConfig) { + ParamReader reader = new ServletRequestParamReader(service, method, endpointsContext, context, null, + methodConfig, ServletInitializationParameters.builder().build()) { @Override User getUser() { return user; @@ -900,9 +1064,9 @@ private void verifySimpleDateSerializationFails(String simpleDateString) Method method = TestEndpoint.class.getDeclaredMethod("getSimpleDate", SimpleDate.class); try { readParameters( - "{" + TestEndpoint.NAME_DATE_AND_TIME + ":\"" + simpleDateString + "\"}", method); - fail("Expected IllegalArgumentException"); - } catch (IllegalArgumentException expected) {} + "{" + TestEndpoint.NAME_DATE_AND_TIME + ":\"" + simpleDateString + "\"}", method, new TestEndpoint()); + fail("Expected BadRequestException"); + } catch (BadRequestException expected) {} } private Calendar getCalendarFromDate(Date date) { diff --git a/endpoints-framework/src/test/java/com/google/api/server/spi/response/CollectionResponseTest.java b/endpoints-framework/src/test/java/com/google/api/server/spi/response/CollectionResponseTest.java index 00fd3e01..d5c8cc19 100644 --- a/endpoints-framework/src/test/java/com/google/api/server/spi/response/CollectionResponseTest.java +++ b/endpoints-framework/src/test/java/com/google/api/server/spi/response/CollectionResponseTest.java @@ -17,6 +17,8 @@ import static com.google.common.truth.Truth.assertThat; +import com.google.api.server.spi.config.model.ApiSerializationConfig; + import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; @@ -29,6 +31,8 @@ import java.util.ArrayList; import java.util.List; +import javax.servlet.http.HttpServletResponse; + /** * Tests of {@link CollectionResponse}. */ @@ -45,8 +49,9 @@ public int getDummy() { @Test public void testCollectionResponse() throws IOException { MockHttpServletResponse servletResponse = new MockHttpServletResponse(); - ServletResponseResultWriter writer = new ServletResponseResultWriter(servletResponse, null); - writer.write(getBeans(2)); + ServletResponseResultWriter writer = new ServletResponseResultWriter( + servletResponse, (ApiSerializationConfig) null, false, false); + writer.write(getBeans(2), HttpServletResponse.SC_OK); ObjectNode json = new ObjectMapper().readValue( servletResponse.getContentAsString(), ObjectNode.class); diff --git a/endpoints-framework/src/test/java/com/google/api/server/spi/response/RedirectExceptionTest.java b/endpoints-framework/src/test/java/com/google/api/server/spi/response/RedirectExceptionTest.java new file mode 100644 index 00000000..ba6496af --- /dev/null +++ b/endpoints-framework/src/test/java/com/google/api/server/spi/response/RedirectExceptionTest.java @@ -0,0 +1,34 @@ +package com.google.api.server.spi.response; + +import org.junit.Assert; +import org.junit.Test; + +public class RedirectExceptionTest { + + @Test + public void success() { + RedirectException e = new RedirectException(302, "message", "location"); + Assert.assertEquals(302, e.getStatusCode()); + Assert.assertEquals("message", e.getMessage()); + Assert.assertEquals("location", e.getLocation()); + } + + @Test + public void statusLimits_fails() { + Assert.assertThrows(IllegalArgumentException.class, () -> new RedirectException(299, "message", "location")); + Assert.assertThrows(IllegalArgumentException.class, () -> new RedirectException(400, "message", "location")); + } + + @Test + public void statusLimits_success() { + RedirectException e = new RedirectException(300, "message", "location"); + Assert.assertEquals(300, e.getStatusCode()); + e = new RedirectException(399, "message", "location"); + Assert.assertEquals(399, e.getStatusCode()); + } + + @Test + public void locationNull_fails() { + Assert.assertThrows(NullPointerException.class, () -> new RedirectException(302, "message", null)); + } +} diff --git a/endpoints-framework/src/test/java/com/google/api/server/spi/response/RestResponseResultWriterTest.java b/endpoints-framework/src/test/java/com/google/api/server/spi/response/RestResponseResultWriterTest.java index f6cf2b1a..46092a03 100644 --- a/endpoints-framework/src/test/java/com/google/api/server/spi/response/RestResponseResultWriterTest.java +++ b/endpoints-framework/src/test/java/com/google/api/server/spi/response/RestResponseResultWriterTest.java @@ -16,6 +16,8 @@ package com.google.api.server.spi.response; import static com.google.common.truth.Truth.assertThat; +import static java.lang.Boolean.FALSE; +import static java.lang.Boolean.TRUE; import com.google.api.server.spi.ObjectMapperUtil; import com.google.api.server.spi.ServiceException; @@ -27,8 +29,14 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; +import org.skyscreamer.jsonassert.JSONAssert; import org.springframework.mock.web.MockHttpServletResponse; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + /** * Tests for {@link RestResponseResultWriter}. */ @@ -219,4 +227,126 @@ private void writeError(boolean enableExceptionCompatibility, String customReaso assertThat(innerError.path("domain").asText()).isEqualTo(expectedDomain); assertThat(innerError.path("reason").asText()).isEqualTo(expectedReason); } + + @Test + public void writeError_extraFields() throws Exception { + MockHttpServletResponse response = new MockHttpServletResponse(); + RestResponseResultWriter writer = new RestResponseResultWriter(response, null, true /* prettyPrint */, + true /* addContentLength */, true /* enableExceptionCompatibility */); + + ServiceException serviceException = new ServiceException(400, "customMessage", "customReason", "customDomain"); + // Extra field string + serviceException.putExtraField("someExtraString", "string1") + .putExtraField("someNullString", (String)null); + // Extra field number + serviceException.putExtraField("someExtraInt", Integer.valueOf(12)) + .putExtraField("someExtraFloat", Float.valueOf(1.2f)) + .putExtraField("someNullNumber", (Number)null); + // Extra field boolean + serviceException.putExtraField("someExtraTrue", TRUE) + .putExtraField("someExtraFalse", FALSE) + .putExtraField("someNullBoolean", (Boolean)null); + // Extra field, keys are equals to reserved keywords when ignoring case + serviceException.putExtraField("Domain", TRUE) + .putExtraField("REASON", Long.valueOf(1234567890)) + .putExtraField("messAge", "hello world!"); + + String expectedError = "{\"error\": {\"errors\": [{" + + " \"domain\": \"customDomain\"," + + " \"reason\": \"customReason\"," + + " \"message\": \"customMessage\"," + + " \"someExtraString\": \"string1\"," + + " \"someNullString\": null," + + " \"someExtraInt\": 12," + + " \"someExtraFloat\": 1.2," + + " \"someNullNumber\": null," + + " \"someExtraTrue\": true," + + " \"someExtraFalse\": false," + + " \"someNullBoolean\": null," + + " \"Domain\": true," + + " \"REASON\": \"1234567890\"," + + " \"messAge\": \"hello world!\"" + + " }]," + + " \"code\": 400," + + " \"message\": \"customMessage\"" + + "}}"; + + writer.writeError(serviceException); + JSONAssert.assertEquals(expectedError, response.getContentAsString(), true); + } + + @Test + public void writeError_extraFieldsUnsafe() throws Exception { + + MockHttpServletResponse response = new MockHttpServletResponse(); + RestResponseResultWriter writer = new RestResponseResultWriter(response, null, true /* prettyPrint */, + true /* addContentLength */, true /* enableExceptionCompatibility */); + + TestServiceExceptionExtraFieldUnsafe serviceException = new TestServiceExceptionExtraFieldUnsafe(400, "customMessage", "customReason", "customDomain"); + + // Extra field array + Boolean[] booleans = new Boolean[] { TRUE, FALSE, TRUE }; + + // Extra field List + List stringList = Arrays.asList("First", "Second", "Last"); + + // Extra field Map + Map map = new HashMap<>(); + map.put(1, new TestValue("Alice", 7, TestEnum.VALUE1)); + map.put(2, new TestValue("Bob", 12, TestEnum.VALUE2)); + map.put(3, new TestValue("Clark", 31, TestEnum.VALUE3)); + + serviceException.putExtraFieldUnsafe("someExtraNull", null) + .putExtraFieldUnsafe("someExtraArray", booleans) + .putExtraFieldUnsafe("someExtraList", stringList) + .putExtraFieldUnsafe("someExtraMap", map); + + String expectedError = "{\"error\": {\"errors\": [{" + + " \"domain\": \"customDomain\"," + + " \"reason\": \"customReason\"," + + " \"message\": \"customMessage\"," + + " \"someExtraNull\": null," + + " \"someExtraArray\": [true, false, true]," + + " \"someExtraList\": [\"First\", \"Second\", \"Last\"]," + + " \"someExtraMap\": {" + + " \"1\": {\"name\": \"Alice\", \"age\": 7, \"testEnum\": \"VALUE1\"}," + + " \"2\": {\"name\": \"Bob\", \"age\": 12, \"testEnum\": \"VALUE2\"}," + + " \"3\": {\"name\": \"Clark\", \"age\": 31, \"testEnum\": \"VALUE3\"}" + + " }" + + " }]," + + " \"code\": 400," + + " \"message\": \"customMessage\"" + + "}}"; + + writer.writeError(serviceException); + JSONAssert.assertEquals(expectedError, response.getContentAsString(), true); + } + + enum TestEnum { + VALUE1, VALUE2, VALUE3; + } + + class TestValue { + public String name; + public int age; + public TestEnum testEnum; + + TestValue(String name, int age, TestEnum testEnum) { + this.name = name; + this.age = age; + this.testEnum = testEnum; + } + } + + class TestServiceExceptionExtraFieldUnsafe extends ServiceException { + + TestServiceExceptionExtraFieldUnsafe(int statusCode, String statusMessage, String reason, String domain) { + super(statusCode, statusMessage, reason, domain); + } + + @Override + public TestServiceExceptionExtraFieldUnsafe putExtraFieldUnsafe(String fieldName, Object value) { + return (TestServiceExceptionExtraFieldUnsafe) super.putExtraFieldUnsafe(fieldName, value); + } + } } diff --git a/endpoints-framework/src/test/java/com/google/api/server/spi/response/ServletResponseResultWriterTest.java b/endpoints-framework/src/test/java/com/google/api/server/spi/response/ServletResponseResultWriterTest.java index 7762fdde..589af7d1 100644 --- a/endpoints-framework/src/test/java/com/google/api/server/spi/response/ServletResponseResultWriterTest.java +++ b/endpoints-framework/src/test/java/com/google/api/server/spi/response/ServletResponseResultWriterTest.java @@ -17,17 +17,30 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import com.google.api.server.spi.ConfiguredObjectMapper; import com.google.api.server.spi.ObjectMapperUtil; +import com.google.api.server.spi.ServiceException; +import com.google.api.server.spi.config.model.ApiSerializationConfig; import com.google.api.server.spi.types.DateAndTime; import com.google.api.server.spi.types.SimpleDate; import com.google.appengine.api.datastore.Blob; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.core.json.JsonWriteFeature; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectWriter; +import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import javax.servlet.http.HttpServletResponse; import org.junit.Test; import org.junit.runner.RunWith; @@ -41,8 +54,8 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; - -import javax.servlet.http.HttpServletResponse; +import java.util.Optional; +import java.util.OptionalLong; /** * Tests for {@link ServletResponseResultWriter}. @@ -60,12 +73,15 @@ public void testTypeChangesInMapAsString() throws Exception { Map value = new HashMap<>(); value.put("nonPrimitive", 100L); value.put("primitive", 200L); + value.put("optionalLong", OptionalLong.of(150L)); + value.put("optionalOfLong", Optional.of(250L)); value.put("date", new Date(DATE_VALUE)); value.put("dateAndTime", DateAndTime.parseRfc3339String(DATE_AND_TIME_VALUE_STRING)); value.put("simpleDate", new SimpleDate(2002, 10, 2)); testTypeChangesAsString(value); } + @Test @SuppressWarnings("unused") public void testTypeChangesInBeanAsString() throws Exception { Object value = new Object() { @@ -75,6 +91,12 @@ public long getPrimitive() { public Long getNonPrimitive() { return 100L; } + public OptionalLong getOptionalLong() { + return OptionalLong.of(150L); + } + public Optional getOptionalOfLong() { + return Optional.of(250L); + } public Long getLongNull() { return null; } @@ -114,21 +136,571 @@ public List getEmptyLongList() { ObjectNode output = testTypeChangesAsString(value); assertTrue(output.path("longNull").isMissingNode()); assertTrue(output.path("stringNull").isMissingNode()); - assertEquals("", output.path("stringEmpty").asText()); + assertEquals("", output.path("stringEmpty").asText(null)); + assertTrue(output.path("dateNull").isMissingNode()); + assertTrue(output.path("dateAndTimeNull").isMissingNode()); + assertTrue(output.path("simpleDateNull").isMissingNode()); + assertTrue(output.path("emptyLongArray").isMissingNode()); + assertTrue(output.path("emptyLongList").isMissingNode()); + } + + @Test + @SuppressWarnings("unused") + public void testPropertyInclusion_noModifier() throws Exception { + Object value = new Object() { + public String getStringEmpty() { + return ""; + } + public String getStringNotEmpty() { + return "not empty"; + } + public Optional getOptionalStringNull() { + return null; + } + public Optional getOptionalStringEmpty() { + return Optional.empty(); + } + public Optional getOptionalStringEmptyContent() { + return Optional.of(""); + } + public Optional getOptionalStringNotEmpty() { + return Optional.of("not empty"); + } + public OptionalLong getOptionalLongNull() { + return null; + } + public OptionalLong getOptionalLongEmpty() { + return OptionalLong.empty(); + } + public OptionalLong getOptionalLongNotEmpty() { + return OptionalLong.of(123L); + } + public Long getLongNull() { + return null; + } + public String getStringNull() { + return null; + } + public Date getDate() { + return new Date(DATE_VALUE); + } + public Date getDateNull() { + return null; + } + public DateAndTime getDateAndTime() { + return DateAndTime.parseRfc3339String(DATE_AND_TIME_VALUE_STRING); + } + public DateAndTime getDateAndTimeNull() { + return null; + } + + public SimpleDate getSimpleDate() { + return new SimpleDate(2002, 10, 2); + } + + // Null or empty objects, no annotation + public SimpleDate getSimpleDateNull() { + return null; + } + public Long[] getNullLongArray() { + return null; + } + public List getNullLongList() { + return null; + } + public Long[] getEmptyLongArray() { + return new Long[0]; + } + public List getEmptyLongList() { + return new ArrayList<>(); + } + public Map getEmptyMap() { + return new HashMap<>(); + } + public List>> getDeeplyEmptyLongList() { + List> list = ImmutableList.of(new HashMap<>()); + return ImmutableList.of(list); + } + public Map> getDeeplyEmptyMapList() { + return ImmutableMap.of(12L, new ArrayList<>()); + } + public Map getDeeplyEmptyMapArray() { + return ImmutableMap.of(12L, new Long[0]); + } + + //string handling in collections / maps + public List getStringListWithEmptyValues() { + return Lists.newArrayList(null, ""); + } + public String[] getStringArrayWithEmptyValues() { + return new String[] {null, ""}; + } + public Map getStringMapWithEmptyValues() { + return new HashMap() {{ + put("null_value", null); + put("empty_value", ""); + }}; + } + public Map> getMapWithEmptyListValues() { + return new HashMap>() {{ + put("null_value", null); + put("empty_value", new ArrayList<>()); + put("empty_string_value", Lists.newArrayList(null, "")); + }}; + } + }; + + ObjectNode legacyOutput = toJSON(value, true); + ObjectNode output = toJSON(value); + + //the new Mapper config must produce same result as the old one using WRITE_EMPTY_JSON_ARRAYS + //see https://github.com/FasterXML/jackson-databind/issues/1547 for more on the issue + assertEquals(legacyOutput, output); + + //String is handled specifically + assertEquals("", output.path("stringEmpty").asText(null)); + assertPathPresent("\"not empty\"", output.path("stringNotEmpty")); + + //optionals + assertTrue(output.path("optionalStringNull").isMissingNode()); + assertPathPresent("null", output.path("optionalStringEmpty")); + assertPathPresent("\"\"", output.path("optionalStringEmptyContent")); + assertPathPresent("\"not empty\"", output.path("optionalStringNotEmpty")); + + //simple objects + assertTrue(output.path("longNull").isMissingNode()); + assertTrue(output.path("stringNull").isMissingNode()); assertTrue(output.path("dateNull").isMissingNode()); assertTrue(output.path("dateAndTimeNull").isMissingNode()); assertTrue(output.path("simpleDateNull").isMissingNode()); + + //collections + assertTrue(output.path("nullLongArray").isMissingNode()); + assertTrue(output.path("nullLongList").isMissingNode()); + assertTrue(output.path("emptyLongArray").isMissingNode()); + assertTrue(output.path("emptyLongList").isMissingNode()); + assertTrue(output.path("emptyMap").isMissingNode()); + assertTrue(output.path("deeplyEmptyLongList").isMissingNode()); + assertTrue(output.path("deeplyEmptyMapList").isMissingNode()); + assertTrue(output.path("deeplyEmptyMapArray").isMissingNode()); + + //strings in collections + assertPathPresent("[null,\"\"]", output.path("stringListWithEmptyValues")); + assertPathPresent("[null,\"\"]", output.path("stringArrayWithEmptyValues")); + assertPathPresent("{\"empty_value\":\"\"}", output.path("stringMapWithEmptyValues")); + assertPathPresent("{\"empty_string_value\":[null,\"\"],\"empty_value\":[]}", + output.path("mapWithEmptyListValues")); + } + + @Test + @SuppressWarnings("unused") + public void testPropertyInclusion_includeAlways() throws Exception { + Object value = new Object() { + // Null or empty objects, annotation value (ALWAYS) + @JsonInclude + public String getStringEmpty() { + return ""; + } + @JsonInclude + public String getStringNotEmpty() { + return "not empty"; + } + @JsonInclude + public Optional getOptionalStringNull() { + return null; + } + @JsonInclude + public Optional getOptionalStringEmpty() { + return Optional.empty(); + } + @JsonInclude + public Optional getOptionalStringNotEmpty() { + return Optional.of("not empty"); + } + @JsonInclude + public SimpleDate getSimpleDateNull() { + return null; + } + @JsonInclude + public Long[] getNullLongArray() { + return null; + } + @JsonInclude + public List getNullLongList() { + return null; + } + @JsonInclude + public Long[] getEmptyLongArray() { + return new Long[0]; + } + @JsonInclude + public List getEmptyLongList() { + return new ArrayList<>(); + } + @JsonInclude + public Map getEmptyMap() { + return new HashMap<>(); + } + @JsonInclude + public List>> getDeeplyEmptyLongList() { + List> list = ImmutableList.of(new HashMap<>()); + return ImmutableList.of(list); + } + @JsonInclude + public Map> getDeeplyEmptyMapList() { + return ImmutableMap.of(12L, new ArrayList<>()); + } + @JsonInclude + public Map getDeeplyEmptyMapArray() { + return ImmutableMap.of(12L, new Long[0]); + } + //string handling in collections / maps + @JsonInclude + public List getStringListWithEmptyValues() { + return Lists.newArrayList(null, ""); + } + @JsonInclude + public String[] getStringArrayWithEmptyValues() { + return new String[] {null, ""}; + } + @JsonInclude + public Map getStringMapWithEmptyValues() { + return new HashMap() {{ + put("null_value", null); + put("empty_value", ""); + }}; + } + @JsonInclude + public Map> getMapWithEmptyListValues() { + return new HashMap>() {{ + put("null_value", null); + put("empty_value", new ArrayList<>()); + put("empty_string_value", Lists.newArrayList(null, "")); + }}; + } + }; + + ObjectNode output = toJSON(value); + + //String is handled specifically + assertEquals("", output.path("stringEmpty").asText(null)); + assertPathPresent("\"not empty\"", output.path("stringNotEmpty")); + + //simple objects + assertPathPresent("null", output.path("simpleDateNull")); + assertPathPresent("null", output.path("nullLongArray")); + assertPathPresent("null", output.path("nullLongList")); + + //optionals + assertPathPresent("null", output.path("optionalStringNull")); + assertPathPresent("null", output.path("optionalStringEmpty")); + assertPathPresent("\"not empty\"", output.path("optionalStringNotEmpty")); + + //collections + assertPathPresent("[]", output.path("emptyLongArray")); + assertPathPresent("[]", output.path("emptyLongList")); + assertPathPresent("{}", output.path("emptyMap")); + assertPathPresent("[[{}]]", output.path("deeplyEmptyLongList")); + assertPathPresent("{\"12\":[]}", output.path("deeplyEmptyMapList")); + assertPathPresent("{\"12\":[]}", output.path("deeplyEmptyMapArray")); + + //strings in collections + assertPathPresent("[null,\"\"]", output.path("stringListWithEmptyValues")); + assertPathPresent("[null,\"\"]", output.path("stringArrayWithEmptyValues")); + assertPathPresent("{\"null_value\":null,\"empty_value\":\"\"}", + output.path("stringMapWithEmptyValues")); + assertPathPresent( + "{\"null_value\":null,\"empty_string_value\":[null,\"\"],\"empty_value\":[]}", + output.path("mapWithEmptyListValues")); + } + + @Test + @SuppressWarnings("unused") + public void testPropertyInclusion_includeNonNull() throws Exception { + Object value = new Object() { + // Null or empty objects, annotation NON_NULL + @JsonInclude(value = JsonInclude.Include.NON_NULL) + public String getStringEmpty() { + return ""; + } + @JsonInclude(value = JsonInclude.Include.NON_NULL) + public String getStringNotEmpty() { + return "not empty"; + } + @JsonInclude(value = JsonInclude.Include.NON_NULL) + public SimpleDate getSimpleDateNull() { + return null; + } + @JsonInclude(value = JsonInclude.Include.NON_NULL) + public Optional getOptionalStringNull() { + return null; + } + @JsonInclude(value = JsonInclude.Include.NON_NULL) + public Optional getOptionalStringEmpty() { + return Optional.empty(); + } + @JsonInclude(value = JsonInclude.Include.NON_NULL) + public Optional getOptionalStringNotEmpty() { + return Optional.of("not empty"); + } + @JsonInclude(value = JsonInclude.Include.NON_NULL) + public Long[] getNullLongArray() { + return null; + } + @JsonInclude(value = JsonInclude.Include.NON_NULL) + public List getNullLongList() { + return null; + } + @JsonInclude(value = JsonInclude.Include.NON_NULL) + public Long[] getEmptyLongArray() { + return new Long[0]; + } + @JsonInclude(value = JsonInclude.Include.NON_NULL) + public List getEmptyLongList() { + return new ArrayList<>(); + } + @JsonInclude(value = JsonInclude.Include.NON_NULL) + public Map getEmptyMap() { + return new HashMap<>(); + } + @JsonInclude(value = JsonInclude.Include.NON_NULL) + public List>> getDeeplyEmptyLongList() { + List> list = ImmutableList.of(new HashMap<>()); + return ImmutableList.of(list); + } + @JsonInclude(value = JsonInclude.Include.NON_NULL) + public Map> getDeeplyEmptyMapList() { + return ImmutableMap.of(12L, new ArrayList<>()); + } + @JsonInclude(value = JsonInclude.Include.NON_NULL) + public Map getDeeplyEmptyMapArray() { + return ImmutableMap.of(12L, new Long[0]); + } + //string handling in collections / maps + @JsonInclude(value = JsonInclude.Include.NON_NULL, content = Include.NON_NULL) + public List getStringListWithEmptyValues() { + return Lists.newArrayList(null, ""); + } + @JsonInclude(value = JsonInclude.Include.NON_NULL, content = Include.NON_NULL) + public String[] getStringArrayWithEmptyValues() { + return new String[] {null, ""}; + } + @JsonInclude(value = JsonInclude.Include.NON_NULL, content = Include.NON_NULL) + public Map getStringMapWithEmptyValues() { + return new HashMap() {{ + put("null_value", null); + put("empty_value", ""); + }}; + } + @JsonInclude(value = JsonInclude.Include.NON_NULL, content = Include.NON_NULL) + public Map> getMapWithEmptyListValues() { + return new HashMap>() {{ + put("null_value", null); + put("empty_value", new ArrayList<>()); + put("empty_string_value", Lists.newArrayList(null, "")); + }}; + } + }; + ObjectNode output = toJSON(value); + + //String is handled specifically + assertEquals("", output.path("stringEmpty").asText(null)); + assertPathPresent("\"not empty\"", output.path("stringNotEmpty")); + + //simple objects + assertTrue(output.path("simpleDateNull").isMissingNode()); + assertTrue(output.path("nullLongArray").isMissingNode()); + assertTrue(output.path("nullLongList").isMissingNode()); + + //optionals + assertTrue(output.path("optionalStringNull").isMissingNode()); + assertPathPresent("null", output.path("optionalStringEmpty")); + assertPathPresent("\"not empty\"", output.path("optionalStringNotEmpty")); + + //collections + assertPathPresent("[]", output.path("emptyLongArray")); + assertPathPresent("[]", output.path("emptyLongList")); + assertPathPresent("{}", output.path("emptyMap")); + assertPathPresent("[[{}]]", output.path("deeplyEmptyLongList")); + assertPathPresent("{\"12\":[]}", output.path("deeplyEmptyMapList")); + assertPathPresent("{\"12\":[]}", output.path("deeplyEmptyMapArray")); + + //strings in collections + assertPathPresent("[null,\"\"]", output.path("stringListWithEmptyValues")); + assertPathPresent("[null,\"\"]", output.path("stringArrayWithEmptyValues")); + assertPathPresent("{\"empty_value\":\"\"}", output.path("stringMapWithEmptyValues")); + assertPathPresent("{\"empty_string_value\":[null,\"\"],\"empty_value\":[]}", + output.path("mapWithEmptyListValues")); + } + + @Test + @SuppressWarnings("unused") + public void testPropertyInclusion_includeNonEmpty() throws Exception { + Object value = new Object() { + // Null or empty objects, annotation NON_EMPTY + @JsonInclude(value = JsonInclude.Include.NON_EMPTY) + public String getStringEmpty() { + return ""; + } + @JsonInclude(value = JsonInclude.Include.NON_EMPTY) + public String getStringNotEmpty() { + return "not empty"; + } + @JsonInclude(value = JsonInclude.Include.NON_EMPTY) + public Optional getOptionalStringNull() { + return null; + } + @JsonInclude(value = JsonInclude.Include.NON_EMPTY) + public Optional getOptionalStringEmpty() { + return Optional.empty(); + } + @JsonInclude(value = JsonInclude.Include.NON_EMPTY) + public Optional getOptionalStringEmptyContent() { + return Optional.of(""); + } + @JsonInclude(value = JsonInclude.Include.NON_EMPTY) + public Optional getOptionalStringNotEmpty() { + return Optional.of("not empty"); + } + @JsonInclude(value = JsonInclude.Include.NON_EMPTY) + public SimpleDate getSimpleDateNull() { + return null; + } + @JsonInclude(value = JsonInclude.Include.NON_EMPTY) + public Long[] getNullLongArray() { + return null; + } + @JsonInclude(value = JsonInclude.Include.NON_EMPTY) + public List getNullLongList() { + return null; + } + @JsonInclude(value = JsonInclude.Include.NON_EMPTY) + public Long[] getEmptyLongArray() { + return new Long[0]; + } + @JsonInclude(value = JsonInclude.Include.NON_EMPTY) + public List getEmptyLongList() { + return new ArrayList<>(); + } + @JsonInclude(value = JsonInclude.Include.NON_EMPTY) + public Map getEmptyMap() { + return new HashMap<>(); + } + @JsonInclude(value = JsonInclude.Include.NON_EMPTY) + public List>> getDeeplyEmptyLongList() { + List> list = ImmutableList.of(new HashMap<>()); + return ImmutableList.of(list); + } + @JsonInclude(value = JsonInclude.Include.NON_EMPTY) + public Map> getDeeplyEmptyMapList() { + return ImmutableMap.of(12L, new ArrayList<>()); + } + @JsonInclude(value = JsonInclude.Include.NON_EMPTY) + public Map getDeeplyEmptyMapArray() { + return ImmutableMap.of(12L, new Long[0]); + } + + // Non empty objects, annotation NON_EMPTY + @JsonInclude(value = JsonInclude.Include.NON_EMPTY) + public Long[] getNonEmptyLongArray() { + return new Long[] {12L}; + } + @JsonInclude(value = JsonInclude.Include.NON_EMPTY) + public List getNonEmptyLongList() { + return ImmutableList.of(12L); + } + @JsonInclude(value = JsonInclude.Include.NON_EMPTY) + public Map getNonEmptyMap() { + return ImmutableMap.of(12L, ""); + } + @JsonInclude(value = JsonInclude.Include.NON_EMPTY) + public List>> getDeeplyNonEmptyLongList() { + List> list = ImmutableList.of(ImmutableMap.of(12L, "")); + return ImmutableList.of(list); + } + @JsonInclude(value = JsonInclude.Include.NON_EMPTY) + public Map> getDeeplyNonEmptyMapList() { + return ImmutableMap.of(12L, ImmutableList.of(23L)); + } + @JsonInclude(value = JsonInclude.Include.NON_EMPTY) + public Map getDeeplyNonEmptyMapArray() { + return ImmutableMap.of(12L, new Long[] {23L}); + } + //string handling in collections / maps + @JsonInclude(value = JsonInclude.Include.NON_EMPTY, content = Include.NON_EMPTY) + public List getStringListWithEmptyValues() { + return Lists.newArrayList(null, ""); + } + @JsonInclude(value = JsonInclude.Include.NON_EMPTY, content = Include.NON_EMPTY) + public String[] getStringArrayWithEmptyValues() { + return new String[] {null, ""}; + } + @JsonInclude(value = JsonInclude.Include.NON_EMPTY, content = Include.NON_EMPTY) + public Map getStringMapWithEmptyValues() { + return new HashMap() {{ + put("null_value", null); + put("empty_value", ""); + }}; + } + @JsonInclude(value = JsonInclude.Include.NON_EMPTY, content = Include.NON_EMPTY) + public Map> getMapWithEmptyListValues() { + return new HashMap>() {{ + put("null_value", null); + put("empty_value", new ArrayList<>()); + put("empty_string_value", Lists.newArrayList(null, "")); + }}; + } + }; + ObjectNode output = toJSON(value); + + //String is handled specifically + assertTrue(output.path("stringEmpty").isMissingNode()); + assertPathPresent("\"not empty\"", output.path("stringNotEmpty")); + + //optionals + assertTrue(output.path("optionalStringNull").isMissingNode()); + assertTrue(output.path("optionalStringEmpty").isMissingNode()); + //this is different than empty plain string behavior + assertPathPresent("\"\"", output.path("optionalStringEmptyContent")); + assertPathPresent("\"not empty\"", output.path("optionalStringNotEmpty")); + + + //simple objects + assertTrue(output.path("simpleDateNull").isMissingNode()); + + //collections + assertTrue(output.path("nullLongArray").isMissingNode()); + assertTrue(output.path("nullLongList").isMissingNode()); assertTrue(output.path("emptyLongArray").isMissingNode()); assertTrue(output.path("emptyLongList").isMissingNode()); + assertTrue(output.path("emptyMap").isMissingNode()); + assertTrue(output.path("deeplyEmptyLongList").isMissingNode()); + assertTrue(output.path("deeplyEmptyMapList").isMissingNode()); + assertTrue(output.path("deeplyEmptyMapArray").isMissingNode()); + assertPathPresent("[\"12\"]", output.path("nonEmptyLongArray")); + assertPathPresent("[\"12\"]", output.path("nonEmptyLongList")); + assertPathPresent("{\"12\":\"\"}", output.path("nonEmptyMap")); + assertPathPresent("[[{\"12\":\"\"}]]", output.path("deeplyNonEmptyLongList")); + assertPathPresent("{\"12\":[\"23\"]}", output.path("deeplyNonEmptyMapList")); + assertPathPresent("{\"12\":[\"23\"]}", output.path("deeplyNonEmptyMapArray")); + + //strings in collections + assertPathPresent("[null,\"\"]", output.path("stringListWithEmptyValues")); + assertPathPresent("[null,\"\"]", output.path("stringArrayWithEmptyValues")); + assertPathPresent("{}", output.path("stringMapWithEmptyValues")); + assertPathPresent("{\"empty_string_value\":[null,\"\"]}", + output.path("mapWithEmptyListValues")); + } + + private void assertPathPresent(String expectedString, JsonNode path) { + assertFalse(path.isMissingNode()); + assertEquals(expectedString, path.toString()); } @Test public void testTypeChangesInArrayAsString() throws Exception { Object[] array = new Object[]{100L, 200L}; - String responseBody = writeToResponse(array); - - ObjectNode output = ObjectMapperUtil.createStandardObjectMapper() - .readValue(responseBody, ObjectNode.class); + ObjectNode output = toJSON(array); ArrayNode items = (ArrayNode) output.get("items"); assertTrue(items.get(0).isTextual()); assertEquals("100", items.get(0).asText()); @@ -139,12 +711,21 @@ public void testTypeChangesInArrayAsString() throws Exception { @Test public void testWriteNull() throws Exception { MockHttpServletResponse response = new MockHttpServletResponse(); - ServletResponseResultWriter writer = new ServletResponseResultWriter(response, null); - writer.write(null); + ServletResponseResultWriter writer = getDefaultWriter(response); + writer.write(null, HttpServletResponse.SC_NO_CONTENT); assertEquals("", response.getContentAsString()); assertEquals(HttpServletResponse.SC_NO_CONTENT, response.getStatus()); } + @Test + public void testWriteCustomStatus() throws Exception { + MockHttpServletResponse response = new MockHttpServletResponse(); + ServletResponseResultWriter writer = getDefaultWriter(response); + writer.write("response", HttpServletResponse.SC_CREATED); + assertEquals("\"response\"", response.getContentAsString()); + assertEquals(HttpServletResponse.SC_CREATED, response.getStatus()); + } + @SuppressWarnings("unused") public void testByteArrayAsBase64() throws Exception { Object value = new Object() { @@ -153,14 +734,14 @@ public byte[] getValues() { } }; ObjectNode output = ObjectMapperUtil.createStandardObjectMapper() - .readValue(writeToResponse(value), ObjectNode.class); + .readValue(writeToResponse(value, false), ObjectNode.class); assertEquals("AQIDBA==", output.path("values").asText()); } @Test public void testWriteErrorResponseHeaders() throws Exception { MockHttpServletResponse response = new MockHttpServletResponse(); - ServletResponseResultWriter writer = new ServletResponseResultWriter(response, null); + ServletResponseResultWriter writer = getDefaultWriter(response); Map headers = new LinkedHashMap<>(); headers.put("name0", "value0"); headers.put("name1", "value1"); @@ -171,9 +752,9 @@ public void testWriteErrorResponseHeaders() throws Exception { @Test public void testPrettyPrint() throws Exception { MockHttpServletResponse response = new MockHttpServletResponse(); - ServletResponseResultWriter writer = new ServletResponseResultWriter(response, null, - true /* prettyPrint */, true /* addContentLength */); - writer.write(ImmutableMap.of("one", "two", "three", "four")); + ServletResponseResultWriter writer = new ServletResponseResultWriter(response, + (ApiSerializationConfig) null, true /* prettyPrint */, true /* addContentLength */); + writer.write(ImmutableMap.of("one", "two", "three", "four"), HttpServletResponse.SC_OK); // If the response is pretty printed, there should be at least two newlines. String body = response.getContentAsString(); int index = body.indexOf('\n'); @@ -194,7 +775,7 @@ public Blob getBlob() { } }; ObjectNode output = ObjectMapperUtil.createStandardObjectMapper() - .readValue(writeToResponse(value), ObjectNode.class); + .readValue(writeToResponse(value, false), ObjectNode.class); assertEquals("AQIDBA==", output.path("blob").asText()); } @@ -206,18 +787,20 @@ public enum TestEnum { public void testEnumAsString() throws Exception { TestEnum value = TestEnum.TEST1; JsonNode output = ObjectMapperUtil.createStandardObjectMapper() - .readValue(writeToResponse(value), JsonNode.class); + .readValue(writeToResponse(value, false), JsonNode.class); assertEquals("TEST1", output.asText()); } private ObjectNode testTypeChangesAsString(Object value) throws Exception { - String responseBody = writeToResponse(value); - ObjectNode output = ObjectMapperUtil.createStandardObjectMapper() - .readValue(responseBody, ObjectNode.class); + ObjectNode output = toJSON(value); assertTrue(output.get("nonPrimitive").isTextual()); assertEquals("100", output.get("nonPrimitive").asText()); assertTrue(output.get("primitive").isTextual()); assertEquals("200", output.get("primitive").asText()); + assertTrue(output.get("optionalLong").isTextual()); + assertEquals("150", output.get("optionalLong").asText()); + assertTrue(output.get("optionalOfLong").isTextual()); + assertEquals("250", output.get("optionalOfLong").asText()); assertEquals( new com.google.api.client.util.DateTime(DATE_VALUE_STRING), new com.google.api.client.util.DateTime(output.get("date").asText())); @@ -229,10 +812,68 @@ private ObjectNode testTypeChangesAsString(Object value) throws Exception { return output; } - private String writeToResponse(Object value) throws IOException { + private ObjectNode toJSON(Object value) throws IOException { + return toJSON(value, false); + } + + private ObjectNode toJSON(Object value, boolean legacy) throws IOException { + String responseBody = writeToResponse(value, legacy); + return ObjectMapperUtil.createStandardObjectMapper() + .readValue(responseBody, ObjectNode.class); + } + + private String writeToResponse(Object value, boolean legacy) throws IOException { MockHttpServletResponse response = new MockHttpServletResponse(); - ServletResponseResultWriter writer = new ServletResponseResultWriter(response, null); - writer.write(value); + ServletResponseResultWriter writer = legacy + ? new ServletResponseResultWriter(response, getLegacyObjectWriter(), false, false) + : getDefaultWriter(response); + writer.write(value, HttpServletResponse.SC_OK); return response.getContentAsString(); } + + @Test + public void testExceptionWriterShouldNotBeCustomized() throws IOException { + MockHttpServletResponse response = new MockHttpServletResponse(); + ServletResponseResultWriter writer = createCustomizedWriter(response); + ServiceException exception = new ServiceException(400, "sample message"); + writer.writeError(exception); + String errorContent = response.getContentAsString(); + assertEquals("{\"error_message\":\"sample message\"}", errorContent); + } + + @Test + public void testWriterCustomization() throws IOException { + Map unorderedMap = new HashMap<>(); + unorderedMap.put("a", "value_a"); + MockHttpServletResponse response = new MockHttpServletResponse(); + ServletResponseResultWriter writer = createCustomizedWriter(response); + writer.write(unorderedMap, HttpServletResponse.SC_OK); + String content = response.getContentAsString(); + assertEquals("{a:\"value_a\"}", content); + } + + //Customized writer: for response, the fields name has no quote. For error, they have. + private ServletResponseResultWriter createCustomizedWriter(HttpServletResponse response) { + return new ServletResponseResultWriter(response, (ApiSerializationConfig) null, false, false) { + @Override + protected ObjectWriter configureWriter(ObjectWriter objectWriter) { + return objectWriter.withoutFeatures(JsonWriteFeature.QUOTE_FIELD_NAMES); + } + }; + } + + //creates an object writer with configuration as before 2.4 + private ObjectWriter getLegacyObjectWriter() { + ObjectMapper mapper = ObjectMapperUtil.createStandardObjectMapper(null); + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + mapper.disable(SerializationFeature.WRITE_EMPTY_JSON_ARRAYS); + return ConfiguredObjectMapper.builder() + .addRegisteredModules(ServletResponseResultWriter.WRITER_MODULES) + .buildWithCustomMapper(mapper).writer(); + } + + private ServletResponseResultWriter getDefaultWriter(MockHttpServletResponse response) { + return new ServletResponseResultWriter( + response, (ApiSerializationConfig) null, false, false); + } } diff --git a/endpoints-framework/src/test/java/com/google/api/server/spi/swagger/SwaggerGeneratorTest.java b/endpoints-framework/src/test/java/com/google/api/server/spi/swagger/SwaggerGeneratorTest.java index 471ba6ea..c403fa74 100644 --- a/endpoints-framework/src/test/java/com/google/api/server/spi/swagger/SwaggerGeneratorTest.java +++ b/endpoints-framework/src/test/java/com/google/api/server/spi/swagger/SwaggerGeneratorTest.java @@ -23,6 +23,7 @@ import com.google.api.server.spi.Constant; import com.google.api.server.spi.IoUtil; import com.google.api.server.spi.ServiceContext; +import com.google.api.server.spi.ServiceException; import com.google.api.server.spi.TypeLoader; import com.google.api.server.spi.config.AnnotationBoolean; import com.google.api.server.spi.config.Api; @@ -30,13 +31,19 @@ import com.google.api.server.spi.config.ApiIssuer; import com.google.api.server.spi.config.ApiIssuerAudience; import com.google.api.server.spi.config.ApiMethod; +import com.google.api.server.spi.config.ApiMethod.HttpMethod; +import com.google.api.server.spi.config.Named; import com.google.api.server.spi.config.annotationreader.ApiConfigAnnotationReader; import com.google.api.server.spi.config.model.ApiConfig; +import com.google.api.server.spi.response.BadRequestException; +import com.google.api.server.spi.response.ConflictException; +import com.google.api.server.spi.response.NotFoundException; import com.google.api.server.spi.swagger.SwaggerGenerator.SwaggerContext; import com.google.api.server.spi.testing.AbsoluteCommonPathEndpoint; import com.google.api.server.spi.testing.AbsolutePathEndpoint; import com.google.api.server.spi.testing.ArrayEndpoint; import com.google.api.server.spi.testing.EnumEndpoint; +import com.google.api.server.spi.testing.FooCommonParamsEndpoint; import com.google.api.server.spi.testing.FooDescriptionEndpoint; import com.google.api.server.spi.testing.FooEndpoint; import com.google.api.server.spi.testing.LimitMetricsEndpoint; @@ -47,9 +54,12 @@ import com.google.api.server.spi.testing.MultiResourceEndpoint.Resource2Endpoint; import com.google.api.server.spi.testing.MultiVersionEndpoint.Version1Endpoint; import com.google.api.server.spi.testing.MultiVersionEndpoint.Version2Endpoint; -import com.google.common.collect.HashMultimap; +import com.google.api.server.spi.testing.OptionalEndpoint; +import com.google.api.server.spi.testing.RequiredPropertiesEndpoint; +import com.google.api.server.spi.testing.SpecialCharsEndpoint; +import com.google.api.server.spi.testing.ResponseStatusEndpoint; +import com.google.api.server.spi.testing.ValidationEndpoint; import com.google.common.collect.ImmutableList; -import com.google.common.collect.Multimap; import com.fasterxml.jackson.databind.ObjectMapper; @@ -59,15 +69,9 @@ import org.junit.runner.RunWith; import org.junit.runners.JUnit4; -import io.swagger.models.HttpMethod; -import io.swagger.models.Operation; -import io.swagger.models.Path; import io.swagger.models.Swagger; import io.swagger.util.Json; -import java.util.Collection; -import java.util.Map.Entry; - /** * Tests for {@link SwaggerGenerator}. */ @@ -75,7 +79,9 @@ public class SwaggerGeneratorTest { private final SwaggerGenerator generator = new SwaggerGenerator(); private final SwaggerContext context = new SwaggerContext() - .setApiRoot("https://swagger-test.appspot.com/api"); + .setScheme("https") + .setHostname("swagger-test.appspot.com") + .setBasePath("/api"); private final ObjectMapper mapper = Json.mapper(); private ApiConfigLoader configLoader; @@ -91,44 +97,86 @@ public void setUp() throws Exception { @Test public void testWriteSwagger_FooEndpoint() throws Exception { ApiConfig config = configLoader.loadConfiguration(ServiceContext.create(), FooEndpoint.class); - Swagger swagger = generator.writeSwagger(ImmutableList.of(config), false, context); + Swagger swagger = generator.writeSwagger(ImmutableList.of(config), context); Swagger expected = readExpectedAsSwagger("foo_endpoint.swagger"); checkSwagger(expected, swagger); } + @Test + public void testWriteSwagger_FooEndpointCustomTemplates() throws Exception { + ApiConfig config = configLoader.loadConfiguration(ServiceContext.create(), FooEndpoint.class); + Swagger swagger = generator.writeSwagger(ImmutableList.of(config), new SwaggerContext() + .setTagTemplate("${ApiName}${ApiVersion}") + .setOperationIdTemplate("${apiName}-${apiVersion}-${method}") + ); + Swagger expected = readExpectedAsSwagger("foo_endpoint_custom_templates.swagger"); + checkSwagger(expected, swagger); + } + + @Test + public void testWriteSwagger_FooEndpointParameterCombineParamSamePath() throws Exception { + ApiConfig config = configLoader.loadConfiguration(ServiceContext.create(), + FooCommonParamsEndpoint.class); + Swagger swagger = generator.writeSwagger(ImmutableList.of(config), context + .setCombineCommonParametersInSamePath(true)); + Swagger expected = readExpectedAsSwagger("foo_endpoint_combine_params_same_path.swagger"); + checkSwagger(expected, swagger); + } + + @Test + public void testWriteSwagger_FooEndpointParameterExtractParamRef() throws Exception { + ApiConfig config = configLoader.loadConfiguration(ServiceContext.create(), + FooCommonParamsEndpoint.class); + Swagger swagger = generator.writeSwagger(ImmutableList.of(config), context + .setExtractCommonParametersAsRefs(true)); + Swagger expected = readExpectedAsSwagger("foo_endpoint_extract_param_refs.swagger"); + checkSwagger(expected, swagger); + } + + @Test + public void testWriteSwagger_FooEndpointParameterCombineAllParam() throws Exception { + ApiConfig config = configLoader.loadConfiguration(ServiceContext.create(), + FooCommonParamsEndpoint.class); + Swagger swagger = generator.writeSwagger(ImmutableList.of(config), context + .setExtractCommonParametersAsRefs(true) + .setCombineCommonParametersInSamePath(true)); + Swagger expected = readExpectedAsSwagger("foo_endpoint_combine_all_params.swagger"); + checkSwagger(expected, swagger); + } + @Test public void testWriteSwagger_FooEndpointDefaultContext() throws Exception { ApiConfig config = configLoader.loadConfiguration(ServiceContext.create(), FooEndpoint.class); - Swagger swagger = generator.writeSwagger(ImmutableList.of(config), false, new SwaggerContext()); + Swagger swagger = generator.writeSwagger(ImmutableList.of(config), new SwaggerContext()); Swagger expected = readExpectedAsSwagger("foo_endpoint_default_context.swagger"); checkSwagger(expected, swagger); } @Test - public void testWriteSwagger_FooEndpointLocalhost() throws Exception { + public void testWriteSwagger_FooEndpointWithApiName() throws Exception { Swagger swagger = getSwagger( - FooEndpoint.class, new SwaggerContext().setApiRoot("http://localhost:8080/api"), false); - Swagger expected = readExpectedAsSwagger("foo_endpoint_localhost.swagger"); + FooEndpoint.class, new SwaggerContext().setApiName("customApiName")); + Swagger expected = readExpectedAsSwagger("foo_endpoint_api_name.swagger"); checkSwagger(expected, swagger); } @Test public void testWriteSwagger_EnumEndpoint() throws Exception { - Swagger swagger = getSwagger(EnumEndpoint.class, new SwaggerContext(), true); + Swagger swagger = getSwagger(EnumEndpoint.class, new SwaggerContext()); Swagger expected = readExpectedAsSwagger("enum_endpoint.swagger"); checkSwagger(expected, swagger); } @Test public void testWriteSwagger_ArrayEndpoint() throws Exception { - Swagger swagger = getSwagger(ArrayEndpoint.class, new SwaggerContext(), true); + Swagger swagger = getSwagger(ArrayEndpoint.class, new SwaggerContext()); Swagger expected = readExpectedAsSwagger("array_endpoint.swagger"); checkSwagger(expected, swagger); } @Test public void testWriteSwagger_MapEndpoint() throws Exception { - Swagger swagger = getSwagger(MapEndpoint.class, new SwaggerContext(), true); + Swagger swagger = getSwagger(MapEndpoint.class, new SwaggerContext()); Swagger expected = readExpectedAsSwagger("map_endpoint.swagger"); checkSwagger(expected, swagger); } @@ -137,7 +185,7 @@ public void testWriteSwagger_MapEndpoint() throws Exception { public void testWriteSwagger_MapEndpoint_Legacy() throws Exception { System.setProperty(MAP_SCHEMA_FORCE_JSON_MAP_SCHEMA.systemPropertyName, ""); try { - Swagger swagger = getSwagger(MapEndpoint.class, new SwaggerContext(), true); + Swagger swagger = getSwagger(MapEndpoint.class, new SwaggerContext()); Swagger expected = readExpectedAsSwagger("map_endpoint_legacy.swagger"); checkSwagger(expected, swagger); } finally { @@ -148,7 +196,7 @@ public void testWriteSwagger_MapEndpoint_Legacy() throws Exception { @Test public void testWriteDiscovery_MapEndpoint_InvalidKeyType() throws Exception { try { - getSwagger(MapEndpointInvalid.class, new SwaggerContext(), true); + getSwagger(MapEndpointInvalid.class, new SwaggerContext()); Assert.fail("Should have failed to generate schema for invalid key type"); } catch (IllegalArgumentException e) { //expected @@ -159,7 +207,7 @@ public void testWriteDiscovery_MapEndpoint_InvalidKeyType() throws Exception { public void testWriteDiscovery_MapEndpoint_InvalidKeyType_ignore() throws Exception { System.setProperty(MAP_SCHEMA_IGNORE_UNSUPPORTED_KEY_TYPES.systemPropertyName, "true"); try { - getSwagger(MapEndpointInvalid.class, new SwaggerContext(), true); + getSwagger(MapEndpointInvalid.class, new SwaggerContext()); } finally { System.clearProperty(MAP_SCHEMA_IGNORE_UNSUPPORTED_KEY_TYPES.systemPropertyName); } @@ -169,7 +217,7 @@ public void testWriteDiscovery_MapEndpoint_InvalidKeyType_ignore() throws Except public void testWriteSwagger_MapEndpoint_WithArrayValue() throws Exception { System.setProperty(MAP_SCHEMA_SUPPORT_ARRAYS_VALUES.systemPropertyName, "TRUE"); try { - Swagger swagger = getSwagger(MapEndpoint.class, new SwaggerContext(), true); + Swagger swagger = getSwagger(MapEndpoint.class, new SwaggerContext()); Swagger expected = readExpectedAsSwagger("map_endpoint_with_array.swagger"); checkSwagger(expected, swagger); } finally { @@ -177,19 +225,11 @@ public void testWriteSwagger_MapEndpoint_WithArrayValue() throws Exception { } } - @Test - public void testWriteSwagger_FooEndpoint_internal() throws Exception { - ApiConfig config = configLoader.loadConfiguration(ServiceContext.create(), FooEndpoint.class); - Swagger swagger = generator.writeSwagger(ImmutableList.of(config), true, context); - Swagger expected = readExpectedAsSwagger("foo_endpoint_internal.swagger"); - checkSwagger(expected, swagger); - } - @Test public void testWriteSwagger_ThirdPartyAuthEndpoint() throws Exception { ApiConfig config = configLoader.loadConfiguration(ServiceContext.create(), ThirdPartyAuthEndpoint.class); - Swagger swagger = generator.writeSwagger(ImmutableList.of(config), true, context); + Swagger swagger = generator.writeSwagger(ImmutableList.of(config), context); Swagger expected = readExpectedAsSwagger("third_party_auth.swagger"); checkSwagger(expected, swagger); } @@ -198,37 +238,46 @@ public void testWriteSwagger_ThirdPartyAuthEndpoint() throws Exception { public void testWriteSwagger_GoogleAuthEndpoint() throws Exception { ApiConfig config = configLoader.loadConfiguration(ServiceContext.create(), GoogleAuthEndpoint.class); - Swagger swagger = generator.writeSwagger(ImmutableList.of(config), true, context); + Swagger swagger = generator.writeSwagger(ImmutableList.of(config), context); Swagger expected = readExpectedAsSwagger("google_auth.swagger"); checkSwagger(expected, swagger); } + @Test + public void testWriteSwagger_MultipleScopes() throws Exception { + ApiConfig config = + configLoader.loadConfiguration(ServiceContext.create(), MultipleScopesEndpoint.class); + Swagger swagger = generator.writeSwagger(ImmutableList.of(config), context); + Swagger expected = readExpectedAsSwagger("multiple_scopes.swagger"); + checkSwagger(expected, swagger); + } + @Test public void testWriteSwagger_ApiKeys() throws Exception { ApiConfig config = configLoader.loadConfiguration(ServiceContext.create(), ApiKeysEndpoint.class); - Swagger swagger = generator.writeSwagger(ImmutableList.of(config), true, context); + Swagger swagger = generator.writeSwagger(ImmutableList.of(config), context); Swagger expected = readExpectedAsSwagger("api_keys.swagger"); checkSwagger(expected, swagger); } @Test public void testWriteSwagger_AbsolutePathEndpoint() throws Exception { - Swagger swagger = getSwagger(AbsolutePathEndpoint.class, new SwaggerContext(), true); + Swagger swagger = getSwagger(AbsolutePathEndpoint.class, new SwaggerContext()); Swagger expected = readExpectedAsSwagger("absolute_path_endpoint.swagger"); checkSwagger(expected, swagger); } @Test public void testWriteSwagger_AbsoluteCommonPathEndpoint() throws Exception { - Swagger swagger = getSwagger(AbsoluteCommonPathEndpoint.class, new SwaggerContext(), true); + Swagger swagger = getSwagger(AbsoluteCommonPathEndpoint.class, new SwaggerContext()); Swagger expected = readExpectedAsSwagger("absolute_common_path_endpoint.swagger"); checkSwagger(expected, swagger); } @Test public void testWriteSwagger_LimitMetricsEndpoint() throws Exception { - Swagger swagger = getSwagger(LimitMetricsEndpoint.class, new SwaggerContext(), true); + Swagger swagger = getSwagger(LimitMetricsEndpoint.class, new SwaggerContext()); Swagger expected = readExpectedAsSwagger("limit_metrics_endpoint.swagger"); checkSwagger(expected, swagger); } @@ -236,11 +285,19 @@ public void testWriteSwagger_LimitMetricsEndpoint() throws Exception { @Test public void testWriteSwagger_FooEndpointWithDescription() throws Exception { ApiConfig config = configLoader.loadConfiguration(ServiceContext.create(), FooDescriptionEndpoint.class); - Swagger swagger = generator.writeSwagger(ImmutableList.of(config), false, context); + Swagger swagger = generator.writeSwagger(ImmutableList.of(config), context); Swagger expected = readExpectedAsSwagger("foo_with_description_endpoint.swagger"); checkSwagger(expected, swagger); } + @Test + public void testWriteSwagger_RequiredPropertiesEndpoint() throws Exception { + ApiConfig config = configLoader.loadConfiguration(ServiceContext.create(), RequiredPropertiesEndpoint.class); + Swagger swagger = generator.writeSwagger(ImmutableList.of(config), context); + Swagger expected = readExpectedAsSwagger("required_parameters_endpoint.swagger"); + checkSwagger(expected, swagger); + } + @Test public void testWriteSwagger_MultiResourceEndpoint() throws Exception { ServiceContext serviceContext = ServiceContext.create(); @@ -248,7 +305,7 @@ public void testWriteSwagger_MultiResourceEndpoint() throws Exception { configLoader.loadConfiguration(serviceContext, NoResourceEndpoint.class), configLoader.loadConfiguration(serviceContext, Resource1Endpoint.class), configLoader.loadConfiguration(serviceContext, Resource2Endpoint.class)); - Swagger swagger = generator.writeSwagger(configs, false, context); + Swagger swagger = generator.writeSwagger(configs, context); Swagger expected = readExpectedAsSwagger("multi_resource_endpoint.swagger"); checkSwagger(expected, swagger); } @@ -259,67 +316,96 @@ public void testWriteSwagger_MultiVersionEndpoint() throws Exception { ImmutableList configs = ImmutableList.of( configLoader.loadConfiguration(serviceContext, Version1Endpoint.class), configLoader.loadConfiguration(serviceContext, Version2Endpoint.class)); - Swagger swagger = generator.writeSwagger(configs, false, context); + Swagger swagger = generator.writeSwagger(configs, context); Swagger expected = readExpectedAsSwagger("multi_version_endpoint.swagger"); checkSwagger(expected, swagger); } - private Swagger getSwagger(Class serviceClass, SwaggerContext context, boolean internal) - throws Exception { - ApiConfig config = configLoader.loadConfiguration(ServiceContext.create(), serviceClass); - return generator.writeSwagger(ImmutableList.of(config), internal, context); + @Test + public void testWriteSwagger_ErrorAsDefaultResponse() throws Exception { + ApiConfig config = configLoader.loadConfiguration(ServiceContext.create(), ExceptionEndpoint.class); + Swagger swagger = generator.writeSwagger(ImmutableList.of(config), context + .setAddGoogleJsonErrorAsDefaultResponse(true)); + Swagger expected = readExpectedAsSwagger("error_codes_default_response.swagger"); + checkSwagger(expected, swagger); } - private Swagger readExpectedAsSwagger(String file) throws Exception { - String expectedString = IoUtil.readResourceFile(SwaggerGeneratorTest.class, file); - return mapper.readValue(expectedString, Swagger.class); + @Test + public void testWriteSwagger_ServiceExceptionErrorCodes() throws Exception { + ApiConfig config = configLoader.loadConfiguration(ServiceContext.create(), ExceptionEndpoint.class); + Swagger swagger = generator.writeSwagger(ImmutableList.of(config), context + .setAddErrorCodesForServiceExceptions(true)); + Swagger expected = readExpectedAsSwagger("error_codes_service_exceptions.swagger"); + checkSwagger(expected, swagger); } - private void checkSwagger(Swagger expected, Swagger actual) throws Exception { - compareSwagger(expected, actual); - // operationIds should be unique to be deployed on Endpoints Management - checkDuplicateOperations(actual); - // Jackson preserves order when deserializing expected result, and SwaggerGenerator should - // always output resource and security definitions in the same order - checkOrdering(expected, actual); + @Test + public void testWriteSwagger_ResponseStatus() throws Exception { + ApiConfig config = configLoader.loadConfiguration(ServiceContext.create(), ResponseStatusEndpoint.class); + Swagger swagger = generator.writeSwagger(ImmutableList.of(config), context); + Swagger expected = readExpectedAsSwagger("response_status.swagger"); + checkSwagger(expected, swagger); } - private void compareSwagger(Swagger expected, Swagger actual) throws Exception { - System.out.println("Actual: " + mapper.writeValueAsString(actual)); - System.out.println("Expected: " + mapper.writeValueAsString(expected)); - assertThat(actual).isEqualTo(expected); - // TODO: Remove once Swagger models check this in equals - assertThat(actual.getVendorExtensions()).isEqualTo(expected.getVendorExtensions()); + @Test + public void testWriteSwagger_AllErrors() throws Exception { + ApiConfig config = configLoader.loadConfiguration(ServiceContext.create(), ExceptionEndpoint.class); + Swagger swagger = generator.writeSwagger(ImmutableList.of(config), context + .setAddGoogleJsonErrorAsDefaultResponse(true) + .setAddErrorCodesForServiceExceptions(true)); + Swagger expected = readExpectedAsSwagger("error_codes_all.swagger"); + checkSwagger(expected, swagger); + } + + @Test + public void testWriteSwagger_Optional() throws Exception { + ApiConfig config = configLoader.loadConfiguration(ServiceContext.create(), OptionalEndpoint.class); + Swagger swagger = generator.writeSwagger(ImmutableList.of(config), context); + Swagger expected = readExpectedAsSwagger("optional_endpoint.swagger"); + checkSwagger(expected, swagger); } - private void checkDuplicateOperations(Swagger actual) { - Multimap operationIds = HashMultimap.create(); - for (Entry pathEntry : actual.getPaths().entrySet()) { - for (Entry opEntry : pathEntry.getValue().getOperationMap() - .entrySet()) { - operationIds - .put(opEntry.getValue().getOperationId(), pathEntry.getKey() + "|" + opEntry.getKey()); - } - } - int duplicateOperationIdCount = 0; - for (Entry> entry : operationIds.asMap().entrySet()) { - if (entry.getValue().size() > 1) { - System.out.println("Duplicate operation id: " + entry); - duplicateOperationIdCount++; - } + @Test + public void testEquivalentPathsNotAccepted() { + try { + ApiConfig config = configLoader.loadConfiguration(ServiceContext.create(), EquivalentPathsEndpoint.class); + generator.writeSwagger(ImmutableList.of(config), context); + } catch (Exception e) { + assertThat(e).isInstanceOf(IllegalStateException.class); + assertThat(e).hasMessageThat().contains("Equivalent paths found"); } - assertThat(duplicateOperationIdCount).named("Duplicate operation ids").isEqualTo(0); } - private void checkOrdering(Swagger expected, Swagger actual) { - if (expected.getSecurityDefinitions() != null && actual.getSecurityDefinitions() != null) { - assertThat(ImmutableList.of(expected.getSecurityDefinitions().keySet())) - .isEqualTo(ImmutableList.of(actual.getSecurityDefinitions().keySet())); - } - if (expected.getDefinitions() != null && actual.getDefinitions() != null) { - assertThat(ImmutableList.of(expected.getDefinitions().keySet())) - .isEqualTo(ImmutableList.of(actual.getDefinitions().keySet())); - } + @Test + public void testWriteSwagger_SpecialChars() throws Exception { + ApiConfig config = configLoader.loadConfiguration(ServiceContext.create(), SpecialCharsEndpoint.class); + Swagger swagger = generator.writeSwagger(ImmutableList.of(config), context + .setExtractCommonParametersAsRefs(true)); + Swagger expected = readExpectedAsSwagger("special_chars.swagger"); + checkSwagger(expected, swagger); + } + + @Test + public void testWriteSwagger_ValidationEndpoint() throws Exception { + Swagger swagger = getSwagger(ValidationEndpoint.class, new SwaggerContext()); + Swagger expected = readExpectedAsSwagger("validation_endpoint.swagger"); + checkSwagger(expected, swagger); + } + + private Swagger getSwagger(Class serviceClass, SwaggerContext context) + throws Exception { + ApiConfig config = configLoader.loadConfiguration(ServiceContext.create(), serviceClass); + return generator.writeSwagger(ImmutableList.of(config), context); + } + + private Swagger readExpectedAsSwagger(String file) throws Exception { + String expectedString = IoUtil.readResourceFile(SwaggerGeneratorTest.class, file); + return mapper.readValue(expectedString, Swagger.class); + } + + private void checkSwagger(Swagger expected, Swagger actual) { + SwaggerSubject.assertThat(actual).isValid(); + SwaggerSubject.assertThat(actual).isSameAs(expected); } @Api(name = "thirdparty", version = "v1", @@ -345,7 +431,7 @@ public void noOverride() { } private static class GoogleAuthEndpoint extends ThirdPartyAuthEndpoint { @ApiMethod( issuerAudiences = { - @ApiIssuerAudience(name = Constant.GOOGLE_ID_TOKEN_NAME, audiences = "googleaud") + @ApiIssuerAudience(name = Constant.GOOGLE_ID_TOKEN_ALT, audiences = "googleaud") } ) public void googleAuth() { } @@ -370,4 +456,47 @@ public void inheritApiKeySetting() { } }) public void apiKeyWithAuth() { } } + + @Api(name = "multipleScopes", + version = "v1", + audiences = {"audience"}, + scopes = "https://mail.google.com/") + private static class MultipleScopesEndpoint { + @ApiMethod + public void noOverride() { } + @ApiMethod(scopes = Constant.API_EMAIL_SCOPE) + public void scopeOverride() { } + @ApiMethod(scopes = "unknownScope") + public void unknownScope() { } + @ApiMethod(audiences = {"audience2"}) + public void overrideAudience() { } + } + + @Api(name = "exceptions", version = "v1") + private static class ExceptionEndpoint { + @ApiMethod + public void doesNotThrow() { } + + @ApiMethod + public void throwsServiceException() throws ServiceException { } + + @ApiMethod + public void throwsNotFoundException() throws NotFoundException { } + + @ApiMethod + public void throwsMultipleExceptions() throws BadRequestException, ConflictException { } + + @ApiMethod + public void throwsUnknownException() throws IllegalStateException { } + } + + @Api(name = "equivalentPaths", version = "v1") + private static class EquivalentPathsEndpoint { + @ApiMethod(path = "foo/{id}", httpMethod = HttpMethod.GET) + public void path1(@Named("id") String id) { } + + @ApiMethod(path = "foo/{fooId}", httpMethod = HttpMethod.POST) + public void path2(@Named("fooId") String fooId) { } + } + } diff --git a/endpoints-framework/src/test/java/com/google/api/server/spi/swagger/SwaggerSubject.java b/endpoints-framework/src/test/java/com/google/api/server/spi/swagger/SwaggerSubject.java new file mode 100644 index 00000000..740218d5 --- /dev/null +++ b/endpoints-framework/src/test/java/com/google/api/server/spi/swagger/SwaggerSubject.java @@ -0,0 +1,158 @@ +package com.google.api.server.spi.swagger; + +import static com.google.common.truth.Truth.assertAbout; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectWriter; +import com.google.common.base.Predicates; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Multimap; +import com.google.common.collect.Sets; +import com.google.common.truth.Fact; +import com.google.common.truth.FailureMetadata; +import com.google.common.truth.Subject; +import io.swagger.models.HttpMethod; +import io.swagger.models.ModelImpl; +import io.swagger.models.Operation; +import io.swagger.models.Path; +import io.swagger.models.Swagger; +import io.swagger.util.Json; +import io.swagger.validator.models.SchemaValidationError; +import io.swagger.validator.models.ValidationResponse; +import io.swagger.validator.services.ValidatorService; +import java.util.List; +import java.util.stream.Collectors; +import org.checkerframework.checker.nullness.compatqual.NullableDecl; +import org.junit.ComparisonFailure; + +import java.util.Collection; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; + +import javax.annotation.Nullable; + +public final class SwaggerSubject extends Subject { + + private final ObjectWriter witer = Json.mapper().writerWithDefaultPrettyPrinter(); + + private final Swagger actual; + + public static SwaggerSubject assertThat(@NullableDecl Swagger swagger) { + return assertAbout(swaggers()).that(swagger); + } + + private static Factory swaggers() { + return SwaggerSubject::new; + } + + private SwaggerSubject(FailureMetadata failureStrategy, @Nullable Object actual) { + super(failureStrategy, actual); + this.actual = actual instanceof Swagger ? (Swagger) actual : null; + } + + void isValid() { + validatesSchema(); + hasNoDuplicateOperations(); + } + + private void validatesSchema() { + //TODO there is probably a better way to validate, to get errors like https://editor.swagger.io/ + // This validator will only validate against the JsonSchema, not check references for example + try { + ValidationResponse validationResponse = new ValidatorService() + .debugByContent(null, null, toString(actual)); + List schemaValidationMessages = validationResponse + .getSchemaValidationMessages(); + if (schemaValidationMessages != null && !schemaValidationMessages.isEmpty()) { + System.out.println("Swagger spec: " + toString(actual)); + throw new AssertionError("Swagger spec is not valid" + + schemaValidationMessages.stream() + .map(error -> "\nValidation error: " + toString(error)) + .collect(Collectors.joining())); + } + } catch (Exception e) { + throw new AssertionError("Could not validate Swagger spec", e); + } + } + + private void hasNoDuplicateOperations() { + Multimap operationIds = HashMultimap.create(); + for (Entry pathEntry : actual.getPaths().entrySet()) { + for (Entry opEntry : pathEntry.getValue().getOperationMap() + .entrySet()) { + operationIds + .put(opEntry.getValue().getOperationId(), pathEntry.getKey() + "|" + opEntry.getKey()); + } + } + Set duplicateOperationIds = Sets.newHashSet(); + for (Entry> entry : operationIds.asMap().entrySet()) { + if (entry.getValue().size() > 1) { + System.out.println("Duplicate operation id: " + entry); + duplicateOperationIds.add(entry.getKey()); + } + } + if (!duplicateOperationIds.isEmpty()) { + failWithActual("Duplicates operations ids found", duplicateOperationIds); + } + } + + void isSameAs(Swagger expected) { + checkEquality(expected); + // Jackson preserves order when deserializing expected result, so we should + // always output resource and security definitions in the same order + compareMapOrdering("Security definition", actual, expected, Swagger::getSecurityDefinitions); + compareMapOrdering("Model definition", actual, expected, Swagger::getDefinitions); + compareMapOrdering("Path", actual, expected, Swagger::getPaths); + compareMapOrdering("Parameter", actual, expected, Swagger::getParameters); + compareMapOrdering("Response", actual, expected, Swagger::getResponses); + } + + private void checkEquality(Swagger expected) { + SwaggerGenerator.normalizeOperationParameters(expected); + normalizeRequiredPropertyList(actual); + normalizeRequiredPropertyList(expected); + if (!Objects.equals(actual, expected)) { + throw new ComparisonFailure("Swagger specs don't match", + toString(expected), toString(actual)); + } + } + + //ModelImpl.required is not "persisted", but gathered from properties + private void normalizeRequiredPropertyList(Swagger swagger) { + if (swagger.getDefinitions() != null) { + swagger.getDefinitions().values().stream() + .filter(clazz -> clazz instanceof ModelImpl) + .map(model -> (ModelImpl) model) + .forEach(model -> model.setRequired(model.getRequired())); + } + } + + private void compareMapOrdering(String message, Swagger actual, + Swagger expected, Function> mapFunction) { + Map actualMap = mapFunction.apply(actual); + Map expectedMap = mapFunction.apply(expected); + if (expectedMap != null && actualMap != null) { + Set actualKeys = actualMap.keySet(); + Set expectedKeys = expectedMap.keySet(); + if (!ImmutableList.copyOf(actualKeys).equals(ImmutableList.copyOf(expectedKeys))) { + throw new ComparisonFailure(message + " orders don't match" + + Fact.fact("\nExpected keys", expectedKeys).toString() + + Fact.fact("\nActual keys", actualKeys).toString(), + toString(expected), toString(actual)); + } + } + } + + private String toString(Object toJson) { + try { + return witer.writeValueAsString(toJson); + } catch (JsonProcessingException e) { + throw new AssertionError("Cannot create String representation", e); + } + } + +} diff --git a/endpoints-framework/src/test/resources/com/google/api/server/spi/discovery/array_endpoint.json b/endpoints-framework/src/test/resources/com/google/api/server/spi/discovery/array_endpoint.json index da404334..de337168 100644 --- a/endpoints-framework/src/test/resources/com/google/api/server/spi/discovery/array_endpoint.json +++ b/endpoints-framework/src/test/resources/com/google/api/server/spi/discovery/array_endpoint.json @@ -211,6 +211,237 @@ "scopes": [ "https://www.googleapis.com/auth/userinfo.email" ] + }, + "setListOfBooleans": { + "httpMethod": "POST", + "id": "myapi.arrayEndpoint.setListOfBooleans", + "parameterOrder": [ + "list", + "array" + ], + "parameters": { + "list": { + "location": "path", + "repeated": true, + "required": true, + "type": "boolean" + }, + "array": { + "location": "path", + "repeated": true, + "required": true, + "type": "boolean" + } + }, + "path": "setListOfBooleans/{list}/{array}", + "scopes": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + }, + "setListOfByteArrays": { + "httpMethod": "POST", + "id": "myapi.arrayEndpoint.setListOfByteArrays", + "parameterOrder": [ + "list", + "array" + ], + "parameters": { + "list": { + "format": "byte", + "location": "path", + "repeated": true, + "required": true, + "type": "string" + }, + "array": { + "format": "byte", + "location": "path", + "repeated": true, + "required": true, + "type": "string" + } + }, + "path": "setListOfByteArrays/{list}/{array}", + "scopes": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + }, + "setListOfDoubles": { + "httpMethod": "POST", + "id": "myapi.arrayEndpoint.setListOfDoubles", + "parameterOrder": [ + "list", + "array" + ], + "parameters": { + "list": { + "format": "double", + "location": "path", + "repeated": true, + "required": true, + "type": "number" + }, + "array": { + "format": "double", + "location": "path", + "repeated": true, + "required": true, + "type": "number" + } + }, + "path": "setListOfDoubles/{list}/{array}", + "scopes": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + }, + "setListOfEnums": { + "httpMethod": "POST", + "id": "myapi.arrayEndpoint.setListOfEnums", + "parameterOrder": [ + "list" + ], + "parameters": { + "list": { + "enum": [ + "VALUE1", + "value_2" + ], + "enumDescriptions": [ + "", + "" + ], + "location": "path", + "repeated": true, + "required": true, + "type": "string" + } + }, + "path": "setListOfEnums/{list}", + "scopes": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + }, + "setListOfFloats": { + "httpMethod": "POST", + "id": "myapi.arrayEndpoint.setListOfFloats", + "parameterOrder": [ + "list", + "array" + ], + "parameters": { + "list": { + "format": "float", + "location": "path", + "repeated": true, + "required": true, + "type": "number" + }, + "array": { + "format": "float", + "location": "path", + "repeated": true, + "required": true, + "type": "number" + } + }, + "path": "setListOfFloats/{list}/{array}", + "scopes": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + }, + "setListOfIntegers": { + "httpMethod": "POST", + "id": "myapi.arrayEndpoint.setListOfIntegers", + "parameterOrder": [ + "list", + "array" + ], + "parameters": { + "list": { + "format": "int32", + "location": "path", + "repeated": true, + "required": true, + "type": "integer" + }, + "array": { + "format": "int32", + "location": "path", + "repeated": true, + "required": true, + "type": "integer" + } + }, + "path": "setListOfIntegers/{list}/{array}", + "scopes": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + }, + "setListOfLongs": { + "httpMethod": "POST", + "id": "myapi.arrayEndpoint.setListOfLongs", + "parameterOrder": [ + "list", + "array" + ], + "parameters": { + "list": { + "format": "int64", + "location": "path", + "repeated": true, + "required": true, + "type": "string" + }, + "array": { + "format": "int64", + "location": "path", + "repeated": true, + "required": true, + "type": "string" + } + }, + "path": "setListOfLongs/{list}/{array}", + "scopes": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + }, + "setListOfString": { + "httpMethod": "POST", + "id": "myapi.arrayEndpoint.setListOfString", + "parameterOrder": [ + "list" + ], + "parameters": { + "list": { + "location": "path", + "repeated": true, + "required": true, + "type": "string" + } + }, + "path": "setListOfString/{list}", + "scopes": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + }, + "setListOfStringAsQueryParam": { + "httpMethod": "POST", + "id": "myapi.arrayEndpoint.setListOfStringAsQueryParam", + "parameterOrder": [ + "list" + ], + "parameters": { + "list": { + "location": "query", + "repeated": true, + "required": true, + "type": "string" + } + }, + "path": "setListOfStringAsQueryParam", + "scopes": [ + "https://www.googleapis.com/auth/userinfo.email" + ] } } } @@ -275,9 +506,21 @@ "integersResponse": { "$ref": "CollectionResponse_Integer" }, + "listOfEnums": { + "items": { + "$ref": "TestEnum" + }, + "type": "array" + }, "listOfString": { "$ref": "ListContainer" }, + "listOfStringAsQueryParam": { + "items": { + "type": "string" + }, + "type": "array" + }, "objectIntegers": { "items": { "format": "int32", @@ -387,6 +630,7 @@ "type": "object" }, "FooCollection": { + "description": "An ordered list of Foo", "id": "FooCollection", "properties": { "items": { @@ -437,6 +681,18 @@ } }, "type": "object" + }, + "TestEnum": { + "enum": [ + "VALUE1", + "value_2" + ], + "enumDescriptions": [ + "", + "" + ], + "id": "TestEnum", + "type": "string" } }, "servicePath": "myapi/v1/", diff --git a/endpoints-framework/src/test/resources/com/google/api/server/spi/discovery/enum_endpoint.json b/endpoints-framework/src/test/resources/com/google/api/server/spi/discovery/enum_endpoint.json index 5ef91688..f9dcbd9d 100644 --- a/endpoints-framework/src/test/resources/com/google/api/server/spi/discovery/enum_endpoint.json +++ b/endpoints-framework/src/test/resources/com/google/api/server/spi/discovery/enum_endpoint.json @@ -30,7 +30,7 @@ "value": { "enum": [ "VALUE1", - "VALUE2" + "value_2" ], "enumDescriptions": [ "", @@ -110,7 +110,7 @@ }, "TestEnum": { "id": "TestEnum", - "enum" : [ "VALUE1", "VALUE2" ], + "enum" : [ "VALUE1", "value_2" ], "enumDescriptions" : [ "", "" ], "type": "string" } diff --git a/endpoints-framework/src/test/resources/com/google/api/server/spi/discovery/foo_with_description_endpoint.json b/endpoints-framework/src/test/resources/com/google/api/server/spi/discovery/foo_with_description_endpoint.json index cf950c36..20099bb6 100644 --- a/endpoints-framework/src/test/resources/com/google/api/server/spi/discovery/foo_with_description_endpoint.json +++ b/endpoints-framework/src/test/resources/com/google/api/server/spi/discovery/foo_with_description_endpoint.json @@ -239,6 +239,7 @@ "type": "object" }, "FooDescription": { + "description": "Description at class level", "id": "FooDescription", "properties": { "choice": { @@ -246,11 +247,11 @@ "description": "description of choice" }, "name": { - "description":"description of name", + "description": "description of name", "type": "string" }, "value": { - "description":"description of value", + "description": "description of value", "format": "int32", "type": "integer" } @@ -258,6 +259,7 @@ "type": "object" }, "TestEnumDescription": { + "description": "A list of enum values", "enum": [ "VALUE1", "VALUE2" @@ -267,7 +269,7 @@ "description of value2" ], "id": "TestEnumDescription", - "type":"string" + "type": "string" } }, "servicePath": "foo/v1/", diff --git a/endpoints-framework/src/test/resources/com/google/api/server/spi/discovery/map_endpoint.json b/endpoints-framework/src/test/resources/com/google/api/server/spi/discovery/map_endpoint.json index d5795674..82bf8386 100644 --- a/endpoints-framework/src/test/resources/com/google/api/server/spi/discovery/map_endpoint.json +++ b/endpoints-framework/src/test/resources/com/google/api/server/spi/discovery/map_endpoint.json @@ -421,6 +421,7 @@ "additionalProperties": { "$ref": "Baz" }, + "description": "A collection of name / Baz pairs", "id": "Map_String_Baz", "type": "object" }, @@ -428,6 +429,7 @@ "additionalProperties": { "$ref": "Foo" }, + "description": "A collection of name / Foo pairs", "id": "Map_String_Foo", "type": "object" }, @@ -443,6 +445,7 @@ "additionalProperties": { "$ref": "Map_String_Foo" }, + "description": "A collection of name / Map_String_Foo pairs", "id": "Map_String_Map_String_Foo", "type": "object" }, diff --git a/endpoints-framework/src/test/resources/com/google/api/server/spi/discovery/map_endpoint_with_array.json b/endpoints-framework/src/test/resources/com/google/api/server/spi/discovery/map_endpoint_with_array.json index 9faa3ed1..ba780bd9 100644 --- a/endpoints-framework/src/test/resources/com/google/api/server/spi/discovery/map_endpoint_with_array.json +++ b/endpoints-framework/src/test/resources/com/google/api/server/spi/discovery/map_endpoint_with_array.json @@ -417,6 +417,7 @@ "additionalProperties": { "$ref": "Baz" }, + "description": "A collection of name / Baz pairs", "id": "Map_String_Baz", "type": "object" }, @@ -424,6 +425,7 @@ "additionalProperties": { "$ref": "Foo" }, + "description": "A collection of name / Foo pairs", "id": "Map_String_Foo", "type": "object" }, @@ -439,6 +441,7 @@ "additionalProperties": { "$ref": "Map_String_Foo" }, + "description": "A collection of name / Map_String_Foo pairs", "id": "Map_String_Map_String_Foo", "type": "object" }, diff --git a/endpoints-framework/src/test/resources/com/google/api/server/spi/discovery/optional_endpoint.json b/endpoints-framework/src/test/resources/com/google/api/server/spi/discovery/optional_endpoint.json new file mode 100644 index 00000000..046a58fb --- /dev/null +++ b/endpoints-framework/src/test/resources/com/google/api/server/spi/discovery/optional_endpoint.json @@ -0,0 +1,179 @@ +{ + "auth" : { + "oauth2" : { + "scopes" : { + "https://www.googleapis.com/auth/userinfo.email" : { + "description" : "View your email address" + } + } + } + }, + "basePath" : "/_ah/api/myapi/v1/", + "baseUrl" : "https://myapi.appspot.com/_ah/api/myapi/v1/", + "batchPath" : "batch", + "description" : "This is an API", + "discoveryVersion" : "v1", + "icons" : { + "x16" : "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", + "x32" : "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" + }, + "id" : "myapi:v1", + "kind" : "discovery#restDescription", + "name" : "myapi", + "parameters" : { + "alt" : { + "default" : "json", + "description" : "Data format for the response.", + "enum" : [ + "json" + ], + "enumDescriptions" : [ + "Responses with Content-Type of application/json" + ], + "location" : "query", + "type" : "string" + }, + "fields" : { + "description" : "Selector specifying which fields to include in a partial response.", + "location" : "query", + "type" : "string" + }, + "key" : { + "description" : "API key. Your API key identifies your project and provides you with API access, quota, and reports. Required unless you provide an OAuth 2.0 token.", + "location" : "query", + "type" : "string" + }, + "oauth_token" : { + "description" : "OAuth 2.0 token for the current user.", + "location" : "query", + "type" : "string" + }, + "prettyPrint" : { + "default" : "true", + "description" : "Returns response with indentations and line breaks.", + "location" : "query", + "type" : "boolean" + }, + "quotaUser" : { + "description" : "Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. Overrides userIp if both are provided.", + "location" : "query", + "type" : "string" + }, + "userIp" : { + "description" : "IP address of the site where the request originates. Use this if you want to enforce per-user limits.", + "location" : "query", + "type" : "string" + } + }, + "protocol" : "rest", + "resources" : { + "optionalEndpoint" : { + "methods" : { + "getResult" : { + "httpMethod" : "GET", + "id" : "myapi.optionalEndpoint.getResult", + "path" : "optionalresults", + "response" : { + "$ref" : "OptionalResults" + }, + "scopes" : [ + "https://www.googleapis.com/auth/userinfo.email" + ] + } + } + } + }, + "rootUrl" : "https://myapi.appspot.com/_ah/api/", + "schemas" : { + "Foo" : { + "id" : "Foo", + "properties" : { + "name" : { + "type" : "string" + }, + "value" : { + "format" : "int32", + "type" : "integer" + } + }, + "type" : "object" + }, + "OptionalResults" : { + "id" : "OptionalResults", + "properties" : { + "enums" : { + "items" : { + "$ref" : "TestEnum" + }, + "type" : "array" + }, + "foos" : { + "items" : { + "$ref" : "Foo" + }, + "type" : "array" + }, + "optionalDate" : { + "format" : "date-time", + "type" : "string" + }, + "optionalDouble" : { + "format" : "double", + "type" : "number" + }, + "optionalDoubleObject" : { + "format" : "double", + "type" : "number" + }, + "optionalEnum" : { + "$ref" : "TestEnum" + }, + "optionalFloatObject" : { + "format" : "float", + "type" : "number" + }, + "optionalFoo" : { + "$ref" : "Foo" + }, + "optionalInt" : { + "format" : "int32", + "type" : "integer" + }, + "optionalInteger" : { + "format" : "int32", + "type" : "integer" + }, + "optionalLong" : { + "format" : "int64", + "type" : "string" + }, + "optionalLongObject" : { + "format" : "int64", + "type" : "string" + }, + "optionalSimpleDate" : { + "format" : "date", + "type" : "string" + }, + "optionalString" : { + "type" : "string" + } + }, + "type" : "object" + }, + "TestEnum" : { + "enum" : [ + "VALUE1", + "value_2" + ], + "enumDescriptions" : [ + "", + "" + ], + "id" : "TestEnum", + "type" : "string" + } + }, + "servicePath" : "myapi/v1/", + "version" : "v1" +} \ No newline at end of file diff --git a/endpoints-framework/src/test/resources/com/google/api/server/spi/discovery/primitive_endpoint.json b/endpoints-framework/src/test/resources/com/google/api/server/spi/discovery/primitive_endpoint.json index e3ccc382..ffed8f9b 100644 --- a/endpoints-framework/src/test/resources/com/google/api/server/spi/discovery/primitive_endpoint.json +++ b/endpoints-framework/src/test/resources/com/google/api/server/spi/discovery/primitive_endpoint.json @@ -134,6 +134,7 @@ "type": "object" }, "PrimitiveBeanCollection": { + "description": "An ordered list of PrimitiveBean", "id": "PrimitiveBeanCollection", "properties": { "items": { diff --git a/endpoints-framework/src/test/resources/com/google/api/server/spi/discovery/required_parameters_endpoint.json b/endpoints-framework/src/test/resources/com/google/api/server/spi/discovery/required_parameters_endpoint.json new file mode 100644 index 00000000..574ba97f --- /dev/null +++ b/endpoints-framework/src/test/resources/com/google/api/server/spi/discovery/required_parameters_endpoint.json @@ -0,0 +1,132 @@ +{ + "auth": { + "oauth2": { + "scopes": { + "https://www.googleapis.com/auth/userinfo.email": { + "description": "View your email address" + } + } + } + }, + "basePath": "/api/requiredProperties/v1/", + "baseUrl": "https://discovery-test.appspot.com/api/requiredProperties/v1/", + "batchPath": "batch", + "description": "This is an API", + "discoveryVersion": "v1", + "icons": { + "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", + "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" + }, + "id": "requiredProperties:v1", + "kind": "discovery#restDescription", + "name": "requiredProperties", + "parameters": { + "alt": { + "default": "json", + "description": "Data format for the response.", + "enum": [ + "json" + ], + "enumDescriptions": [ + "Responses with Content-Type of application/json" + ], + "location": "query", + "type": "string" + }, + "fields": { + "description": "Selector specifying which fields to include in a partial response.", + "location": "query", + "type": "string" + }, + "key": { + "description": "API key. Your API key identifies your project and provides you with API access, quota, and reports. Required unless you provide an OAuth 2.0 token.", + "location": "query", + "type": "string" + }, + "oauth_token": { + "description": "OAuth 2.0 token for the current user.", + "location": "query", + "type": "string" + }, + "prettyPrint": { + "default": "true", + "description": "Returns response with indentations and line breaks.", + "location": "query", + "type": "boolean" + }, + "quotaUser": { + "description": "Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. Overrides userIp if both are provided.", + "location": "query", + "type": "string" + }, + "userIp": { + "description": "IP address of the site where the request originates. Use this if you want to enforce per-user limits.", + "location": "query", + "type": "string" + } + }, + "protocol": "rest", + "resources": { + "requiredPropertiesEndpoint": { + "methods": { + "getRequiredProperties": { + "httpMethod": "GET", + "id": "requiredProperties.requiredPropertiesEndpoint.getRequiredProperties", + "path": "requiredproperties", + "response": { + "$ref": "RequiredProperties" + }, + "scopes": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + } + } + } + }, + "rootUrl": "https://discovery-test.appspot.com/api/", + "schemas": { + "RequiredProperties": { + "id": "RequiredProperties", + "properties": { + "apiResourceProperty_not_required": { + "required": false, + "type": "string" + }, + "apiResourceProperty_required": { + "required": true, + "type": "string" + }, + "apiResourceProperty_undefined": { + "type": "string" + }, + "nonnull": { + "required": true, + "type": "string" + }, + "nullable": { + "required": false, + "type": "string" + }, + "priority1": { + "required": true, + "type": "string" + }, + "priority2": { + "required": true, + "type": "string" + }, + "priority3": { + "required": false, + "type": "string" + }, + "undefined": { + "type": "string" + } + }, + "type": "object" + } + }, + "servicePath": "requiredProperties/v1/", + "title": "API to test required properties", + "version": "v1" +} diff --git a/endpoints-framework/src/test/resources/com/google/api/server/spi/discovery/validation_endpoint.json b/endpoints-framework/src/test/resources/com/google/api/server/spi/discovery/validation_endpoint.json new file mode 100644 index 00000000..8ed37467 --- /dev/null +++ b/endpoints-framework/src/test/resources/com/google/api/server/spi/discovery/validation_endpoint.json @@ -0,0 +1,165 @@ +{ + "auth": { + "oauth2": { + "scopes": { + "https://www.googleapis.com/auth/userinfo.email": { + "description": "View your email address" + } + } + } + }, + "basePath": "/_ah/api/validation/v1/", + "baseUrl": "https://myapi.appspot.com/_ah/api/validation/v1/", + "batchPath": "batch", + "description": "This is an API", + "discoveryVersion": "v1", + "icons": { + "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", + "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" + }, + "id": "validation:v1", + "kind": "discovery#restDescription", + "methods": { + "create": { + "httpMethod": "POST", + "id": "validation.create", + "parameterOrder": [ + "pathParam", + "arraySizeParam", + "decimalMinMaxParam", + "minMaxParam", + "queryParam", + "sizeParam" + ], + "parameters": { + "pathParam": { + "location": "path", + "required": true, + "type": "string", + "pattern": "^\\d+$" + }, + "queryParam": { + "location": "query", + "required": true, + "type": "string", + "pattern": "^[a-z]{2}$" + }, + "minMaxParam": { + "location": "query", + "required": true, + "type": "string", + "format" : "int64", + "minimum": "10", + "maximum": "20" + }, + "decimalMinMaxParam": { + "location": "query", + "required": true, + "type": "number", + "format": "double", + "minimum": "2.3", + "maximum": "4" + }, + "sizeParam": { + "location": "query", + "required": true, + "type": "string" + }, + "arraySizeParam" : { + "location" : "query", + "repeated" : true, + "required" : true, + "type" : "string" + } + }, + "path": "{pathParam}", + "request" : { + "$ref" : "ValidationBean", + "parameterName" : "resource" + }, + "scopes": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + } + }, + "name": "validation", + "parameters": { + "alt": { + "default": "json", + "description": "Data format for the response.", + "enum": [ + "json" + ], + "enumDescriptions": [ + "Responses with Content-Type of application/json" + ], + "location": "query", + "type": "string" + }, + "fields": { + "description": "Selector specifying which fields to include in a partial response.", + "location": "query", + "type": "string" + }, + "key": { + "description": "API key. Your API key identifies your project and provides you with API access, quota, and reports. Required unless you provide an OAuth 2.0 token.", + "location": "query", + "type": "string" + }, + "oauth_token": { + "description": "OAuth 2.0 token for the current user.", + "location": "query", + "type": "string" + }, + "prettyPrint": { + "default": "true", + "description": "Returns response with indentations and line breaks.", + "location": "query", + "type": "boolean" + }, + "quotaUser": { + "description": "Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. Overrides userIp if both are provided.", + "location": "query", + "type": "string" + }, + "userIp": { + "description": "IP address of the site where the request originates. Use this if you want to enforce per-user limits.", + "location": "query", + "type": "string" + } + }, + "protocol": "rest", + "rootUrl": "https://myapi.appspot.com/_ah/api/", + "schemas" : { + "ValidationBean" : { + "id" : "ValidationBean", + "properties" : { + "arrayTest" : { + "items" : { + "type" : "string" + }, + "type" : "array" + }, + "decimalMinMaxTest" : { + "format" : "double", + "type" : "number", + "minimum": "3.4", + "maximum": "4.5" + }, + "minMaxTest" : { + "format" : "int64", + "type" : "string", + "minimum": "2", + "maximum": "6" + }, + "myPatternTest" : { + "type" : "string", + "pattern": "^[0-9]{2}$" + } + }, + "type" : "object" + } + }, + "servicePath": "validation/v1/", + "version": "v1" +} diff --git a/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/absolute_common_path_endpoint.swagger b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/absolute_common_path_endpoint.swagger index 075c602a..f437f30c 100644 --- a/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/absolute_common_path_endpoint.swagger +++ b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/absolute_common_path_endpoint.swagger @@ -6,6 +6,11 @@ }, "host": "myapi.appspot.com", "basePath": "/_ah/api", + "tags": [ + { + "name": "absolutepath:v1" + } + ], "schemes": [ "https" ], @@ -18,10 +23,12 @@ "paths": { "/absolutepath/v1/absolutepathmethod": { "post": { - "operationId": "AbsolutepathV1AbsolutePath", - "parameters": [], + "tags": [ + "absolutepath:v1" + ], + "operationId": "absolutepath:v1.absolutePath", "responses": { - "200": { + "204": { "description": "A successful response" } } @@ -29,11 +36,13 @@ }, "/absolutepath/v1/create": { "post": { - "operationId": "AbsolutepathV1CreateFoo", - "parameters": [], + "tags": [ + "absolutepath:v1" + ], + "operationId": "absolutepath:v1.createFoo", "responses": { "200": { - "description": "A successful response", + "description": "A Foo response", "schema": { "$ref": "#/definitions/Foo" } diff --git a/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/absolute_path_endpoint.swagger b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/absolute_path_endpoint.swagger index 53b6bf50..e3d26515 100644 --- a/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/absolute_path_endpoint.swagger +++ b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/absolute_path_endpoint.swagger @@ -6,6 +6,11 @@ }, "host": "myapi.appspot.com", "basePath": "/_ah/api", + "tags": [ + { + "name": "absolutepath:v1" + } + ], "schemes": [ "https" ], @@ -18,11 +23,13 @@ "paths": { "/absolutepath/v1/create": { "post": { - "operationId": "AbsolutepathV1CreateFoo", - "parameters": [], + "tags": [ + "absolutepath:v1" + ], + "operationId": "absolutepath:v1.createFoo", "responses": { "200": { - "description": "A successful response", + "description": "A Foo response", "schema": { "$ref": "#/definitions/Foo" } @@ -32,10 +39,12 @@ }, "/absolutepathmethod/v1": { "post": { - "operationId": "AbsolutepathV1AbsolutePath", - "parameters": [], + "tags": [ + "absolutepath:v1" + ], + "operationId": "absolutepath:v1.absolutePath", "responses": { - "200": { + "204": { "description": "A successful response" } } @@ -43,10 +52,12 @@ }, "/absolutepathmethod2/v1": { "post": { - "operationId": "AbsolutepathV1AbsolutePath2", - "parameters": [], + "tags": [ + "absolutepath:v1" + ], + "operationId": "absolutepath:v1.absolutePath2", "responses": { - "200": { + "204": { "description": "A successful response" } } diff --git a/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/api_keys.swagger b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/api_keys.swagger index 44143838..cfafec92 100644 --- a/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/api_keys.swagger +++ b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/api_keys.swagger @@ -6,6 +6,11 @@ }, "host": "swagger-test.appspot.com", "basePath": "/api", + "tags": [ + { + "name": "apikeys:v1" + } + ], "schemes": [ "https" ], @@ -18,10 +23,12 @@ "paths": { "/apikeys/v1/apiKeyWithAuth": { "post": { - "operationId": "ApikeysV1ApiKeyWithAuth", - "parameters": [], + "tags": [ + "apikeys:v1" + ], + "operationId": "apikeys:v1.apiKeyWithAuth", "responses": { - "200": { + "204": { "description": "A successful response" } }, @@ -35,10 +42,12 @@ }, "/apikeys/v1/inheritApiKeySetting": { "post": { - "operationId": "ApikeysV1InheritApiKeySetting", - "parameters": [], + "tags": [ + "apikeys:v1" + ], + "operationId": "apikeys:v1.inheritApiKeySetting", "responses": { - "200": { + "204": { "description": "A successful response" } }, @@ -51,10 +60,12 @@ }, "/apikeys/v1/overrideApiKeySetting": { "post": { - "operationId": "ApikeysV1OverrideApiKeySetting", - "parameters": [], + "tags": [ + "apikeys:v1" + ], + "operationId": "apikeys:v1.overrideApiKeySetting", "responses": { - "200": { + "204": { "description": "A successful response" } } @@ -62,6 +73,11 @@ } }, "securityDefinitions": { + "api_key": { + "type": "apiKey", + "name": "key", + "in": "query" + }, "auth0-6fa4a909": { "type": "oauth2", "authorizationUrl": "", @@ -69,11 +85,6 @@ "x-google-issuer": "https://test.auth0.com/authorize", "x-google-jwks_uri": "https://test.auth0.com/.wellknown/jwks.json", "x-google-audiences": "auth0audmethod" - }, - "api_key": { - "type": "apiKey", - "name": "key", - "in": "query" } } } diff --git a/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/array_endpoint.swagger b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/array_endpoint.swagger index b2efe3c3..6df49121 100644 --- a/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/array_endpoint.swagger +++ b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/array_endpoint.swagger @@ -6,6 +6,11 @@ }, "host": "myapi.appspot.com", "basePath": "/_ah/api", + "tags": [ + { + "name": "myapi:v1" + } + ], "schemes": [ "https" ], @@ -18,11 +23,13 @@ "paths": { "/myapi/v1/arrayendpoint": { "get": { - "operationId": "MyapiV1GetArrayService", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getArrayService", "responses": { "200": { - "description": "A successful response", + "description": "A ArrayEndpoint response", "schema": { "$ref": "#/definitions/ArrayEndpoint" } @@ -32,11 +39,13 @@ }, "/myapi/v1/baz": { "get": { - "operationId": "MyapiV1GetBaz", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getBaz", "responses": { "200": { - "description": "A successful response", + "description": "A Baz response", "schema": { "$ref": "#/definitions/Baz" } @@ -46,11 +55,13 @@ }, "/myapi/v1/collectionresponse_foo": { "get": { - "operationId": "MyapiV1GetFoosResponse", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getFoosResponse", "responses": { "200": { - "description": "A successful response", + "description": "A CollectionResponse_Foo response", "schema": { "$ref": "#/definitions/CollectionResponse_Foo" } @@ -60,11 +71,13 @@ }, "/myapi/v1/foocollection": { "get": { - "operationId": "MyapiV1GetFoos", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getFoos", "responses": { "200": { - "description": "A successful response", + "description": "A FooCollection response", "schema": { "$ref": "#/definitions/FooCollection" } @@ -74,11 +87,13 @@ }, "/myapi/v1/foocollectioncollection": { "get": { - "operationId": "MyapiV1GetAllArrayedFoos", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getAllArrayedFoos", "responses": { "200": { - "description": "A successful response", + "description": "A FooCollectionCollection response", "schema": { "$ref": "#/definitions/FooCollectionCollection" } @@ -88,11 +103,13 @@ }, "/myapi/v1/getAllFoos": { "get": { - "operationId": "MyapiV1GetAllFoos", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getAllFoos", "responses": { "200": { - "description": "A successful response", + "description": "A FooCollectionCollection response", "schema": { "$ref": "#/definitions/FooCollectionCollection" } @@ -102,11 +119,13 @@ }, "/myapi/v1/getAllFoosResponse": { "get": { - "operationId": "MyapiV1GetAllFoosResponse", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getAllFoosResponse", "responses": { "200": { - "description": "A successful response", + "description": "A CollectionResponse_FooCollection response", "schema": { "$ref": "#/definitions/CollectionResponse_FooCollection" } @@ -116,11 +135,13 @@ }, "/myapi/v1/getAllNestedFoosResponse": { "get": { - "operationId": "MyapiV1GetAllNestedFoosResponse", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getAllNestedFoosResponse", "responses": { "200": { - "description": "A successful response", + "description": "A CollectionResponse_FooCollectionCollection response", "schema": { "$ref": "#/definitions/CollectionResponse_FooCollectionCollection" } @@ -130,11 +151,13 @@ }, "/myapi/v1/getArrayedFoos": { "get": { - "operationId": "MyapiV1GetArrayedFoos", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getArrayedFoos", "responses": { "200": { - "description": "A successful response", + "description": "A FooCollection response", "schema": { "$ref": "#/definitions/FooCollection" } @@ -144,11 +167,13 @@ }, "/myapi/v1/getIntegersResponse": { "get": { - "operationId": "MyapiV1GetIntegersResponse", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getIntegersResponse", "responses": { "200": { - "description": "A successful response", + "description": "A CollectionResponse_Integer response", "schema": { "$ref": "#/definitions/CollectionResponse_Integer" } @@ -158,11 +183,13 @@ }, "/myapi/v1/getListOfString": { "get": { - "operationId": "MyapiV1GetListOfString", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getListOfString", "responses": { "200": { - "description": "A successful response", + "description": "A ListContainer response", "schema": { "$ref": "#/definitions/ListContainer" } @@ -172,11 +199,13 @@ }, "/myapi/v1/getObjectIntegers": { "get": { - "operationId": "MyapiV1GetObjectIntegers", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getObjectIntegers", "responses": { "200": { - "description": "A successful response", + "description": "A IntegerCollection response", "schema": { "$ref": "#/definitions/IntegerCollection" } @@ -186,129 +215,323 @@ }, "/myapi/v1/integercollection": { "get": { - "operationId": "MyapiV1GetIntegers", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getIntegers", "responses": { "200": { - "description": "A successful response", + "description": "A IntegerCollection response", "schema": { "$ref": "#/definitions/IntegerCollection" } } } } - } - }, - "definitions": { - "CollectionResponse_Integer": { - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "type": "integer", - "format": "int32" + }, + "/myapi/v1/setListOfBooleans/{list}/{array}": { + "post": { + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.setListOfBooleans", + "parameters": [ + { + "name": "list", + "in": "path", + "required": true, + "type": "array", + "items": { + "type": "boolean" + }, + "collectionFormat": "csv" + }, + { + "name": "array", + "in": "path", + "required": true, + "type": "array", + "items": { + "type": "boolean" + }, + "collectionFormat": "csv" + } + ], + "responses": { + "204": { + "description": "A successful response" } - }, - "nextPageToken": { - "type": "string" } } }, - "CollectionResponse_Foo": { - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "$ref": "#/definitions/Foo" + "/myapi/v1/setListOfByteArrays/{list}/{array}": { + "post": { + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.setListOfByteArrays", + "parameters": [ + { + "name": "list", + "in": "path", + "required": true, + "type": "array", + "items": { + "type": "string", + "format": "byte", + "pattern": "^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$" + }, + "collectionFormat": "csv" + }, + { + "name": "array", + "in": "path", + "required": true, + "type": "array", + "items": { + "type": "string", + "format": "byte", + "pattern": "^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$" + }, + "collectionFormat": "csv" + } + ], + "responses": { + "204": { + "description": "A successful response" } - }, - "nextPageToken": { - "type": "string" } } }, - "Foo": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "value": { - "type": "integer", - "format": "int32" + "/myapi/v1/setListOfDoubles/{list}/{array}": { + "post": { + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.setListOfDoubles", + "parameters": [ + { + "name": "list", + "in": "path", + "required": true, + "type": "array", + "items": { + "type": "number", + "format": "double" + }, + "collectionFormat": "csv" + }, + { + "name": "array", + "in": "path", + "required": true, + "type": "array", + "items": { + "type": "number", + "format": "double" + }, + "collectionFormat": "csv" + } + ], + "responses": { + "204": { + "description": "A successful response" + } } } }, - "FooCollectionCollection": { - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { + "/myapi/v1/setListOfEnums/{list}": { + "post": { + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.setListOfEnums", + "parameters": [ + { + "name": "list", + "in": "path", + "required": true, "type": "array", "items": { - "$ref": "#/definitions/Foo" - } + "type": "string", + "enum": [ + "VALUE1", + "value_2" + ] + }, + "collectionFormat": "csv" + } + ], + "responses": { + "204": { + "description": "A successful response" } } } }, - "Baz": { - "type": "object", - "properties": { - "foo": { - "$ref": "#/definitions/Foo" - }, - "foos": { - "type": "array", - "items": { - "$ref": "#/definitions/Foo" + "/myapi/v1/setListOfFloats/{list}/{array}": { + "post": { + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.setListOfFloats", + "parameters": [ + { + "name": "list", + "in": "path", + "required": true, + "type": "array", + "items": { + "type": "number", + "format": "float" + }, + "collectionFormat": "csv" + }, + { + "name": "array", + "in": "path", + "required": true, + "type": "array", + "items": { + "type": "number", + "format": "float" + }, + "collectionFormat": "csv" + } + ], + "responses": { + "204": { + "description": "A successful response" } } } }, - "ListContainer": { - "type": "object", - "properties": { - "strings": { - "type": "array", - "items": { - "type": "string" + "/myapi/v1/setListOfIntegers/{list}/{array}": { + "post": { + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.setListOfIntegers", + "parameters": [ + { + "name": "list", + "in": "path", + "required": true, + "type": "array", + "items": { + "type": "integer", + "format": "int32" + }, + "collectionFormat": "csv" + }, + { + "name": "array", + "in": "path", + "required": true, + "type": "array", + "items": { + "type": "integer", + "format": "int32" + }, + "collectionFormat": "csv" + } + ], + "responses": { + "204": { + "description": "A successful response" } } } }, - "IntegerCollection": { - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "type": "integer", - "format": "int32" + "/myapi/v1/setListOfLongs/{list}/{array}": { + "post": { + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.setListOfLongs", + "parameters": [ + { + "name": "list", + "in": "path", + "required": true, + "type": "array", + "items": { + "type": "integer", + "format": "int64" + }, + "collectionFormat": "csv" + }, + { + "name": "array", + "in": "path", + "required": true, + "type": "array", + "items": { + "type": "integer", + "format": "int64" + }, + "collectionFormat": "csv" + } + ], + "responses": { + "204": { + "description": "A successful response" } } } }, - "CollectionResponse_FooCollection": { - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { + "/myapi/v1/setListOfString/{list}": { + "post": { + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.setListOfString", + "parameters": [ + { + "name": "list", + "in": "path", + "required": true, "type": "array", "items": { - "$ref": "#/definitions/Foo" - } + "type": "string" + }, + "collectionFormat": "csv" + } + ], + "responses": { + "204": { + "description": "A successful response" } - }, - "nextPageToken": { - "type": "string" } } }, + "/myapi/v1/setListOfStringAsQueryParam": { + "post": { + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.setListOfStringAsQueryParam", + "parameters": [ + { + "name": "list", + "in": "query", + "required": false, + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi" + } + ], + "responses": { + "204": { + "description": "A successful response" + } + } + } + } + }, + "definitions": { "ArrayEndpoint": { "type": "object", "properties": { @@ -367,9 +590,25 @@ "integersResponse": { "$ref": "#/definitions/CollectionResponse_Integer" }, + "listOfEnums": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "VALUE1", + "value_2" + ] + } + }, "listOfString": { "$ref": "#/definitions/ListContainer" }, + "listOfStringAsQueryParam": { + "type": "array", + "items": { + "type": "string" + } + }, "objectIntegers": { "type": "array", "items": { @@ -379,7 +618,21 @@ } } }, - "FooCollection": { + "Baz": { + "type": "object", + "properties": { + "foo": { + "$ref": "#/definitions/Foo" + }, + "foos": { + "type": "array", + "items": { + "$ref": "#/definitions/Foo" + } + } + } + }, + "CollectionResponse_Foo": { "type": "object", "properties": { "items": { @@ -387,6 +640,26 @@ "items": { "$ref": "#/definitions/Foo" } + }, + "nextPageToken": { + "type": "string" + } + } + }, + "CollectionResponse_FooCollection": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/Foo" + } + } + }, + "nextPageToken": { + "type": "string" } } }, @@ -409,6 +682,82 @@ "type": "string" } } + }, + "CollectionResponse_Integer": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + }, + "nextPageToken": { + "type": "string" + } + } + }, + "Foo": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "integer", + "format": "int32" + } + } + }, + "FooCollection": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/Foo" + } + } + }, + "description": "An ordered list of Foo" + }, + "FooCollectionCollection": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/Foo" + } + } + } + } + }, + "IntegerCollection": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + } + } + }, + "ListContainer": { + "type": "object", + "properties": { + "strings": { + "type": "array", + "items": { + "type": "string" + } + } + } } } } diff --git a/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/enum_endpoint.swagger b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/enum_endpoint.swagger index 98bf9c95..f828a6cc 100644 --- a/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/enum_endpoint.swagger +++ b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/enum_endpoint.swagger @@ -6,6 +6,11 @@ }, "host": "myapi.appspot.com", "basePath": "/_ah/api", + "tags": [ + { + "name": "enum:v1" + } + ], "schemes": [ "https" ], @@ -18,7 +23,10 @@ "paths": { "/enum/v1/{value}": { "post": { - "operationId": "EnumV1Create", + "tags": [ + "enum:v1" + ], + "operationId": "enum:v1.create", "parameters": [ { "name": "value", @@ -27,13 +35,13 @@ "type": "string", "enum": [ "VALUE1", - "VALUE2" + "value_2" ] } ], "responses": { "200": { - "description": "A successful response", + "description": "A EnumValue response", "schema": { "$ref": "#/definitions/EnumValue" } @@ -50,7 +58,7 @@ "type": "string", "enum": [ "VALUE1", - "VALUE2" + "value_2" ] } } diff --git a/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/error_codes_all.swagger b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/error_codes_all.swagger new file mode 100644 index 00000000..a79f7ad2 --- /dev/null +++ b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/error_codes_all.swagger @@ -0,0 +1,219 @@ +{ + "swagger": "2.0", + "info": { + "version": "1.0.0", + "title": "swagger-test.appspot.com" + }, + "host": "swagger-test.appspot.com", + "basePath": "/api", + "tags": [ + { + "name": "exceptions:v1" + } + ], + "schemes": [ + "https" + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "paths": { + "/exceptions/v1/doesNotThrow": { + "post": { + "tags": [ + "exceptions:v1" + ], + "operationId": "exceptions:v1.doesNotThrow", + "responses": { + "204": { + "description": "A successful response" + }, + "default": { + "$ref": "#/responses/DefaultError" + } + } + } + }, + "/exceptions/v1/throwsMultipleExceptions": { + "post": { + "tags": [ + "exceptions:v1" + ], + "operationId": "exceptions:v1.throwsMultipleExceptions", + "responses": { + "204": { + "description": "A successful response" + }, + "400": { + "$ref": "#/responses/BadRequest" + }, + "409": { + "$ref": "#/responses/Conflict" + }, + "default": { + "$ref": "#/responses/DefaultError" + } + } + } + }, + "/exceptions/v1/throwsNotFoundException": { + "post": { + "tags": [ + "exceptions:v1" + ], + "operationId": "exceptions:v1.throwsNotFoundException", + "responses": { + "204": { + "description": "A successful response" + }, + "404": { + "$ref": "#/responses/NotFound" + }, + "default": { + "$ref": "#/responses/DefaultError" + } + } + } + }, + "/exceptions/v1/throwsServiceException": { + "post": { + "tags": [ + "exceptions:v1" + ], + "operationId": "exceptions:v1.throwsServiceException", + "responses": { + "204": { + "description": "A successful response" + }, + "default": { + "$ref": "#/responses/DefaultError" + } + } + } + }, + "/exceptions/v1/throwsUnknownException": { + "post": { + "tags": [ + "exceptions:v1" + ], + "operationId": "exceptions:v1.throwsUnknownException", + "responses": { + "204": { + "description": "A successful response" + }, + "default": { + "$ref": "#/responses/DefaultError" + } + } + } + } + }, + "definitions": { + "Details": { + "type": "object", + "properties": { + "detail": { + "type": "string" + }, + "parameterViolations": { + "type": "array", + "items": { + "$ref": "#/definitions/ParameterViolations" + } + }, + "reason": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "GoogleJsonErrorContainer": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "details": { + "type": "array", + "items": { + "$ref": "#/definitions/Details" + } + }, + "errors": { + "type": "array", + "items": { + "type": "object", + "properties": { + "domain": { + "type": "string" + }, + "location": { + "type": "string" + }, + "locationType": { + "type": "string" + }, + "message": { + "type": "string" + }, + "reason": { + "type": "string" + } + } + } + }, + "message": { + "type": "string" + } + } + } + } + }, + "ParameterViolations": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "parameter": { + "type": "string" + } + } + } + }, + "responses": { + "BadRequest": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/GoogleJsonErrorContainer" + } + }, + "Conflict": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/GoogleJsonErrorContainer" + } + }, + "DefaultError": { + "description": "A failed response", + "schema": { + "$ref": "#/definitions/GoogleJsonErrorContainer" + } + }, + "NotFound": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/GoogleJsonErrorContainer" + } + } + } +} diff --git a/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/error_codes_default_response.swagger b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/error_codes_default_response.swagger new file mode 100644 index 00000000..9254e80e --- /dev/null +++ b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/error_codes_default_response.swagger @@ -0,0 +1,192 @@ +{ + "swagger": "2.0", + "info": { + "version": "1.0.0", + "title": "swagger-test.appspot.com" + }, + "host": "swagger-test.appspot.com", + "basePath": "/api", + "tags": [ + { + "name": "exceptions:v1" + } + ], + "schemes": [ + "https" + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "paths": { + "/exceptions/v1/doesNotThrow": { + "post": { + "tags": [ + "exceptions:v1" + ], + "operationId": "exceptions:v1.doesNotThrow", + "responses": { + "204": { + "description": "A successful response" + }, + "default": { + "$ref": "#/responses/DefaultError" + } + } + } + }, + "/exceptions/v1/throwsMultipleExceptions": { + "post": { + "tags": [ + "exceptions:v1" + ], + "operationId": "exceptions:v1.throwsMultipleExceptions", + "responses": { + "204": { + "description": "A successful response" + }, + "default": { + "$ref": "#/responses/DefaultError" + } + } + } + }, + "/exceptions/v1/throwsNotFoundException": { + "post": { + "tags": [ + "exceptions:v1" + ], + "operationId": "exceptions:v1.throwsNotFoundException", + "responses": { + "204": { + "description": "A successful response" + }, + "default": { + "$ref": "#/responses/DefaultError" + } + } + } + }, + "/exceptions/v1/throwsServiceException": { + "post": { + "tags": [ + "exceptions:v1" + ], + "operationId": "exceptions:v1.throwsServiceException", + "responses": { + "204": { + "description": "A successful response" + }, + "default": { + "$ref": "#/responses/DefaultError" + } + } + } + }, + "/exceptions/v1/throwsUnknownException": { + "post": { + "tags": [ + "exceptions:v1" + ], + "operationId": "exceptions:v1.throwsUnknownException", + "responses": { + "204": { + "description": "A successful response" + }, + "default": { + "$ref": "#/responses/DefaultError" + } + } + } + } + }, + "definitions": { + "Details": { + "type": "object", + "properties": { + "detail": { + "type": "string" + }, + "parameterViolations": { + "type": "array", + "items": { + "$ref": "#/definitions/ParameterViolations" + } + }, + "reason": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "GoogleJsonErrorContainer": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "details": { + "type": "array", + "items": { + "$ref": "#/definitions/Details" + } + }, + "errors": { + "type": "array", + "items": { + "type": "object", + "properties": { + "domain": { + "type": "string" + }, + "location": { + "type": "string" + }, + "locationType": { + "type": "string" + }, + "message": { + "type": "string" + }, + "reason": { + "type": "string" + } + } + } + }, + "message": { + "type": "string" + } + } + } + } + }, + "ParameterViolations": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "parameter": { + "type": "string" + } + } + } + }, + "responses": { + "DefaultError": { + "description": "A failed response", + "schema": { + "$ref": "#/definitions/GoogleJsonErrorContainer" + } + } + } +} diff --git a/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/error_codes_service_exceptions.swagger b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/error_codes_service_exceptions.swagger new file mode 100644 index 00000000..f2032473 --- /dev/null +++ b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/error_codes_service_exceptions.swagger @@ -0,0 +1,198 @@ +{ + "swagger": "2.0", + "info": { + "version": "1.0.0", + "title": "swagger-test.appspot.com" + }, + "host": "swagger-test.appspot.com", + "basePath": "/api", + "tags": [ + { + "name": "exceptions:v1" + } + ], + "schemes": [ + "https" + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "paths": { + "/exceptions/v1/doesNotThrow": { + "post": { + "tags": [ + "exceptions:v1" + ], + "operationId": "exceptions:v1.doesNotThrow", + "responses": { + "204": { + "description": "A successful response" + } + } + } + }, + "/exceptions/v1/throwsMultipleExceptions": { + "post": { + "tags": [ + "exceptions:v1" + ], + "operationId": "exceptions:v1.throwsMultipleExceptions", + "responses": { + "204": { + "description": "A successful response" + }, + "400": { + "$ref": "#/responses/BadRequest" + }, + "409": { + "$ref": "#/responses/Conflict" + } + } + } + }, + "/exceptions/v1/throwsNotFoundException": { + "post": { + "tags": [ + "exceptions:v1" + ], + "operationId": "exceptions:v1.throwsNotFoundException", + "responses": { + "204": { + "description": "A successful response" + }, + "404": { + "$ref": "#/responses/NotFound" + } + } + } + }, + "/exceptions/v1/throwsServiceException": { + "post": { + "tags": [ + "exceptions:v1" + ], + "operationId": "exceptions:v1.throwsServiceException", + "responses": { + "204": { + "description": "A successful response" + } + } + } + }, + "/exceptions/v1/throwsUnknownException": { + "post": { + "tags": [ + "exceptions:v1" + ], + "operationId": "exceptions:v1.throwsUnknownException", + "responses": { + "204": { + "description": "A successful response" + } + } + } + } + }, + "definitions": { + "Details": { + "type": "object", + "properties": { + "detail": { + "type": "string" + }, + "parameterViolations": { + "type": "array", + "items": { + "$ref": "#/definitions/ParameterViolations" + } + }, + "reason": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "GoogleJsonErrorContainer": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "details": { + "type": "array", + "items": { + "$ref": "#/definitions/Details" + } + }, + "errors": { + "type": "array", + "items": { + "type": "object", + "properties": { + "domain": { + "type": "string" + }, + "location": { + "type": "string" + }, + "locationType": { + "type": "string" + }, + "message": { + "type": "string" + }, + "reason": { + "type": "string" + } + } + } + }, + "message": { + "type": "string" + } + } + } + } + }, + "ParameterViolations": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "parameter": { + "type": "string" + } + } + } + }, + "responses": { + "BadRequest": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/GoogleJsonErrorContainer" + } + }, + "Conflict": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/GoogleJsonErrorContainer" + } + }, + "NotFound": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/GoogleJsonErrorContainer" + } + } + } +} diff --git a/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/foo_endpoint.swagger b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/foo_endpoint.swagger index 4d6a053e..12ea29f8 100644 --- a/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/foo_endpoint.swagger +++ b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/foo_endpoint.swagger @@ -6,6 +6,15 @@ }, "host": "swagger-test.appspot.com", "basePath": "/api", + "tags": [ + { + "name": "foo:v1", + "description": "Just Foo Things", + "externalDocs": { + "url": "https://example.com" + } + } + ], "schemes": [ "https" ], @@ -18,8 +27,11 @@ "paths": { "/foo/v1/foos": { "get": { + "tags": [ + "foo:v1" + ], "description": "list desc", - "operationId": "FooV1ListFoos", + "operationId": "foo:v1.listFoos", "parameters": [ { "name": "n", @@ -31,7 +43,7 @@ ], "responses": { "200": { - "description": "A successful response", + "description": "A CollectionResponse_Foo response", "schema": { "$ref": "#/definitions/CollectionResponse_Foo" } @@ -39,19 +51,25 @@ }, "security": [ { - "google_id_token-3a26ea04": [] + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] }, { - "google_id_token_https-3a26ea04": [] + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] } ] }, "post": { - "operationId": "FooV1Toplevel", - "parameters": [], + "tags": [ + "foo:v1" + ], + "operationId": "foo:v1.toplevel", "responses": { "200": { - "description": "A successful response", + "description": "A CollectionResponse_Foo response", "schema": { "$ref": "#/definitions/CollectionResponse_Foo" } @@ -59,18 +77,25 @@ }, "security": [ { - "google_id_token-3a26ea04": [] + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] }, { - "google_id_token_https-3a26ea04": [] + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] } ] } }, "/foo/v1/foos/{id}": { "get": { + "tags": [ + "foo:v1" + ], "description": "get desc", - "operationId": "FooV1GetFoo", + "operationId": "foo:v1.getFoo", "parameters": [ { "name": "id", @@ -82,7 +107,7 @@ ], "responses": { "200": { - "description": "A successful response", + "description": "A Foo response", "schema": { "$ref": "#/definitions/Foo" } @@ -90,16 +115,23 @@ }, "security": [ { - "google_id_token-3a26ea04": [] + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] }, { - "google_id_token_https-3a26ea04": [] + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] } ] }, "post": { + "tags": [ + "foo:v1" + ], "description": "update desc", - "operationId": "FooV1UpdateFoo", + "operationId": "foo:v1.updateFoo", "parameters": [ { "name": "id", @@ -110,8 +142,8 @@ }, { "in": "body", - "name": "body", - "required": false, + "name": "Foo", + "required": true, "schema": { "$ref": "#/definitions/Foo" } @@ -119,7 +151,7 @@ ], "responses": { "200": { - "description": "A successful response", + "description": "A Foo response", "schema": { "$ref": "#/definitions/Foo" } @@ -127,16 +159,23 @@ }, "security": [ { - "google_id_token-3a26ea04": [] + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] }, { - "google_id_token_https-3a26ea04": [] + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] } ] }, "put": { + "tags": [ + "foo:v1" + ], "description": "create desc", - "operationId": "FooV1CreateFoo", + "operationId": "foo:v1.createFoo", "parameters": [ { "name": "id", @@ -147,8 +186,8 @@ }, { "in": "body", - "name": "body", - "required": false, + "name": "Foo", + "required": true, "schema": { "$ref": "#/definitions/Foo" } @@ -156,7 +195,7 @@ ], "responses": { "200": { - "description": "A successful response", + "description": "A Foo response", "schema": { "$ref": "#/definitions/Foo" } @@ -164,16 +203,23 @@ }, "security": [ { - "google_id_token-3a26ea04": [] + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] }, { - "google_id_token_https-3a26ea04": [] + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] } ] }, "delete": { + "tags": [ + "foo:v1" + ], "description": "delete desc", - "operationId": "FooV1DeleteFoo", + "operationId": "foo:v1.deleteFoo", "parameters": [ { "name": "id", @@ -185,7 +231,7 @@ ], "responses": { "200": { - "description": "A successful response", + "description": "A Foo response", "schema": { "$ref": "#/definitions/Foo" } @@ -193,10 +239,14 @@ }, "security": [ { - "google_id_token-3a26ea04": [] + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] }, { - "google_id_token_https-3a26ea04": [] + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] } ] } @@ -205,34 +255,28 @@ "securityDefinitions": { "google_id_token-3a26ea04": { "type": "oauth2", - "authorizationUrl": "", + "authorizationUrl": "https://accounts.google.com/o/oauth2/v2/auth", "flow": "implicit", - "x-google-issuer": "accounts.google.com", + "scopes": { + "https://www.googleapis.com/auth/userinfo.email": "View your email address" + }, + "x-google-issuer": "https://accounts.google.com", "x-google-jwks_uri": "https://www.googleapis.com/oauth2/v1/certs", "x-google-audiences": "audience" }, - "google_id_token_https-3a26ea04": { + "google_id_token_legacy-3a26ea04": { "type": "oauth2", - "authorizationUrl": "", + "authorizationUrl": "https://accounts.google.com/o/oauth2/v2/auth", "flow": "implicit", - "x-google-issuer": "https://accounts.google.com", + "scopes": { + "https://www.googleapis.com/auth/userinfo.email": "View your email address" + }, + "x-google-issuer": "accounts.google.com", "x-google-jwks_uri": "https://www.googleapis.com/oauth2/v1/certs", "x-google-audiences": "audience" } }, "definitions": { - "Foo": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "value": { - "type": "integer", - "format": "int32" - } - } - }, "CollectionResponse_Foo": { "type": "object", "properties": { @@ -246,6 +290,18 @@ "type": "string" } } + }, + "Foo": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "integer", + "format": "int32" + } + } } } } diff --git a/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/foo_endpoint_localhost.swagger b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/foo_endpoint_api_name.swagger similarity index 57% rename from endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/foo_endpoint_localhost.swagger rename to endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/foo_endpoint_api_name.swagger index f91532dd..300d057c 100644 --- a/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/foo_endpoint_localhost.swagger +++ b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/foo_endpoint_api_name.swagger @@ -2,12 +2,21 @@ "swagger": "2.0", "info": { "version": "1.0.0", - "title": "localhost:8080" + "title": "myapi.appspot.com" }, - "host": "localhost:8080", - "basePath": "/api", + "host": "myapi.appspot.com", + "basePath": "/_ah/api", + "tags": [ + { + "name": "foo:v1", + "description": "Just Foo Things", + "externalDocs": { + "url": "https://example.com" + } + } + ], "schemes": [ - "http" + "https" ], "consumes": [ "application/json" @@ -18,8 +27,11 @@ "paths": { "/foo/v1/foos": { "get": { + "tags": [ + "foo:v1" + ], "description": "list desc", - "operationId": "FooV1ListFoos", + "operationId": "foo:v1.listFoos", "parameters": [ { "name": "n", @@ -31,7 +43,7 @@ ], "responses": { "200": { - "description": "A successful response", + "description": "A CollectionResponse_Foo response", "schema": { "$ref": "#/definitions/CollectionResponse_Foo" } @@ -39,19 +51,25 @@ }, "security": [ { - "google_id_token-3a26ea04": [] + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] }, { - "google_id_token_https-3a26ea04": [] + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] } ] }, "post": { - "operationId": "FooV1Toplevel", - "parameters": [], + "tags": [ + "foo:v1" + ], + "operationId": "foo:v1.toplevel", "responses": { "200": { - "description": "A successful response", + "description": "A CollectionResponse_Foo response", "schema": { "$ref": "#/definitions/CollectionResponse_Foo" } @@ -59,18 +77,25 @@ }, "security": [ { - "google_id_token-3a26ea04": [] + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] }, { - "google_id_token_https-3a26ea04": [] + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] } ] } }, "/foo/v1/foos/{id}": { "get": { + "tags": [ + "foo:v1" + ], "description": "get desc", - "operationId": "FooV1GetFoo", + "operationId": "foo:v1.getFoo", "parameters": [ { "name": "id", @@ -82,7 +107,7 @@ ], "responses": { "200": { - "description": "A successful response", + "description": "A Foo response", "schema": { "$ref": "#/definitions/Foo" } @@ -90,16 +115,23 @@ }, "security": [ { - "google_id_token-3a26ea04": [] + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] }, { - "google_id_token_https-3a26ea04": [] + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] } ] }, "post": { + "tags": [ + "foo:v1" + ], "description": "update desc", - "operationId": "FooV1UpdateFoo", + "operationId": "foo:v1.updateFoo", "parameters": [ { "name": "id", @@ -110,8 +142,8 @@ }, { "in": "body", - "name": "body", - "required": false, + "name": "Foo", + "required": true, "schema": { "$ref": "#/definitions/Foo" } @@ -119,7 +151,7 @@ ], "responses": { "200": { - "description": "A successful response", + "description": "A Foo response", "schema": { "$ref": "#/definitions/Foo" } @@ -127,16 +159,23 @@ }, "security": [ { - "google_id_token-3a26ea04": [] + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] }, { - "google_id_token_https-3a26ea04": [] + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] } ] }, "put": { + "tags": [ + "foo:v1" + ], "description": "create desc", - "operationId": "FooV1CreateFoo", + "operationId": "foo:v1.createFoo", "parameters": [ { "name": "id", @@ -147,8 +186,8 @@ }, { "in": "body", - "name": "body", - "required": false, + "name": "Foo", + "required": true, "schema": { "$ref": "#/definitions/Foo" } @@ -156,7 +195,7 @@ ], "responses": { "200": { - "description": "A successful response", + "description": "A Foo response", "schema": { "$ref": "#/definitions/Foo" } @@ -164,16 +203,23 @@ }, "security": [ { - "google_id_token-3a26ea04": [] + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] }, { - "google_id_token_https-3a26ea04": [] + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] } ] }, "delete": { + "tags": [ + "foo:v1" + ], "description": "delete desc", - "operationId": "FooV1DeleteFoo", + "operationId": "foo:v1.deleteFoo", "parameters": [ { "name": "id", @@ -185,7 +231,7 @@ ], "responses": { "200": { - "description": "A successful response", + "description": "A Foo response", "schema": { "$ref": "#/definitions/Foo" } @@ -193,10 +239,14 @@ }, "security": [ { - "google_id_token-3a26ea04": [] + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] }, { - "google_id_token_https-3a26ea04": [] + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] } ] } @@ -205,34 +255,28 @@ "securityDefinitions": { "google_id_token-3a26ea04": { "type": "oauth2", - "authorizationUrl": "", + "authorizationUrl": "https://accounts.google.com/o/oauth2/v2/auth", "flow": "implicit", - "x-google-issuer": "accounts.google.com", + "scopes": { + "https://www.googleapis.com/auth/userinfo.email": "View your email address" + }, + "x-google-issuer": "https://accounts.google.com", "x-google-jwks_uri": "https://www.googleapis.com/oauth2/v1/certs", "x-google-audiences": "audience" }, - "google_id_token_https-3a26ea04": { + "google_id_token_legacy-3a26ea04": { "type": "oauth2", - "authorizationUrl": "", + "authorizationUrl": "https://accounts.google.com/o/oauth2/v2/auth", "flow": "implicit", - "x-google-issuer": "https://accounts.google.com", + "scopes": { + "https://www.googleapis.com/auth/userinfo.email": "View your email address" + }, + "x-google-issuer": "accounts.google.com", "x-google-jwks_uri": "https://www.googleapis.com/oauth2/v1/certs", "x-google-audiences": "audience" } }, "definitions": { - "Foo": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "value": { - "type": "integer", - "format": "int32" - } - } - }, "CollectionResponse_Foo": { "type": "object", "properties": { @@ -246,6 +290,19 @@ "type": "string" } } + }, + "Foo": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "integer", + "format": "int32" + } + } } - } + }, + "x-google-api-name": "customApiName" } diff --git a/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/foo_endpoint_combine_all_params.swagger b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/foo_endpoint_combine_all_params.swagger new file mode 100644 index 00000000..6b1370d6 --- /dev/null +++ b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/foo_endpoint_combine_all_params.swagger @@ -0,0 +1,394 @@ +{ + "swagger": "2.0", + "info": { + "version": "1.0.0", + "title": "swagger-test.appspot.com" + }, + "host": "swagger-test.appspot.com", + "basePath": "/api", + "tags": [ + { + "name": "foo:v1", + "description": "Just Foo Things", + "externalDocs": { + "url": "https://example.com" + } + } + ], + "schemes": [ + "https" + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "paths": { + "/foo/v1/fooos": { + "get": { + "tags": [ + "foo:v1" + ], + "operationId": "foo:v1.listFooos", + "parameters": [ + { + "$ref": "#/parameters/n_query_parameter" + } + ], + "responses": { + "200": { + "description": "A CollectionResponse_Foo response", + "schema": { + "$ref": "#/definitions/CollectionResponse_Foo" + } + } + }, + "security": [ + { + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + }, + { + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + } + ] + } + }, + "/foo/v1/fooos/{n}": { + "get": { + "tags": [ + "foo:v1" + ], + "operationId": "foo:v1.listFooosInPath", + "responses": { + "200": { + "description": "A CollectionResponse_Foo response", + "schema": { + "$ref": "#/definitions/CollectionResponse_Foo" + } + } + }, + "security": [ + { + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + }, + { + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + } + ] + }, + "parameters": [ + { + "name": "n", + "in": "path", + "required": true, + "type": "integer", + "format": "int32" + } + ] + }, + "/foo/v1/fooosNotRequired": { + "get": { + "tags": [ + "foo:v1" + ], + "operationId": "foo:v1.listFooosNotRequired", + "responses": { + "200": { + "description": "A CollectionResponse_Foo response", + "schema": { + "$ref": "#/definitions/CollectionResponse_Foo" + } + } + }, + "security": [ + { + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + }, + { + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + } + ] + }, + "parameters": [ + { + "name": "n", + "in": "query", + "required": false, + "type": "integer", + "format": "int32" + } + ] + }, + "/foo/v1/foos": { + "get": { + "tags": [ + "foo:v1" + ], + "description": "list desc", + "operationId": "foo:v1.listFoos", + "parameters": [ + { + "$ref": "#/parameters/n_query_parameter" + } + ], + "responses": { + "200": { + "description": "A CollectionResponse_Foo response", + "schema": { + "$ref": "#/definitions/CollectionResponse_Foo" + } + } + }, + "security": [ + { + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + }, + { + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + } + ] + }, + "post": { + "tags": [ + "foo:v1" + ], + "operationId": "foo:v1.toplevel", + "responses": { + "200": { + "description": "A CollectionResponse_Foo response", + "schema": { + "$ref": "#/definitions/CollectionResponse_Foo" + } + } + }, + "security": [ + { + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + }, + { + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + } + ] + } + }, + "/foo/v1/foos/{id}": { + "get": { + "tags": [ + "foo:v1" + ], + "description": "get desc", + "operationId": "foo:v1.getFoo", + "responses": { + "200": { + "description": "A Foo response", + "schema": { + "$ref": "#/definitions/Foo" + } + } + }, + "security": [ + { + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + }, + { + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + } + ] + }, + "post": { + "tags": [ + "foo:v1" + ], + "description": "update desc", + "operationId": "foo:v1.updateFoo", + "parameters": [ + { + "$ref": "#/parameters/Foo_body_parameter" + } + ], + "responses": { + "200": { + "description": "A Foo response", + "schema": { + "$ref": "#/definitions/Foo" + } + } + }, + "security": [ + { + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + }, + { + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + } + ] + }, + "put": { + "tags": [ + "foo:v1" + ], + "description": "create desc", + "operationId": "foo:v1.createFoo", + "parameters": [ + { + "$ref": "#/parameters/Foo_body_parameter" + } + ], + "responses": { + "200": { + "description": "A Foo response", + "schema": { + "$ref": "#/definitions/Foo" + } + } + }, + "security": [ + { + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + }, + { + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + } + ] + }, + "delete": { + "tags": [ + "foo:v1" + ], + "description": "delete desc", + "operationId": "foo:v1.deleteFoo", + "responses": { + "200": { + "description": "A Foo response", + "schema": { + "$ref": "#/definitions/Foo" + } + } + }, + "security": [ + { + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + }, + { + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + } + ] + }, + "parameters": [ + { + "name": "id", + "in": "path", + "description": "id desc", + "required": true, + "type": "string" + } + ] + } + }, + "securityDefinitions": { + "google_id_token-3a26ea04": { + "type": "oauth2", + "authorizationUrl": "https://accounts.google.com/o/oauth2/v2/auth", + "flow": "implicit", + "scopes": { + "https://www.googleapis.com/auth/userinfo.email": "View your email address" + }, + "x-google-issuer": "https://accounts.google.com", + "x-google-jwks_uri": "https://www.googleapis.com/oauth2/v1/certs", + "x-google-audiences": "audience" + }, + "google_id_token_legacy-3a26ea04": { + "type": "oauth2", + "authorizationUrl": "https://accounts.google.com/o/oauth2/v2/auth", + "flow": "implicit", + "scopes": { + "https://www.googleapis.com/auth/userinfo.email": "View your email address" + }, + "x-google-issuer": "accounts.google.com", + "x-google-jwks_uri": "https://www.googleapis.com/oauth2/v1/certs", + "x-google-audiences": "audience" + } + }, + "definitions": { + "CollectionResponse_Foo": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/Foo" + } + }, + "nextPageToken": { + "type": "string" + } + } + }, + "Foo": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "integer", + "format": "int32" + } + } + } + }, + "parameters": { + "Foo_body_parameter": { + "in": "body", + "name": "Foo", + "required": true, + "schema": { + "$ref": "#/definitions/Foo" + } + }, + "n_query_parameter": { + "name": "n", + "in": "query", + "required": true, + "type": "integer", + "format": "int32" + } + } +} diff --git a/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/foo_endpoint_combine_params_same_path.swagger b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/foo_endpoint_combine_params_same_path.swagger new file mode 100644 index 00000000..b20476f5 --- /dev/null +++ b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/foo_endpoint_combine_params_same_path.swagger @@ -0,0 +1,395 @@ +{ + "swagger": "2.0", + "info": { + "version": "1.0.0", + "title": "swagger-test.appspot.com" + }, + "host": "swagger-test.appspot.com", + "basePath": "/api", + "tags": [ + { + "name": "foo:v1", + "description": "Just Foo Things", + "externalDocs": { + "url": "https://example.com" + } + } + ], + "schemes": [ + "https" + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "paths": { + "/foo/v1/fooos": { + "get": { + "tags": [ + "foo:v1" + ], + "operationId": "foo:v1.listFooos", + "responses": { + "200": { + "description": "A CollectionResponse_Foo response", + "schema": { + "$ref": "#/definitions/CollectionResponse_Foo" + } + } + }, + "security": [ + { + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + }, + { + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + } + ] + }, + "parameters": [ + { + "name": "n", + "in": "query", + "required": true, + "type": "integer", + "format": "int32" + } + ] + }, + "/foo/v1/fooos/{n}": { + "get": { + "tags": [ + "foo:v1" + ], + "operationId": "foo:v1.listFooosInPath", + "responses": { + "200": { + "description": "A CollectionResponse_Foo response", + "schema": { + "$ref": "#/definitions/CollectionResponse_Foo" + } + } + }, + "security": [ + { + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + }, + { + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + } + ] + }, + "parameters": [ + { + "name": "n", + "in": "path", + "required": true, + "type": "integer", + "format": "int32" + } + ] + }, + "/foo/v1/fooosNotRequired": { + "get": { + "tags": [ + "foo:v1" + ], + "operationId": "foo:v1.listFooosNotRequired", + "responses": { + "200": { + "description": "A CollectionResponse_Foo response", + "schema": { + "$ref": "#/definitions/CollectionResponse_Foo" + } + } + }, + "security": [ + { + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + }, + { + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + } + ] + }, + "parameters": [ + { + "name": "n", + "in": "query", + "required": false, + "type": "integer", + "format": "int32" + } + ] + }, + "/foo/v1/foos": { + "get": { + "tags": [ + "foo:v1" + ], + "description": "list desc", + "operationId": "foo:v1.listFoos", + "parameters": [ + { + "name": "n", + "in": "query", + "required": true, + "type": "integer", + "format": "int32" + } + ], + "responses": { + "200": { + "description": "A CollectionResponse_Foo response", + "schema": { + "$ref": "#/definitions/CollectionResponse_Foo" + } + } + }, + "security": [ + { + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + }, + { + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + } + ] + }, + "post": { + "tags": [ + "foo:v1" + ], + "operationId": "foo:v1.toplevel", + "responses": { + "200": { + "description": "A CollectionResponse_Foo response", + "schema": { + "$ref": "#/definitions/CollectionResponse_Foo" + } + } + }, + "security": [ + { + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + }, + { + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + } + ] + } + }, + "/foo/v1/foos/{id}": { + "get": { + "tags": [ + "foo:v1" + ], + "description": "get desc", + "operationId": "foo:v1.getFoo", + "responses": { + "200": { + "description": "A Foo response", + "schema": { + "$ref": "#/definitions/Foo" + } + } + }, + "security": [ + { + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + }, + { + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + } + ] + }, + "post": { + "tags": [ + "foo:v1" + ], + "description": "update desc", + "operationId": "foo:v1.updateFoo", + "parameters": [ + { + "in": "body", + "name": "Foo", + "required": true, + "schema": { + "$ref": "#/definitions/Foo" + } + } + ], + "responses": { + "200": { + "description": "A Foo response", + "schema": { + "$ref": "#/definitions/Foo" + } + } + }, + "security": [ + { + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + }, + { + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + } + ] + }, + "put": { + "tags": [ + "foo:v1" + ], + "description": "create desc", + "operationId": "foo:v1.createFoo", + "parameters": [ + { + "in": "body", + "name": "Foo", + "required": true, + "schema": { + "$ref": "#/definitions/Foo" + } + } + ], + "responses": { + "200": { + "description": "A Foo response", + "schema": { + "$ref": "#/definitions/Foo" + } + } + }, + "security": [ + { + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + }, + { + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + } + ] + }, + "delete": { + "tags": [ + "foo:v1" + ], + "description": "delete desc", + "operationId": "foo:v1.deleteFoo", + "responses": { + "200": { + "description": "A Foo response", + "schema": { + "$ref": "#/definitions/Foo" + } + } + }, + "security": [ + { + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + }, + { + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + } + ] + }, + "parameters": [ + { + "name": "id", + "in": "path", + "description": "id desc", + "required": true, + "type": "string" + } + ] + } + }, + "securityDefinitions": { + "google_id_token-3a26ea04": { + "type": "oauth2", + "authorizationUrl": "https://accounts.google.com/o/oauth2/v2/auth", + "flow": "implicit", + "scopes": { + "https://www.googleapis.com/auth/userinfo.email": "View your email address" + }, + "x-google-issuer": "https://accounts.google.com", + "x-google-jwks_uri": "https://www.googleapis.com/oauth2/v1/certs", + "x-google-audiences": "audience" + }, + "google_id_token_legacy-3a26ea04": { + "type": "oauth2", + "authorizationUrl": "https://accounts.google.com/o/oauth2/v2/auth", + "flow": "implicit", + "scopes": { + "https://www.googleapis.com/auth/userinfo.email": "View your email address" + }, + "x-google-issuer": "accounts.google.com", + "x-google-jwks_uri": "https://www.googleapis.com/oauth2/v1/certs", + "x-google-audiences": "audience" + } + }, + "definitions": { + "CollectionResponse_Foo": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/Foo" + } + }, + "nextPageToken": { + "type": "string" + } + } + }, + "Foo": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "integer", + "format": "int32" + } + } + } + } +} diff --git a/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/foo_endpoint_internal.swagger b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/foo_endpoint_custom_templates.swagger similarity index 58% rename from endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/foo_endpoint_internal.swagger rename to endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/foo_endpoint_custom_templates.swagger index 95dd8029..63c832f7 100644 --- a/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/foo_endpoint_internal.swagger +++ b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/foo_endpoint_custom_templates.swagger @@ -2,10 +2,19 @@ "swagger": "2.0", "info": { "version": "1.0.0", - "title": "swagger-test.appspot.com" + "title": "myapi.appspot.com" }, - "host": "swagger-test.appspot.com", - "basePath": "/api", + "host": "myapi.appspot.com", + "basePath": "/_ah/api", + "tags": [ + { + "name": "FooV1", + "description": "Just Foo Things", + "externalDocs": { + "url": "https://example.com" + } + } + ], "schemes": [ "https" ], @@ -18,8 +27,11 @@ "paths": { "/foo/v1/foos": { "get": { + "tags": [ + "FooV1" + ], "description": "list desc", - "operationId": "FooV1ListFoos", + "operationId": "foo-v1-listFoos", "parameters": [ { "name": "n", @@ -31,7 +43,7 @@ ], "responses": { "200": { - "description": "A successful response", + "description": "A CollectionResponse_Foo response", "schema": { "$ref": "#/definitions/CollectionResponse_Foo" } @@ -39,19 +51,25 @@ }, "security": [ { - "google_id_token-3a26ea04": [] + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] }, { - "google_id_token_https-3a26ea04": [] + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] } ] }, "post": { - "operationId": "FooV1Toplevel", - "parameters": [], + "tags": [ + "FooV1" + ], + "operationId": "foo-v1-toplevel", "responses": { "200": { - "description": "A successful response", + "description": "A CollectionResponse_Foo response", "schema": { "$ref": "#/definitions/CollectionResponse_Foo" } @@ -59,18 +77,25 @@ }, "security": [ { - "google_id_token-3a26ea04": [] + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] }, { - "google_id_token_https-3a26ea04": [] + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] } ] } }, "/foo/v1/foos/{id}": { "get": { + "tags": [ + "FooV1" + ], "description": "get desc", - "operationId": "FooV1GetFoo", + "operationId": "foo-v1-getFoo", "parameters": [ { "name": "id", @@ -82,7 +107,7 @@ ], "responses": { "200": { - "description": "A successful response", + "description": "A Foo response", "schema": { "$ref": "#/definitions/Foo" } @@ -90,16 +115,23 @@ }, "security": [ { - "google_id_token-3a26ea04": [] + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] }, { - "google_id_token_https-3a26ea04": [] + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] } ] }, "post": { + "tags": [ + "FooV1" + ], "description": "update desc", - "operationId": "FooV1UpdateFoo", + "operationId": "foo-v1-updateFoo", "parameters": [ { "name": "id", @@ -110,8 +142,8 @@ }, { "in": "body", - "name": "body", - "required": false, + "name": "Foo", + "required": true, "schema": { "$ref": "#/definitions/Foo" } @@ -119,7 +151,7 @@ ], "responses": { "200": { - "description": "A successful response", + "description": "A Foo response", "schema": { "$ref": "#/definitions/Foo" } @@ -127,16 +159,23 @@ }, "security": [ { - "google_id_token-3a26ea04": [] + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] }, { - "google_id_token_https-3a26ea04": [] + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] } ] }, "put": { + "tags": [ + "FooV1" + ], "description": "create desc", - "operationId": "FooV1CreateFoo", + "operationId": "foo-v1-createFoo", "parameters": [ { "name": "id", @@ -147,8 +186,8 @@ }, { "in": "body", - "name": "body", - "required": false, + "name": "Foo", + "required": true, "schema": { "$ref": "#/definitions/Foo" } @@ -156,7 +195,7 @@ ], "responses": { "200": { - "description": "A successful response", + "description": "A Foo response", "schema": { "$ref": "#/definitions/Foo" } @@ -164,16 +203,23 @@ }, "security": [ { - "google_id_token-3a26ea04": [] + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] }, { - "google_id_token_https-3a26ea04": [] + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] } ] }, "delete": { + "tags": [ + "FooV1" + ], "description": "delete desc", - "operationId": "FooV1DeleteFoo", + "operationId": "foo-v1-deleteFoo", "parameters": [ { "name": "id", @@ -185,7 +231,7 @@ ], "responses": { "200": { - "description": "A successful response", + "description": "A Foo response", "schema": { "$ref": "#/definitions/Foo" } @@ -193,10 +239,14 @@ }, "security": [ { - "google_id_token-3a26ea04": [] + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] }, { - "google_id_token_https-3a26ea04": [] + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] } ] } @@ -205,17 +255,23 @@ "securityDefinitions": { "google_id_token-3a26ea04": { "type": "oauth2", - "authorizationUrl": "", + "authorizationUrl": "https://accounts.google.com/o/oauth2/v2/auth", "flow": "implicit", - "x-google-issuer": "accounts.google.com", + "scopes": { + "https://www.googleapis.com/auth/userinfo.email": "View your email address" + }, + "x-google-issuer": "https://accounts.google.com", "x-google-jwks_uri": "https://www.googleapis.com/oauth2/v1/certs", "x-google-audiences": "audience" }, - "google_id_token_https-3a26ea04": { + "google_id_token_legacy-3a26ea04": { "type": "oauth2", - "authorizationUrl": "", + "authorizationUrl": "https://accounts.google.com/o/oauth2/v2/auth", "flow": "implicit", - "x-google-issuer": "https://accounts.google.com", + "scopes": { + "https://www.googleapis.com/auth/userinfo.email": "View your email address" + }, + "x-google-issuer": "accounts.google.com", "x-google-jwks_uri": "https://www.googleapis.com/oauth2/v1/certs", "x-google-audiences": "audience" } diff --git a/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/foo_endpoint_default_context.swagger b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/foo_endpoint_default_context.swagger index f531f79e..42313fac 100644 --- a/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/foo_endpoint_default_context.swagger +++ b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/foo_endpoint_default_context.swagger @@ -6,6 +6,15 @@ }, "host": "myapi.appspot.com", "basePath": "/_ah/api", + "tags": [ + { + "name": "foo:v1", + "description": "Just Foo Things", + "externalDocs": { + "url": "https://example.com" + } + } + ], "schemes": [ "https" ], @@ -18,8 +27,11 @@ "paths": { "/foo/v1/foos": { "get": { + "tags": [ + "foo:v1" + ], "description": "list desc", - "operationId": "FooV1ListFoos", + "operationId": "foo:v1.listFoos", "parameters": [ { "name": "n", @@ -31,7 +43,7 @@ ], "responses": { "200": { - "description": "A successful response", + "description": "A CollectionResponse_Foo response", "schema": { "$ref": "#/definitions/CollectionResponse_Foo" } @@ -39,19 +51,25 @@ }, "security": [ { - "google_id_token-3a26ea04": [] + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] }, { - "google_id_token_https-3a26ea04": [] + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] } ] }, "post": { - "operationId": "FooV1Toplevel", - "parameters": [], + "tags": [ + "foo:v1" + ], + "operationId": "foo:v1.toplevel", "responses": { "200": { - "description": "A successful response", + "description": "A CollectionResponse_Foo response", "schema": { "$ref": "#/definitions/CollectionResponse_Foo" } @@ -59,18 +77,25 @@ }, "security": [ { - "google_id_token-3a26ea04": [] + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] }, { - "google_id_token_https-3a26ea04": [] + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] } ] } }, "/foo/v1/foos/{id}": { "get": { + "tags": [ + "foo:v1" + ], "description": "get desc", - "operationId": "FooV1GetFoo", + "operationId": "foo:v1.getFoo", "parameters": [ { "name": "id", @@ -82,7 +107,7 @@ ], "responses": { "200": { - "description": "A successful response", + "description": "A Foo response", "schema": { "$ref": "#/definitions/Foo" } @@ -90,16 +115,23 @@ }, "security": [ { - "google_id_token-3a26ea04": [] + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] }, { - "google_id_token_https-3a26ea04": [] + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] } ] }, "post": { + "tags": [ + "foo:v1" + ], "description": "update desc", - "operationId": "FooV1UpdateFoo", + "operationId": "foo:v1.updateFoo", "parameters": [ { "name": "id", @@ -110,8 +142,8 @@ }, { "in": "body", - "name": "body", - "required": false, + "name": "Foo", + "required": true, "schema": { "$ref": "#/definitions/Foo" } @@ -119,7 +151,7 @@ ], "responses": { "200": { - "description": "A successful response", + "description": "A Foo response", "schema": { "$ref": "#/definitions/Foo" } @@ -127,16 +159,23 @@ }, "security": [ { - "google_id_token-3a26ea04": [] + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] }, { - "google_id_token_https-3a26ea04": [] + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] } ] }, "put": { + "tags": [ + "foo:v1" + ], "description": "create desc", - "operationId": "FooV1CreateFoo", + "operationId": "foo:v1.createFoo", "parameters": [ { "name": "id", @@ -147,8 +186,8 @@ }, { "in": "body", - "name": "body", - "required": false, + "name": "Foo", + "required": true, "schema": { "$ref": "#/definitions/Foo" } @@ -156,7 +195,7 @@ ], "responses": { "200": { - "description": "A successful response", + "description": "A Foo response", "schema": { "$ref": "#/definitions/Foo" } @@ -164,16 +203,23 @@ }, "security": [ { - "google_id_token-3a26ea04": [] + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] }, { - "google_id_token_https-3a26ea04": [] + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] } ] }, "delete": { + "tags": [ + "foo:v1" + ], "description": "delete desc", - "operationId": "FooV1DeleteFoo", + "operationId": "foo:v1.deleteFoo", "parameters": [ { "name": "id", @@ -185,7 +231,7 @@ ], "responses": { "200": { - "description": "A successful response", + "description": "A Foo response", "schema": { "$ref": "#/definitions/Foo" } @@ -193,10 +239,14 @@ }, "security": [ { - "google_id_token-3a26ea04": [] + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] }, { - "google_id_token_https-3a26ea04": [] + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] } ] } @@ -205,17 +255,23 @@ "securityDefinitions": { "google_id_token-3a26ea04": { "type": "oauth2", - "authorizationUrl": "", + "authorizationUrl": "https://accounts.google.com/o/oauth2/v2/auth", "flow": "implicit", - "x-google-issuer": "accounts.google.com", + "scopes": { + "https://www.googleapis.com/auth/userinfo.email": "View your email address" + }, + "x-google-issuer": "https://accounts.google.com", "x-google-jwks_uri": "https://www.googleapis.com/oauth2/v1/certs", "x-google-audiences": "audience" }, - "google_id_token_https-3a26ea04": { + "google_id_token_legacy-3a26ea04": { "type": "oauth2", - "authorizationUrl": "", + "authorizationUrl": "https://accounts.google.com/o/oauth2/v2/auth", "flow": "implicit", - "x-google-issuer": "https://accounts.google.com", + "scopes": { + "https://www.googleapis.com/auth/userinfo.email": "View your email address" + }, + "x-google-issuer": "accounts.google.com", "x-google-jwks_uri": "https://www.googleapis.com/oauth2/v1/certs", "x-google-audiences": "audience" } diff --git a/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/foo_endpoint_extract_param_refs.swagger b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/foo_endpoint_extract_param_refs.swagger new file mode 100644 index 00000000..ad779877 --- /dev/null +++ b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/foo_endpoint_extract_param_refs.swagger @@ -0,0 +1,408 @@ +{ + "swagger": "2.0", + "info": { + "version": "1.0.0", + "title": "swagger-test.appspot.com" + }, + "host": "swagger-test.appspot.com", + "basePath": "/api", + "tags": [ + { + "name": "foo:v1", + "description": "Just Foo Things", + "externalDocs": { + "url": "https://example.com" + } + } + ], + "schemes": [ + "https" + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "paths": { + "/foo/v1/fooos": { + "get": { + "tags": [ + "foo:v1" + ], + "operationId": "foo:v1.listFooos", + "parameters": [ + { + "$ref": "#/parameters/n_query_parameter" + } + ], + "responses": { + "200": { + "description": "A CollectionResponse_Foo response", + "schema": { + "$ref": "#/definitions/CollectionResponse_Foo" + } + } + }, + "security": [ + { + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + }, + { + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + } + ] + } + }, + "/foo/v1/fooos/{n}": { + "get": { + "tags": [ + "foo:v1" + ], + "operationId": "foo:v1.listFooosInPath", + "parameters": [ + { + "name": "n", + "in": "path", + "required": true, + "type": "integer", + "format": "int32" + } + ], + "responses": { + "200": { + "description": "A CollectionResponse_Foo response", + "schema": { + "$ref": "#/definitions/CollectionResponse_Foo" + } + } + }, + "security": [ + { + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + }, + { + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + } + ] + } + }, + "/foo/v1/fooosNotRequired": { + "get": { + "tags": [ + "foo:v1" + ], + "operationId": "foo:v1.listFooosNotRequired", + "parameters": [ + { + "name": "n", + "in": "query", + "required": false, + "type": "integer", + "format": "int32" + } + ], + "responses": { + "200": { + "description": "A CollectionResponse_Foo response", + "schema": { + "$ref": "#/definitions/CollectionResponse_Foo" + } + } + }, + "security": [ + { + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + }, + { + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + } + ] + } + }, + "/foo/v1/foos": { + "get": { + "tags": [ + "foo:v1" + ], + "description": "list desc", + "operationId": "foo:v1.listFoos", + "parameters": [ + { + "$ref": "#/parameters/n_query_parameter" + } + ], + "responses": { + "200": { + "description": "A CollectionResponse_Foo response", + "schema": { + "$ref": "#/definitions/CollectionResponse_Foo" + } + } + }, + "security": [ + { + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + }, + { + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + } + ] + }, + "post": { + "tags": [ + "foo:v1" + ], + "operationId": "foo:v1.toplevel", + "responses": { + "200": { + "description": "A CollectionResponse_Foo response", + "schema": { + "$ref": "#/definitions/CollectionResponse_Foo" + } + } + }, + "security": [ + { + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + }, + { + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + } + ] + } + }, + "/foo/v1/foos/{id}": { + "get": { + "tags": [ + "foo:v1" + ], + "description": "get desc", + "operationId": "foo:v1.getFoo", + "parameters": [ + { + "$ref": "#/parameters/id_path_parameter" + } + ], + "responses": { + "200": { + "description": "A Foo response", + "schema": { + "$ref": "#/definitions/Foo" + } + } + }, + "security": [ + { + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + }, + { + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + } + ] + }, + "post": { + "tags": [ + "foo:v1" + ], + "description": "update desc", + "operationId": "foo:v1.updateFoo", + "parameters": [ + { + "$ref": "#/parameters/id_path_parameter" + }, + { + "$ref": "#/parameters/Foo_body_parameter" + } + ], + "responses": { + "200": { + "description": "A Foo response", + "schema": { + "$ref": "#/definitions/Foo" + } + } + }, + "security": [ + { + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + }, + { + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + } + ] + }, + "put": { + "tags": [ + "foo:v1" + ], + "description": "create desc", + "operationId": "foo:v1.createFoo", + "parameters": [ + { + "$ref": "#/parameters/id_path_parameter" + }, + { + "$ref": "#/parameters/Foo_body_parameter" + } + ], + "responses": { + "200": { + "description": "A Foo response", + "schema": { + "$ref": "#/definitions/Foo" + } + } + }, + "security": [ + { + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + }, + { + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + } + ] + }, + "delete": { + "tags": [ + "foo:v1" + ], + "description": "delete desc", + "operationId": "foo:v1.deleteFoo", + "parameters": [ + { + "$ref": "#/parameters/id_path_parameter" + } + ], + "responses": { + "200": { + "description": "A Foo response", + "schema": { + "$ref": "#/definitions/Foo" + } + } + }, + "security": [ + { + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + }, + { + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + } + ] + } + } + }, + "securityDefinitions": { + "google_id_token-3a26ea04": { + "type": "oauth2", + "authorizationUrl": "https://accounts.google.com/o/oauth2/v2/auth", + "flow": "implicit", + "scopes": { + "https://www.googleapis.com/auth/userinfo.email": "View your email address" + }, + "x-google-issuer": "https://accounts.google.com", + "x-google-jwks_uri": "https://www.googleapis.com/oauth2/v1/certs", + "x-google-audiences": "audience" + }, + "google_id_token_legacy-3a26ea04": { + "type": "oauth2", + "authorizationUrl": "https://accounts.google.com/o/oauth2/v2/auth", + "flow": "implicit", + "scopes": { + "https://www.googleapis.com/auth/userinfo.email": "View your email address" + }, + "x-google-issuer": "accounts.google.com", + "x-google-jwks_uri": "https://www.googleapis.com/oauth2/v1/certs", + "x-google-audiences": "audience" + } + }, + "definitions": { + "CollectionResponse_Foo": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/Foo" + } + }, + "nextPageToken": { + "type": "string" + } + } + }, + "Foo": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "integer", + "format": "int32" + } + } + } + }, + "parameters": { + "Foo_body_parameter": { + "in": "body", + "name": "Foo", + "required": true, + "schema": { + "$ref": "#/definitions/Foo" + } + }, + "id_path_parameter": { + "name": "id", + "in": "path", + "description": "id desc", + "required": true, + "type": "string" + }, + "n_query_parameter": { + "name": "n", + "in": "query", + "required": true, + "type": "integer", + "format": "int32" + } + } +} diff --git a/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/foo_with_description_endpoint.swagger b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/foo_with_description_endpoint.swagger index c291a6a6..0b3eba64 100644 --- a/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/foo_with_description_endpoint.swagger +++ b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/foo_with_description_endpoint.swagger @@ -6,6 +6,12 @@ }, "host": "swagger-test.appspot.com", "basePath": "/api", + "tags": [ + { + "name": "foo:v1", + "description": "Just Foo Things" + } + ], "schemes": [ "https" ], @@ -18,8 +24,11 @@ "paths": { "/foo/v1/foos": { "get": { + "tags": [ + "foo:v1" + ], "description": "list desc", - "operationId": "FooV1ListFoos", + "operationId": "foo:v1.listFoos", "parameters": [ { "name": "n", @@ -42,7 +51,7 @@ ], "responses": { "200": { - "description": "A successful response", + "description": "A CollectionResponse_FooDescription response", "schema": { "$ref": "#/definitions/CollectionResponse_FooDescription" } @@ -50,19 +59,25 @@ }, "security": [ { - "google_id_token-3a26ea04": [] + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] }, { - "google_id_token_https-3a26ea04": [] + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] } ] }, "post": { - "operationId": "FooV1Toplevel", - "parameters": [], + "tags": [ + "foo:v1" + ], + "operationId": "foo:v1.toplevel", "responses": { "200": { - "description": "A successful response", + "description": "A CollectionResponse_FooDescription response", "schema": { "$ref": "#/definitions/CollectionResponse_FooDescription" } @@ -70,18 +85,25 @@ }, "security": [ { - "google_id_token-3a26ea04": [] + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] }, { - "google_id_token_https-3a26ea04": [] + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] } ] } }, "/foo/v1/foos/{id}": { "get": { + "tags": [ + "foo:v1" + ], "description": "get desc", - "operationId": "FooV1GetFoo", + "operationId": "foo:v1.getFoo", "parameters": [ { "name": "id", @@ -93,7 +115,7 @@ ], "responses": { "200": { - "description": "A successful response", + "description": "A FooDescription response", "schema": { "$ref": "#/definitions/FooDescription" } @@ -101,16 +123,23 @@ }, "security": [ { - "google_id_token-3a26ea04": [] + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] }, { - "google_id_token_https-3a26ea04": [] + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] } ] }, "post": { + "tags": [ + "foo:v1" + ], "description": "update desc", - "operationId": "FooV1UpdateFoo", + "operationId": "foo:v1.updateFoo", "parameters": [ { "name": "id", @@ -121,8 +150,8 @@ }, { "in": "body", - "name": "body", - "required": false, + "name": "FooDescription", + "required": true, "schema": { "$ref": "#/definitions/FooDescription" } @@ -130,7 +159,7 @@ ], "responses": { "200": { - "description": "A successful response", + "description": "A FooDescription response", "schema": { "$ref": "#/definitions/FooDescription" } @@ -138,16 +167,23 @@ }, "security": [ { - "google_id_token-3a26ea04": [] + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] }, { - "google_id_token_https-3a26ea04": [] + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] } ] }, "put": { + "tags": [ + "foo:v1" + ], "description": "create desc", - "operationId": "FooV1CreateFoo", + "operationId": "foo:v1.createFoo", "parameters": [ { "name": "id", @@ -158,8 +194,9 @@ }, { "in": "body", - "name": "body", - "required": false, + "name": "FooDescription", + "description": "Description at method parameter level", + "required": true, "schema": { "$ref": "#/definitions/FooDescription" } @@ -167,7 +204,7 @@ ], "responses": { "200": { - "description": "A successful response", + "description": "A FooDescription response", "schema": { "$ref": "#/definitions/FooDescription" } @@ -175,16 +212,23 @@ }, "security": [ { - "google_id_token-3a26ea04": [] + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] }, { - "google_id_token_https-3a26ea04": [] + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] } ] }, "delete": { + "tags": [ + "foo:v1" + ], "description": "delete desc", - "operationId": "FooV1DeleteFoo", + "operationId": "foo:v1.deleteFoo", "parameters": [ { "name": "id", @@ -196,7 +240,7 @@ ], "responses": { "200": { - "description": "A successful response", + "description": "A FooDescription response", "schema": { "$ref": "#/definitions/FooDescription" } @@ -204,10 +248,14 @@ }, "security": [ { - "google_id_token-3a26ea04": [] + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] }, { - "google_id_token_https-3a26ea04": [] + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] } ] } @@ -216,22 +264,42 @@ "securityDefinitions": { "google_id_token-3a26ea04": { "type": "oauth2", - "authorizationUrl": "", + "authorizationUrl": "https://accounts.google.com/o/oauth2/v2/auth", "flow": "implicit", - "x-google-issuer": "accounts.google.com", + "scopes": { + "https://www.googleapis.com/auth/userinfo.email": "View your email address" + }, + "x-google-issuer": "https://accounts.google.com", "x-google-jwks_uri": "https://www.googleapis.com/oauth2/v1/certs", "x-google-audiences": "audience" }, - "google_id_token_https-3a26ea04": { + "google_id_token_legacy-3a26ea04": { "type": "oauth2", - "authorizationUrl": "", + "authorizationUrl": "https://accounts.google.com/o/oauth2/v2/auth", "flow": "implicit", - "x-google-issuer": "https://accounts.google.com", + "scopes": { + "https://www.googleapis.com/auth/userinfo.email": "View your email address" + }, + "x-google-issuer": "accounts.google.com", "x-google-jwks_uri": "https://www.googleapis.com/oauth2/v1/certs", "x-google-audiences": "audience" } }, "definitions": { + "CollectionResponse_FooDescription": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/FooDescription" + } + }, + "nextPageToken": { + "type": "string" + } + } + }, "FooDescription": { "type": "object", "properties": { @@ -252,21 +320,8 @@ "format": "int32", "description": "description of value" } - } - }, - "CollectionResponse_FooDescription": { - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "$ref": "#/definitions/FooDescription" - } - }, - "nextPageToken": { - "type": "string" - } - } + }, + "description": "Description at class level" } } } diff --git a/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/google_auth.swagger b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/google_auth.swagger index 532b9fde..27340862 100644 --- a/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/google_auth.swagger +++ b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/google_auth.swagger @@ -6,6 +6,11 @@ }, "host": "swagger-test.appspot.com", "basePath": "/api", + "tags": [ + { + "name": "thirdparty:v1" + } + ], "schemes": [ "https" ], @@ -18,10 +23,12 @@ "paths": { "/thirdparty/v1/authOverride": { "post": { - "operationId": "ThirdpartyV1AuthOverride", - "parameters": [], + "tags": [ + "thirdparty:v1" + ], + "operationId": "thirdparty:v1.authOverride", "responses": { - "200": { + "204": { "description": "A successful response" } }, @@ -34,26 +41,32 @@ }, "/thirdparty/v1/googleAuth": { "post": { - "operationId": "ThirdpartyV1GoogleAuth", - "parameters": [], + "tags": [ + "thirdparty:v1" + ], + "operationId": "thirdparty:v1.googleAuth", "responses": { - "200": { + "204": { "description": "A successful response" } }, "security": [ { - "google_id_token-57e345d7": [] + "google_id_token_legacy-57e345d7": [ + "https://www.googleapis.com/auth/userinfo.email" + ] } ] } }, "/thirdparty/v1/noOverride": { "post": { - "operationId": "ThirdpartyV1NoOverride", - "parameters": [], + "tags": [ + "thirdparty:v1" + ], + "operationId": "thirdparty:v1.noOverride", "responses": { - "200": { + "204": { "description": "A successful response" } }, @@ -74,14 +87,6 @@ "x-google-jwks_uri": "https://test.auth0.com/.wellknown/jwks.json", "x-google-audiences": "auth0audmethod" }, - "google_id_token-57e345d7": { - "type": "oauth2", - "authorizationUrl": "", - "flow": "implicit", - "x-google-issuer": "accounts.google.com", - "x-google-jwks_uri": "https://www.googleapis.com/oauth2/v1/certs", - "x-google-audiences": "googleaud" - }, "auth0-a05d2f2": { "type": "oauth2", "authorizationUrl": "", @@ -89,6 +94,17 @@ "x-google-issuer": "https://test.auth0.com/authorize", "x-google-jwks_uri": "https://test.auth0.com/.wellknown/jwks.json", "x-google-audiences": "auth0audapi" + }, + "google_id_token_legacy-57e345d7": { + "type": "oauth2", + "authorizationUrl": "https://accounts.google.com/o/oauth2/v2/auth", + "flow": "implicit", + "scopes": { + "https://www.googleapis.com/auth/userinfo.email": "View your email address" + }, + "x-google-issuer": "accounts.google.com", + "x-google-jwks_uri": "https://www.googleapis.com/oauth2/v1/certs", + "x-google-audiences": "googleaud" } } } diff --git a/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/limit_metrics_endpoint.swagger b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/limit_metrics_endpoint.swagger index 05eaf11c..12b2e3f2 100644 --- a/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/limit_metrics_endpoint.swagger +++ b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/limit_metrics_endpoint.swagger @@ -6,6 +6,11 @@ }, "host": "myapi.appspot.com", "basePath": "/_ah/api", + "tags": [ + { + "name": "limits:v1" + } + ], "schemes": [ "https" ], @@ -18,11 +23,13 @@ "paths": { "/limits/v1/create": { "post": { - "operationId": "LimitsV1CreateFoo", - "parameters": [], + "tags": [ + "limits:v1" + ], + "operationId": "limits:v1.createFoo", "responses": { "200": { - "description": "A successful response", + "description": "A Foo response", "schema": { "$ref": "#/definitions/Foo" } @@ -37,10 +44,12 @@ }, "/limits/v1/customFoo": { "post": { - "operationId": "LimitsV1CustomFoo", - "parameters": [], + "tags": [ + "limits:v1" + ], + "operationId": "limits:v1.customFoo", "responses": { - "200": { + "204": { "description": "A successful response" } }, @@ -72,7 +81,8 @@ { "name": "read", "valueType": "INT64", - "metricKind": "GAUGE" + "metricKind": "GAUGE", + "displayName": "Read requests" }, { "name": "write", @@ -88,8 +98,7 @@ "values": { "STANDARD": 100 }, - "unit": "1/min/{project}", - "displayName": "Read requests" + "unit": "1/min/{project}" }, { "name": "write", diff --git a/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/map_endpoint.swagger b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/map_endpoint.swagger index 5fb2ba37..7623dd85 100644 --- a/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/map_endpoint.swagger +++ b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/map_endpoint.swagger @@ -6,6 +6,11 @@ }, "host": "myapi.appspot.com", "basePath": "/_ah/api", + "tags": [ + { + "name": "myapi:v1" + } + ], "schemes": [ "https" ], @@ -18,13 +23,18 @@ "paths": { "/myapi/v1/getDateTimeKeyMap": { "get": { - "operationId": "MyapiV1GetDateTimeKeyMap", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getDateTimeKeyMap", "responses": { "200": { - "description": "A successful response", + "description": "A Map_DateTime_String response", "schema": { - "$ref": "#/definitions/Map_DateTime_String" + "type": "object", + "additionalProperties": { + "type": "string" + } } } } @@ -32,11 +42,13 @@ }, "/myapi/v1/getMapOfStrings": { "get": { - "operationId": "MyapiV1GetMapOfStrings", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getMapOfStrings", "responses": { "200": { - "description": "A successful response", + "description": "A MapContainer response", "schema": { "$ref": "#/definitions/MapContainer" } @@ -46,13 +58,19 @@ }, "/myapi/v1/getStringCollectionMap": { "get": { - "operationId": "MyapiV1GetStringCollectionMap", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getStringCollectionMap", "responses": { "200": { - "description": "A successful response", + "description": "A JsonMap response", "schema": { - "$ref": "#/definitions/JsonMap" + "type": "object", + "additionalProperties": { + "type": "object", + "properties": {} + } } } } @@ -60,13 +78,18 @@ }, "/myapi/v1/map_boolean_string": { "get": { - "operationId": "MyapiV1GetBooleanKeyMap", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getBooleanKeyMap", "responses": { "200": { - "description": "A successful response", + "description": "A Map_Boolean_String response", "schema": { - "$ref": "#/definitions/Map_Boolean_String" + "type": "object", + "additionalProperties": { + "type": "string" + } } } } @@ -74,13 +97,18 @@ }, "/myapi/v1/map_datetime_string": { "get": { - "operationId": "MyapiV1GetDateKeyMap", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getDateKeyMap", "responses": { "200": { - "description": "A successful response", + "description": "A Map_DateTime_String response", "schema": { - "$ref": "#/definitions/Map_DateTime_String" + "type": "object", + "additionalProperties": { + "type": "string" + } } } } @@ -88,13 +116,18 @@ }, "/myapi/v1/map_float_string": { "get": { - "operationId": "MyapiV1GetFloatKeyMap", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getFloatKeyMap", "responses": { "200": { - "description": "A successful response", + "description": "A Map_Float_String response", "schema": { - "$ref": "#/definitions/Map_Float_String" + "type": "object", + "additionalProperties": { + "type": "string" + } } } } @@ -102,13 +135,18 @@ }, "/myapi/v1/map_integer_string": { "get": { - "operationId": "MyapiV1GetIntKeyMap", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getIntKeyMap", "responses": { "200": { - "description": "A successful response", + "description": "A Map_Integer_String response", "schema": { - "$ref": "#/definitions/Map_Integer_String" + "type": "object", + "additionalProperties": { + "type": "string" + } } } } @@ -116,13 +154,18 @@ }, "/myapi/v1/map_long_string": { "get": { - "operationId": "MyapiV1GetLongKeyMap", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getLongKeyMap", "responses": { "200": { - "description": "A successful response", + "description": "A Map_Long_String response", "schema": { - "$ref": "#/definitions/Map_Long_String" + "type": "object", + "additionalProperties": { + "type": "string" + } } } } @@ -130,13 +173,18 @@ }, "/myapi/v1/map_string_baz": { "get": { - "operationId": "MyapiV1GetBazMap", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getBazMap", "responses": { "200": { - "description": "A successful response", + "description": "A Map_String_Baz response", "schema": { - "$ref": "#/definitions/Map_String_Baz" + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Baz" + } } } } @@ -144,13 +192,18 @@ }, "/myapi/v1/map_string_foo": { "get": { - "operationId": "MyapiV1GetFooMap", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getFooMap", "responses": { "200": { - "description": "A successful response", + "description": "A Map_String_Foo response", "schema": { - "$ref": "#/definitions/Map_String_Foo" + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Foo" + } } } } @@ -158,13 +211,19 @@ }, "/myapi/v1/map_string_integer": { "get": { - "operationId": "MyapiV1GetIntMap", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getIntMap", "responses": { "200": { - "description": "A successful response", + "description": "A Map_String_Integer response", "schema": { - "$ref": "#/definitions/Map_String_Integer" + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "int32" + } } } } @@ -172,13 +231,21 @@ }, "/myapi/v1/map_string_map_string_foo": { "get": { - "operationId": "MyapiV1GetFooMapMap", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getFooMapMap", "responses": { "200": { - "description": "A successful response", + "description": "A Map_String_Map_String_Foo response", "schema": { - "$ref": "#/definitions/Map_String_Map_String_Foo" + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Foo" + } + } } } } @@ -186,13 +253,18 @@ }, "/myapi/v1/map_string_string": { "get": { - "operationId": "MyapiV1GetStringMap", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getStringMap", "responses": { "200": { - "description": "A successful response", + "description": "A Map_String_String response", "schema": { - "$ref": "#/definitions/Map_String_String" + "type": "object", + "additionalProperties": { + "type": "string" + } } } } @@ -200,13 +272,19 @@ }, "/myapi/v1/map_string_stringcollection": { "get": { - "operationId": "MyapiV1GetStringArrayMap", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getStringArrayMap", "responses": { "200": { - "description": "A successful response", + "description": "A JsonMap response", "schema": { - "$ref": "#/definitions/JsonMap" + "type": "object", + "additionalProperties": { + "type": "object", + "properties": {} + } } } } @@ -214,13 +292,18 @@ }, "/myapi/v1/map_string_stringvalue": { "get": { - "operationId": "MyapiV1GetStringValueMap", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getStringValueMap", "responses": { "200": { - "description": "A successful response", + "description": "A Map_String_StringValue response", "schema": { - "$ref": "#/definitions/Map_String_StringValue" + "type": "object", + "additionalProperties": { + "type": "string" + } } } } @@ -228,13 +311,18 @@ }, "/myapi/v1/map_testenum_string": { "get": { - "operationId": "MyapiV1GetEnumKeyMap", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getEnumKeyMap", "responses": { "200": { - "description": "A successful response", + "description": "A Map_TestEnum_String response", "schema": { - "$ref": "#/definitions/Map_TestEnum_String" + "type": "object", + "additionalProperties": { + "type": "string" + } } } } @@ -242,11 +330,13 @@ }, "/myapi/v1/mapendpoint": { "get": { - "operationId": "MyapiV1GetMapService", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getMapService", "responses": { "200": { - "description": "A successful response", + "description": "A MapEndpoint response", "schema": { "$ref": "#/definitions/MapEndpoint" } @@ -256,13 +346,19 @@ }, "/myapi/v1/mapsubclass": { "get": { - "operationId": "MyapiV1GetMapSubclass", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getMapSubclass", "responses": { "200": { - "description": "A successful response", + "description": "A Map_Boolean_Integer response", "schema": { - "$ref": "#/definitions/Map_Boolean_Integer" + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "int32" + } } } } @@ -270,16 +366,18 @@ } }, "definitions": { - "Map_Boolean_String": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "Map_Float_String": { + "Baz": { "type": "object", - "additionalProperties": { - "type": "string" + "properties": { + "foo": { + "$ref": "#/definitions/Foo" + }, + "foos": { + "type": "array", + "items": { + "$ref": "#/definitions/Foo" + } + } } }, "Foo": { @@ -294,122 +392,90 @@ } } }, - "Map_Integer_String": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "Map_TestEnum_String": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "Map_Boolean_Integer": { - "type": "object", - "additionalProperties": { - "type": "integer", - "format": "int32" - } - }, - "JsonMap": { - "type": "object" - }, - "Map_String_StringValue": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, "MapContainer": { "type": "object", "properties": { "stringMap": { - "$ref": "#/definitions/Map_String_StringValue" - } - } - }, - "Map_String_Integer": { - "type": "object", - "additionalProperties": { - "type": "integer", - "format": "int32" - } - }, - "Map_String_Baz": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/Baz" - } - }, - "Map_String_Map_String_Foo": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/Map_String_Foo" - } - }, - "Map_String_String": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "Baz": { - "type": "object", - "properties": { - "foo": { - "$ref": "#/definitions/Foo" - }, - "foos": { - "type": "array", - "items": { - "$ref": "#/definitions/Foo" + "type": "object", + "description": "A map of string values", + "additionalProperties": { + "type": "string" } } } }, - "Map_Long_String": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, "MapEndpoint": { "type": "object", "properties": { "bazMap": { - "$ref": "#/definitions/Map_String_Baz" + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Baz" + } }, "booleanKeyMap": { - "$ref": "#/definitions/Map_Boolean_String" + "type": "object", + "additionalProperties": { + "type": "string" + } }, "dateKeyMap": { - "$ref": "#/definitions/Map_DateTime_String" + "type": "object", + "additionalProperties": { + "type": "string" + } }, "dateTimeKeyMap": { - "$ref": "#/definitions/Map_DateTime_String" + "type": "object", + "additionalProperties": { + "type": "string" + } }, "enumKeyMap": { - "$ref": "#/definitions/Map_TestEnum_String" + "type": "object", + "additionalProperties": { + "type": "string" + } }, "floatKeyMap": { - "$ref": "#/definitions/Map_Float_String" + "type": "object", + "additionalProperties": { + "type": "string" + } }, "fooMap": { - "$ref": "#/definitions/Map_String_Foo" + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Foo" + } }, "fooMapMap": { - "$ref": "#/definitions/Map_String_Map_String_Foo" + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Foo" + } + } }, "intKeyMap": { - "$ref": "#/definitions/Map_Integer_String" + "type": "object", + "additionalProperties": { + "type": "string" + } }, "intMap": { - "$ref": "#/definitions/Map_String_Integer" + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "int32" + } }, "longKeyMap": { - "$ref": "#/definitions/Map_Long_String" + "type": "object", + "additionalProperties": { + "type": "string" + } }, "mapOfStrings": { "$ref": "#/definitions/MapContainer" @@ -418,34 +484,39 @@ "$ref": "#/definitions/MapEndpoint" }, "mapSubclass": { - "$ref": "#/definitions/Map_Boolean_Integer" + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "int32" + } }, "stringArrayMap": { - "$ref": "#/definitions/JsonMap" + "type": "object", + "additionalProperties": { + "type": "object", + "properties": {} + } }, "stringCollectionMap": { - "$ref": "#/definitions/JsonMap" + "type": "object", + "additionalProperties": { + "type": "object", + "properties": {} + } }, "stringMap": { - "$ref": "#/definitions/Map_String_String" + "type": "object", + "additionalProperties": { + "type": "string" + } }, "stringValueMap": { - "$ref": "#/definitions/Map_String_StringValue" + "type": "object", + "additionalProperties": { + "type": "string" + } } } - }, - "Map_String_Foo": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/Foo" - } - }, - "Map_DateTime_String": { - "type": "object", - "additionalProperties": { - "type": "string" - } } } } - diff --git a/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/map_endpoint_legacy.swagger b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/map_endpoint_legacy.swagger index 8d4cf4e1..99147501 100644 --- a/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/map_endpoint_legacy.swagger +++ b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/map_endpoint_legacy.swagger @@ -6,6 +6,11 @@ }, "host": "myapi.appspot.com", "basePath": "/_ah/api", + "tags": [ + { + "name": "myapi:v1" + } + ], "schemes": [ "https" ], @@ -18,13 +23,19 @@ "paths": { "/myapi/v1/getDateTimeKeyMap": { "get": { - "operationId": "MyapiV1GetDateTimeKeyMap", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getDateTimeKeyMap", "responses": { "200": { - "description": "A successful response", + "description": "A JsonMap response", "schema": { - "$ref": "#/definitions/JsonMap" + "type": "object", + "additionalProperties": { + "type": "object", + "properties": {} + } } } } @@ -32,11 +43,13 @@ }, "/myapi/v1/getMapOfStrings": { "get": { - "operationId": "MyapiV1GetMapOfStrings", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getMapOfStrings", "responses": { "200": { - "description": "A successful response", + "description": "A MapContainer response", "schema": { "$ref": "#/definitions/MapContainer" } @@ -46,13 +59,19 @@ }, "/myapi/v1/getStringCollectionMap": { "get": { - "operationId": "MyapiV1GetStringCollectionMap", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getStringCollectionMap", "responses": { "200": { - "description": "A successful response", + "description": "A JsonMap response", "schema": { - "$ref": "#/definitions/JsonMap" + "type": "object", + "additionalProperties": { + "type": "object", + "properties": {} + } } } } @@ -60,13 +79,19 @@ }, "/myapi/v1/map_boolean_string": { "get": { - "operationId": "MyapiV1GetBooleanKeyMap", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getBooleanKeyMap", "responses": { "200": { - "description": "A successful response", + "description": "A JsonMap response", "schema": { - "$ref": "#/definitions/JsonMap" + "type": "object", + "additionalProperties": { + "type": "object", + "properties": {} + } } } } @@ -74,13 +99,19 @@ }, "/myapi/v1/map_datetime_string": { "get": { - "operationId": "MyapiV1GetDateKeyMap", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getDateKeyMap", "responses": { "200": { - "description": "A successful response", + "description": "A JsonMap response", "schema": { - "$ref": "#/definitions/JsonMap" + "type": "object", + "additionalProperties": { + "type": "object", + "properties": {} + } } } } @@ -88,13 +119,19 @@ }, "/myapi/v1/map_float_string": { "get": { - "operationId": "MyapiV1GetFloatKeyMap", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getFloatKeyMap", "responses": { "200": { - "description": "A successful response", + "description": "A JsonMap response", "schema": { - "$ref": "#/definitions/JsonMap" + "type": "object", + "additionalProperties": { + "type": "object", + "properties": {} + } } } } @@ -102,13 +139,19 @@ }, "/myapi/v1/map_integer_string": { "get": { - "operationId": "MyapiV1GetIntKeyMap", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getIntKeyMap", "responses": { "200": { - "description": "A successful response", + "description": "A JsonMap response", "schema": { - "$ref": "#/definitions/JsonMap" + "type": "object", + "additionalProperties": { + "type": "object", + "properties": {} + } } } } @@ -116,13 +159,19 @@ }, "/myapi/v1/map_long_string": { "get": { - "operationId": "MyapiV1GetLongKeyMap", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getLongKeyMap", "responses": { "200": { - "description": "A successful response", + "description": "A JsonMap response", "schema": { - "$ref": "#/definitions/JsonMap" + "type": "object", + "additionalProperties": { + "type": "object", + "properties": {} + } } } } @@ -130,13 +179,19 @@ }, "/myapi/v1/map_string_baz": { "get": { - "operationId": "MyapiV1GetBazMap", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getBazMap", "responses": { "200": { - "description": "A successful response", + "description": "A JsonMap response", "schema": { - "$ref": "#/definitions/JsonMap" + "type": "object", + "additionalProperties": { + "type": "object", + "properties": {} + } } } } @@ -144,13 +199,19 @@ }, "/myapi/v1/map_string_foo": { "get": { - "operationId": "MyapiV1GetFooMap", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getFooMap", "responses": { "200": { - "description": "A successful response", + "description": "A JsonMap response", "schema": { - "$ref": "#/definitions/JsonMap" + "type": "object", + "additionalProperties": { + "type": "object", + "properties": {} + } } } } @@ -158,13 +219,19 @@ }, "/myapi/v1/map_string_integer": { "get": { - "operationId": "MyapiV1GetIntMap", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getIntMap", "responses": { "200": { - "description": "A successful response", + "description": "A JsonMap response", "schema": { - "$ref": "#/definitions/JsonMap" + "type": "object", + "additionalProperties": { + "type": "object", + "properties": {} + } } } } @@ -172,13 +239,19 @@ }, "/myapi/v1/map_string_map_string_foo": { "get": { - "operationId": "MyapiV1GetFooMapMap", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getFooMapMap", "responses": { "200": { - "description": "A successful response", + "description": "A JsonMap response", "schema": { - "$ref": "#/definitions/JsonMap" + "type": "object", + "additionalProperties": { + "type": "object", + "properties": {} + } } } } @@ -186,13 +259,19 @@ }, "/myapi/v1/map_string_string": { "get": { - "operationId": "MyapiV1GetStringMap", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getStringMap", "responses": { "200": { - "description": "A successful response", + "description": "A JsonMap response", "schema": { - "$ref": "#/definitions/JsonMap" + "type": "object", + "additionalProperties": { + "type": "object", + "properties": {} + } } } } @@ -200,13 +279,19 @@ }, "/myapi/v1/map_string_stringcollection": { "get": { - "operationId": "MyapiV1GetStringArrayMap", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getStringArrayMap", "responses": { "200": { - "description": "A successful response", + "description": "A JsonMap response", "schema": { - "$ref": "#/definitions/JsonMap" + "type": "object", + "additionalProperties": { + "type": "object", + "properties": {} + } } } } @@ -214,13 +299,19 @@ }, "/myapi/v1/map_string_stringvalue": { "get": { - "operationId": "MyapiV1GetStringValueMap", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getStringValueMap", "responses": { "200": { - "description": "A successful response", + "description": "A JsonMap response", "schema": { - "$ref": "#/definitions/JsonMap" + "type": "object", + "additionalProperties": { + "type": "object", + "properties": {} + } } } } @@ -228,13 +319,19 @@ }, "/myapi/v1/map_testenum_string": { "get": { - "operationId": "MyapiV1GetEnumKeyMap", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getEnumKeyMap", "responses": { "200": { - "description": "A successful response", + "description": "A JsonMap response", "schema": { - "$ref": "#/definitions/JsonMap" + "type": "object", + "additionalProperties": { + "type": "object", + "properties": {} + } } } } @@ -242,11 +339,13 @@ }, "/myapi/v1/mapendpoint": { "get": { - "operationId": "MyapiV1GetMapService", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getMapService", "responses": { "200": { - "description": "A successful response", + "description": "A MapEndpoint response", "schema": { "$ref": "#/definitions/MapEndpoint" } @@ -256,13 +355,19 @@ }, "/myapi/v1/mapsubclass": { "get": { - "operationId": "MyapiV1GetMapSubclass", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getMapSubclass", "responses": { "200": { - "description": "A successful response", + "description": "A JsonMap response", "schema": { - "$ref": "#/definitions/JsonMap" + "type": "object", + "additionalProperties": { + "type": "object", + "properties": {} + } } } } @@ -270,14 +375,16 @@ } }, "definitions": { - "JsonMap": { - "type": "object" - }, "MapContainer": { "type": "object", "properties": { "stringMap": { - "$ref": "#/definitions/JsonMap" + "type": "object", + "description": "A map of string values", + "additionalProperties": { + "type": "object", + "properties": {} + } } } }, @@ -285,37 +392,81 @@ "type": "object", "properties": { "bazMap": { - "$ref": "#/definitions/JsonMap" + "type": "object", + "additionalProperties": { + "type": "object", + "properties": {} + } }, "booleanKeyMap": { - "$ref": "#/definitions/JsonMap" + "type": "object", + "additionalProperties": { + "type": "object", + "properties": {} + } }, "dateKeyMap": { - "$ref": "#/definitions/JsonMap" + "type": "object", + "additionalProperties": { + "type": "object", + "properties": {} + } }, "dateTimeKeyMap": { - "$ref": "#/definitions/JsonMap" + "type": "object", + "additionalProperties": { + "type": "object", + "properties": {} + } }, "enumKeyMap": { - "$ref": "#/definitions/JsonMap" + "type": "object", + "additionalProperties": { + "type": "object", + "properties": {} + } }, "floatKeyMap": { - "$ref": "#/definitions/JsonMap" + "type": "object", + "additionalProperties": { + "type": "object", + "properties": {} + } }, "fooMap": { - "$ref": "#/definitions/JsonMap" + "type": "object", + "additionalProperties": { + "type": "object", + "properties": {} + } }, "fooMapMap": { - "$ref": "#/definitions/JsonMap" + "type": "object", + "additionalProperties": { + "type": "object", + "properties": {} + } }, "intKeyMap": { - "$ref": "#/definitions/JsonMap" + "type": "object", + "additionalProperties": { + "type": "object", + "properties": {} + } }, "intMap": { - "$ref": "#/definitions/JsonMap" + "type": "object", + "additionalProperties": { + "type": "object", + "properties": {} + } }, "longKeyMap": { - "$ref": "#/definitions/JsonMap" + "type": "object", + "additionalProperties": { + "type": "object", + "properties": {} + } }, "mapOfStrings": { "$ref": "#/definitions/MapContainer" @@ -324,19 +475,39 @@ "$ref": "#/definitions/MapEndpoint" }, "mapSubclass": { - "$ref": "#/definitions/JsonMap" + "type": "object", + "additionalProperties": { + "type": "object", + "properties": {} + } }, "stringArrayMap": { - "$ref": "#/definitions/JsonMap" + "type": "object", + "additionalProperties": { + "type": "object", + "properties": {} + } }, "stringCollectionMap": { - "$ref": "#/definitions/JsonMap" + "type": "object", + "additionalProperties": { + "type": "object", + "properties": {} + } }, "stringMap": { - "$ref": "#/definitions/JsonMap" + "type": "object", + "additionalProperties": { + "type": "object", + "properties": {} + } }, "stringValueMap": { - "$ref": "#/definitions/JsonMap" + "type": "object", + "additionalProperties": { + "type": "object", + "properties": {} + } } } } diff --git a/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/map_endpoint_with_array.swagger b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/map_endpoint_with_array.swagger index aa7e536b..4732533a 100644 --- a/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/map_endpoint_with_array.swagger +++ b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/map_endpoint_with_array.swagger @@ -6,6 +6,11 @@ }, "host": "myapi.appspot.com", "basePath": "/_ah/api", + "tags": [ + { + "name": "myapi:v1" + } + ], "schemes": [ "https" ], @@ -18,13 +23,18 @@ "paths": { "/myapi/v1/getDateTimeKeyMap": { "get": { - "operationId": "MyapiV1GetDateTimeKeyMap", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getDateTimeKeyMap", "responses": { "200": { - "description": "A successful response", + "description": "A Map_DateTime_String response", "schema": { - "$ref": "#/definitions/Map_DateTime_String" + "type": "object", + "additionalProperties": { + "type": "string" + } } } } @@ -32,11 +42,13 @@ }, "/myapi/v1/getMapOfStrings": { "get": { - "operationId": "MyapiV1GetMapOfStrings", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getMapOfStrings", "responses": { "200": { - "description": "A successful response", + "description": "A MapContainer response", "schema": { "$ref": "#/definitions/MapContainer" } @@ -46,13 +58,21 @@ }, "/myapi/v1/getStringCollectionMap": { "get": { - "operationId": "MyapiV1GetStringCollectionMap", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getStringCollectionMap", "responses": { "200": { - "description": "A successful response", + "description": "A Map_String_StringCollection response", "schema": { - "$ref": "#/definitions/Map_String_StringCollection" + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } } } } @@ -60,13 +80,18 @@ }, "/myapi/v1/map_boolean_string": { "get": { - "operationId": "MyapiV1GetBooleanKeyMap", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getBooleanKeyMap", "responses": { "200": { - "description": "A successful response", + "description": "A Map_Boolean_String response", "schema": { - "$ref": "#/definitions/Map_Boolean_String" + "type": "object", + "additionalProperties": { + "type": "string" + } } } } @@ -74,13 +99,18 @@ }, "/myapi/v1/map_datetime_string": { "get": { - "operationId": "MyapiV1GetDateKeyMap", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getDateKeyMap", "responses": { "200": { - "description": "A successful response", + "description": "A Map_DateTime_String response", "schema": { - "$ref": "#/definitions/Map_DateTime_String" + "type": "object", + "additionalProperties": { + "type": "string" + } } } } @@ -88,13 +118,18 @@ }, "/myapi/v1/map_float_string": { "get": { - "operationId": "MyapiV1GetFloatKeyMap", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getFloatKeyMap", "responses": { "200": { - "description": "A successful response", + "description": "A Map_Float_String response", "schema": { - "$ref": "#/definitions/Map_Float_String" + "type": "object", + "additionalProperties": { + "type": "string" + } } } } @@ -102,13 +137,18 @@ }, "/myapi/v1/map_integer_string": { "get": { - "operationId": "MyapiV1GetIntKeyMap", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getIntKeyMap", "responses": { "200": { - "description": "A successful response", + "description": "A Map_Integer_String response", "schema": { - "$ref": "#/definitions/Map_Integer_String" + "type": "object", + "additionalProperties": { + "type": "string" + } } } } @@ -116,13 +156,18 @@ }, "/myapi/v1/map_long_string": { "get": { - "operationId": "MyapiV1GetLongKeyMap", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getLongKeyMap", "responses": { "200": { - "description": "A successful response", + "description": "A Map_Long_String response", "schema": { - "$ref": "#/definitions/Map_Long_String" + "type": "object", + "additionalProperties": { + "type": "string" + } } } } @@ -130,13 +175,18 @@ }, "/myapi/v1/map_string_baz": { "get": { - "operationId": "MyapiV1GetBazMap", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getBazMap", "responses": { "200": { - "description": "A successful response", + "description": "A Map_String_Baz response", "schema": { - "$ref": "#/definitions/Map_String_Baz" + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Baz" + } } } } @@ -144,13 +194,18 @@ }, "/myapi/v1/map_string_foo": { "get": { - "operationId": "MyapiV1GetFooMap", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getFooMap", "responses": { "200": { - "description": "A successful response", + "description": "A Map_String_Foo response", "schema": { - "$ref": "#/definitions/Map_String_Foo" + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Foo" + } } } } @@ -158,13 +213,19 @@ }, "/myapi/v1/map_string_integer": { "get": { - "operationId": "MyapiV1GetIntMap", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getIntMap", "responses": { "200": { - "description": "A successful response", + "description": "A Map_String_Integer response", "schema": { - "$ref": "#/definitions/Map_String_Integer" + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "int32" + } } } } @@ -172,13 +233,21 @@ }, "/myapi/v1/map_string_map_string_foo": { "get": { - "operationId": "MyapiV1GetFooMapMap", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getFooMapMap", "responses": { "200": { - "description": "A successful response", + "description": "A Map_String_Map_String_Foo response", "schema": { - "$ref": "#/definitions/Map_String_Map_String_Foo" + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Foo" + } + } } } } @@ -186,13 +255,18 @@ }, "/myapi/v1/map_string_string": { "get": { - "operationId": "MyapiV1GetStringMap", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getStringMap", "responses": { "200": { - "description": "A successful response", + "description": "A Map_String_String response", "schema": { - "$ref": "#/definitions/Map_String_String" + "type": "object", + "additionalProperties": { + "type": "string" + } } } } @@ -200,13 +274,21 @@ }, "/myapi/v1/map_string_stringcollection": { "get": { - "operationId": "MyapiV1GetStringArrayMap", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getStringArrayMap", "responses": { "200": { - "description": "A successful response", + "description": "A Map_String_StringCollection response", "schema": { - "$ref": "#/definitions/Map_String_StringCollection" + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } } } } @@ -214,13 +296,18 @@ }, "/myapi/v1/map_string_stringvalue": { "get": { - "operationId": "MyapiV1GetStringValueMap", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getStringValueMap", "responses": { "200": { - "description": "A successful response", + "description": "A Map_String_StringValue response", "schema": { - "$ref": "#/definitions/Map_String_StringValue" + "type": "object", + "additionalProperties": { + "type": "string" + } } } } @@ -228,13 +315,18 @@ }, "/myapi/v1/map_testenum_string": { "get": { - "operationId": "MyapiV1GetEnumKeyMap", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getEnumKeyMap", "responses": { "200": { - "description": "A successful response", + "description": "A Map_TestEnum_String response", "schema": { - "$ref": "#/definitions/Map_TestEnum_String" + "type": "object", + "additionalProperties": { + "type": "string" + } } } } @@ -242,11 +334,13 @@ }, "/myapi/v1/mapendpoint": { "get": { - "operationId": "MyapiV1GetMapService", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getMapService", "responses": { "200": { - "description": "A successful response", + "description": "A MapEndpoint response", "schema": { "$ref": "#/definitions/MapEndpoint" } @@ -256,13 +350,19 @@ }, "/myapi/v1/mapsubclass": { "get": { - "operationId": "MyapiV1GetMapSubclass", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.getMapSubclass", "responses": { "200": { - "description": "A successful response", + "description": "A Map_Boolean_Integer response", "schema": { - "$ref": "#/definitions/Map_Boolean_Integer" + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "int32" + } } } } @@ -270,16 +370,18 @@ } }, "definitions": { - "Map_Boolean_String": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "Map_Float_String": { + "Baz": { "type": "object", - "additionalProperties": { - "type": "string" + "properties": { + "foo": { + "$ref": "#/definitions/Foo" + }, + "foos": { + "type": "array", + "items": { + "$ref": "#/definitions/Foo" + } + } } }, "Foo": { @@ -294,128 +396,90 @@ } } }, - "Map_Integer_String": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "Map_TestEnum_String": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "Map_Boolean_Integer": { - "type": "object", - "additionalProperties": { - "type": "integer", - "format": "int32" - } - }, - "Map_String_StringValue": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, "MapContainer": { "type": "object", "properties": { "stringMap": { - "$ref": "#/definitions/Map_String_StringValue" - } - } - }, - "Map_String_Integer": { - "type": "object", - "additionalProperties": { - "type": "integer", - "format": "int32" - } - }, - "Map_String_Baz": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/Baz" - } - }, - "Map_String_Map_String_Foo": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/Map_String_Foo" - } - }, - "Map_String_String": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "Baz": { - "type": "object", - "properties": { - "foo": { - "$ref": "#/definitions/Foo" - }, - "foos": { - "type": "array", - "items": { - "$ref": "#/definitions/Foo" + "type": "object", + "description": "A map of string values", + "additionalProperties": { + "type": "string" } } } }, - "Map_String_StringCollection": { - "type": "object", - "additionalProperties": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "Map_Long_String": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, "MapEndpoint": { "type": "object", "properties": { "bazMap": { - "$ref": "#/definitions/Map_String_Baz" + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Baz" + } }, "booleanKeyMap": { - "$ref": "#/definitions/Map_Boolean_String" + "type": "object", + "additionalProperties": { + "type": "string" + } }, "dateKeyMap": { - "$ref": "#/definitions/Map_DateTime_String" + "type": "object", + "additionalProperties": { + "type": "string" + } }, "dateTimeKeyMap": { - "$ref": "#/definitions/Map_DateTime_String" + "type": "object", + "additionalProperties": { + "type": "string" + } }, "enumKeyMap": { - "$ref": "#/definitions/Map_TestEnum_String" + "type": "object", + "additionalProperties": { + "type": "string" + } }, "floatKeyMap": { - "$ref": "#/definitions/Map_Float_String" + "type": "object", + "additionalProperties": { + "type": "string" + } }, "fooMap": { - "$ref": "#/definitions/Map_String_Foo" + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Foo" + } }, "fooMapMap": { - "$ref": "#/definitions/Map_String_Map_String_Foo" + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Foo" + } + } }, "intKeyMap": { - "$ref": "#/definitions/Map_Integer_String" + "type": "object", + "additionalProperties": { + "type": "string" + } }, "intMap": { - "$ref": "#/definitions/Map_String_Integer" + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "int32" + } }, "longKeyMap": { - "$ref": "#/definitions/Map_Long_String" + "type": "object", + "additionalProperties": { + "type": "string" + } }, "mapOfStrings": { "$ref": "#/definitions/MapContainer" @@ -424,33 +488,43 @@ "$ref": "#/definitions/MapEndpoint" }, "mapSubclass": { - "$ref": "#/definitions/Map_Boolean_Integer" + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "int32" + } }, "stringArrayMap": { - "$ref": "#/definitions/Map_String_StringCollection" + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } }, "stringCollectionMap": { - "$ref": "#/definitions/Map_String_StringCollection" + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } }, "stringMap": { - "$ref": "#/definitions/Map_String_String" + "type": "object", + "additionalProperties": { + "type": "string" + } }, "stringValueMap": { - "$ref": "#/definitions/Map_String_StringValue" + "type": "object", + "additionalProperties": { + "type": "string" + } } } - }, - "Map_String_Foo": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/Foo" - } - }, - "Map_DateTime_String": { - "type": "object", - "additionalProperties": { - "type": "string" - } } } } diff --git a/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/multi_resource_endpoint.swagger b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/multi_resource_endpoint.swagger index f207c1c2..cfdfaa25 100644 --- a/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/multi_resource_endpoint.swagger +++ b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/multi_resource_endpoint.swagger @@ -6,6 +6,17 @@ }, "host": "swagger-test.appspot.com", "basePath": "/api", + "tags": [ + { + "name": "multiresource:v1" + }, + { + "name": "multiresource:v1.Resource1" + }, + { + "name": "multiresource:v1.Resource2" + } + ], "schemes": [ "https" ], @@ -18,11 +29,13 @@ "paths": { "/multiresource/v1/noresource": { "get": { - "operationId": "MultiresourceV1Get", - "parameters": [], + "tags": [ + "multiresource:v1" + ], + "operationId": "multiresource:v1.get", "responses": { "200": { - "description": "A successful response", + "description": "A Foo response", "schema": { "$ref": "#/definitions/Foo" } @@ -32,11 +45,13 @@ }, "/multiresource/v1/resource1": { "get": { - "operationId": "MultiresourceV1Resource1Get", - "parameters": [], + "tags": [ + "multiresource:v1.Resource1" + ], + "operationId": "multiresource:v1.Resource1.get", "responses": { "200": { - "description": "A successful response", + "description": "A Foo response", "schema": { "$ref": "#/definitions/Foo" } @@ -46,11 +61,13 @@ }, "/multiresource/v1/resource2": { "get": { - "operationId": "MultiresourceV1Resource2Get", - "parameters": [], + "tags": [ + "multiresource:v1.Resource2" + ], + "operationId": "multiresource:v1.Resource2.get", "responses": { "200": { - "description": "A successful response", + "description": "A Foo response", "schema": { "$ref": "#/definitions/Foo" } diff --git a/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/multi_version_endpoint.swagger b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/multi_version_endpoint.swagger index ec4900b6..fb59a521 100644 --- a/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/multi_version_endpoint.swagger +++ b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/multi_version_endpoint.swagger @@ -6,6 +6,14 @@ }, "host": "swagger-test.appspot.com", "basePath": "/api", + "tags": [ + { + "name": "myapi:v1" + }, + { + "name": "myapi:v2" + } + ], "schemes": [ "https" ], @@ -18,11 +26,13 @@ "paths": { "/myapi/v1/foo": { "get": { - "operationId": "MyapiV1Get", - "parameters": [], + "tags": [ + "myapi:v1" + ], + "operationId": "myapi:v1.get", "responses": { "200": { - "description": "A successful response", + "description": "A Foo response", "schema": { "$ref": "#/definitions/Foo" } @@ -32,11 +42,13 @@ }, "/myapi/v2/foo": { "get": { - "operationId": "MyapiV2Get", - "parameters": [], + "tags": [ + "myapi:v2" + ], + "operationId": "myapi:v2.get", "responses": { "200": { - "description": "A successful response", + "description": "A Foo response", "schema": { "$ref": "#/definitions/Foo" } diff --git a/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/multiple_scopes.swagger b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/multiple_scopes.swagger new file mode 100644 index 00000000..69938e1c --- /dev/null +++ b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/multiple_scopes.swagger @@ -0,0 +1,175 @@ +{ + "swagger": "2.0", + "info": { + "version": "1.0.0", + "title": "swagger-test.appspot.com" + }, + "host": "swagger-test.appspot.com", + "basePath": "/api", + "tags": [ + { + "name": "multipleScopes:v1" + } + ], + "schemes": [ + "https" + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "paths": { + "/multipleScopes/v1/noOverride": { + "post": { + "tags": [ + "multipleScopes:v1" + ], + "operationId": "multipleScopes:v1.noOverride", + "responses": { + "204": { + "description": "A successful response" + } + }, + "security": [ + { + "google_id_token-3a26ea04": [ + "https://mail.google.com/" + ] + }, + { + "google_id_token_legacy-3a26ea04": [ + "https://mail.google.com/" + ] + } + ] + } + }, + "/multipleScopes/v1/overrideAudience": { + "post": { + "tags": [ + "multipleScopes:v1" + ], + "operationId": "multipleScopes:v1.overrideAudience", + "responses": { + "204": { + "description": "A successful response" + } + }, + "security": [ + { + "google_id_token-ab656ae": [ + "https://mail.google.com/" + ] + }, + { + "google_id_token_legacy-ab656ae": [ + "https://mail.google.com/" + ] + } + ] + } + }, + "/multipleScopes/v1/scopeOverride": { + "post": { + "tags": [ + "multipleScopes:v1" + ], + "operationId": "multipleScopes:v1.scopeOverride", + "responses": { + "204": { + "description": "A successful response" + } + }, + "security": [ + { + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + }, + { + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + } + ] + } + }, + "/multipleScopes/v1/unknownScope": { + "post": { + "tags": [ + "multipleScopes:v1" + ], + "operationId": "multipleScopes:v1.unknownScope", + "responses": { + "204": { + "description": "A successful response" + } + }, + "security": [ + { + "google_id_token-3a26ea04": [ + "unknownScope" + ] + }, + { + "google_id_token_legacy-3a26ea04": [ + "unknownScope" + ] + } + ] + } + } + }, + "securityDefinitions": { + "google_id_token-3a26ea04": { + "type": "oauth2", + "authorizationUrl": "https://accounts.google.com/o/oauth2/v2/auth", + "flow": "implicit", + "scopes": { + "https://www.googleapis.com/auth/userinfo.email": "View your email address", + "https://mail.google.com/": "Read, send, delete, and manage your email", + "unknownScope": "unknownScope" + }, + "x-google-issuer": "https://accounts.google.com", + "x-google-jwks_uri": "https://www.googleapis.com/oauth2/v1/certs", + "x-google-audiences": "audience" + }, + "google_id_token-ab656ae": { + "type": "oauth2", + "authorizationUrl": "https://accounts.google.com/o/oauth2/v2/auth", + "flow": "implicit", + "scopes": { + "https://mail.google.com/": "Read, send, delete, and manage your email" + }, + "x-google-issuer": "https://accounts.google.com", + "x-google-jwks_uri": "https://www.googleapis.com/oauth2/v1/certs", + "x-google-audiences": "audience2" + }, + "google_id_token_legacy-3a26ea04": { + "type": "oauth2", + "authorizationUrl": "https://accounts.google.com/o/oauth2/v2/auth", + "flow": "implicit", + "scopes": { + "https://www.googleapis.com/auth/userinfo.email": "View your email address", + "https://mail.google.com/": "Read, send, delete, and manage your email", + "unknownScope": "unknownScope" + }, + "x-google-issuer": "accounts.google.com", + "x-google-jwks_uri": "https://www.googleapis.com/oauth2/v1/certs", + "x-google-audiences": "audience" + }, + "google_id_token_legacy-ab656ae": { + "type": "oauth2", + "authorizationUrl": "https://accounts.google.com/o/oauth2/v2/auth", + "flow": "implicit", + "scopes": { + "https://mail.google.com/": "Read, send, delete, and manage your email" + }, + "x-google-issuer": "accounts.google.com", + "x-google-jwks_uri": "https://www.googleapis.com/oauth2/v1/certs", + "x-google-audiences": "audience2" + } + } +} diff --git a/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/optional_endpoint.swagger b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/optional_endpoint.swagger new file mode 100644 index 00000000..a54be4f9 --- /dev/null +++ b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/optional_endpoint.swagger @@ -0,0 +1,109 @@ +{ + "swagger" : "2.0", + "info" : { + "version" : "1.0.0", + "title" : "swagger-test.appspot.com" + }, + "host" : "swagger-test.appspot.com", + "basePath" : "/api", + "tags" : [ { + "name" : "myapi:v1" + } ], + "schemes" : [ "https" ], + "consumes" : [ "application/json" ], + "produces" : [ "application/json" ], + "paths" : { + "/myapi/v1/optionalresults" : { + "get" : { + "tags" : [ "myapi:v1" ], + "operationId" : "myapi:v1.getResult", + "responses" : { + "200" : { + "description" : "A OptionalResults response", + "schema" : { + "$ref" : "#/definitions/OptionalResults" + } + } + } + } + } + }, + "definitions" : { + "Foo" : { + "type" : "object", + "properties" : { + "name" : { + "type" : "string" + }, + "value" : { + "type" : "integer", + "format" : "int32" + } + } + }, + "OptionalResults" : { + "type" : "object", + "properties" : { + "enums" : { + "type" : "array", + "items" : { + "type" : "string", + "enum" : [ "VALUE1", "value_2" ] + } + }, + "foos" : { + "type" : "array", + "items" : { + "$ref" : "#/definitions/Foo" + } + }, + "optionalDate" : { + "type" : "string", + "format" : "date-time" + }, + "optionalDouble" : { + "type" : "number", + "format" : "double" + }, + "optionalDoubleObject" : { + "type" : "number", + "format" : "double" + }, + "optionalEnum" : { + "type" : "string", + "enum" : [ "VALUE1", "value_2" ] + }, + "optionalFloatObject" : { + "type" : "number", + "format" : "float" + }, + "optionalFoo" : { + "$ref" : "#/definitions/Foo" + }, + "optionalInt" : { + "type" : "integer", + "format" : "int32" + }, + "optionalInteger" : { + "type" : "integer", + "format" : "int32" + }, + "optionalLong" : { + "type" : "integer", + "format" : "int64" + }, + "optionalLongObject" : { + "type" : "integer", + "format" : "int64" + }, + "optionalSimpleDate" : { + "type" : "string", + "format" : "date" + }, + "optionalString" : { + "type" : "string" + } + } + } + } +} \ No newline at end of file diff --git a/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/required_parameters_endpoint.swagger b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/required_parameters_endpoint.swagger new file mode 100644 index 00000000..3fa64dab --- /dev/null +++ b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/required_parameters_endpoint.swagger @@ -0,0 +1,81 @@ +{ + "swagger": "2.0", + "info": { + "version": "1.0.0", + "title": "swagger-test.appspot.com" + }, + "host": "swagger-test.appspot.com", + "basePath": "/api", + "tags": [ + { + "name": "requiredProperties:v1" + } + ], + "schemes": [ + "https" + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "paths": { + "/requiredProperties/v1/requiredproperties": { + "get": { + "tags": [ + "requiredProperties:v1" + ], + "operationId": "requiredProperties:v1.getRequiredProperties", + "responses": { + "200": { + "description": "A RequiredProperties response", + "schema": { + "$ref": "#/definitions/RequiredProperties" + } + } + } + } + } + }, + "definitions": { + "RequiredProperties": { + "type": "object", + "required": [ + "apiResourceProperty_required", + "nonnull", + "priority1", + "priority2" + ], + "properties": { + "apiResourceProperty_not_required": { + "type": "string" + }, + "apiResourceProperty_required": { + "type": "string" + }, + "apiResourceProperty_undefined": { + "type": "string" + }, + "nonnull": { + "type": "string" + }, + "nullable": { + "type": "string" + }, + "priority1": { + "type": "string" + }, + "priority2": { + "type": "string" + }, + "priority3": { + "type": "string" + }, + "undefined": { + "type": "string" + } + } + } + } +} diff --git a/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/response_status.swagger b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/response_status.swagger new file mode 100644 index 00000000..665fa3bc --- /dev/null +++ b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/response_status.swagger @@ -0,0 +1,79 @@ +{ + "swagger": "2.0", + "info": { + "version": "1.0.0", + "title": "swagger-test.appspot.com" + }, + "host": "swagger-test.appspot.com", + "basePath": "/api", + "tags": [ + { + "name" : "responseStatus:v1" + } + ], + "schemes" : [ "https" ], + "consumes" : [ "application/json" ], + "produces" : [ "application/json" ], + "paths" : { + "/responseStatus/v1/responseStatusCreatedReturnString" : { + "post" : { + "tags" : [ "responseStatus:v1" ], + "operationId" : "responseStatus:v1.responseStatusCreatedReturnString", + "responses" : { + "201" : { + "description" : "A StringValue response", + "schema" : { + "$ref" : "#/definitions/StringValue" + } + } + } + } + }, + "/responseStatus/v1/responseStatusCreatedReturnVoid" : { + "post" : { + "tags" : [ "responseStatus:v1" ], + "operationId" : "responseStatus:v1.responseStatusCreatedReturnVoid", + "responses" : { + "201" : { + "description" : "A successful response" + } + } + } + }, + "/responseStatus/v1/responseStatusUnsetReturnString" : { + "post" : { + "tags" : [ "responseStatus:v1" ], + "operationId" : "responseStatus:v1.responseStatusUnsetReturnString", + "responses" : { + "200" : { + "description" : "A StringValue response", + "schema" : { + "$ref" : "#/definitions/StringValue" + } + } + } + } + }, + "/responseStatus/v1/responseStatusUnsetReturnVoid" : { + "post" : { + "tags" : [ "responseStatus:v1" ], + "operationId" : "responseStatus:v1.responseStatusUnsetReturnVoid", + "responses" : { + "204" : { + "description" : "A successful response" + } + } + } + } + }, + "definitions" : { + "StringValue" : { + "type" : "object", + "properties" : { + "value" : { + "type" : "string" + } + } + } + } +} diff --git a/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/special_chars.swagger b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/special_chars.swagger new file mode 100644 index 00000000..6844b429 --- /dev/null +++ b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/special_chars.swagger @@ -0,0 +1,92 @@ +{ + "swagger": "2.0", + "info": { + "version": "1.0.0", + "title": "swagger-test.appspot.com" + }, + "host": "swagger-test.appspot.com", + "basePath": "/api", + "tags": [ + { + "name": "specialChars:v1" + } + ], + "schemes": [ + "https" + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "paths": { + "/specialChars/v1/paramSpecialChar1": { + "post": { + "tags": [ + "specialChars:v1" + ], + "operationId": "specialChars:v1.paramSpecialChar1", + "parameters": [ + { + "$ref": "#/parameters/%C2%B5_query_parameter" + }, + { + "in": "body", + "name": "Requestù", + "required": true, + "schema": { + "$ref": "#/definitions/Request%C3%B9" + } + } + ], + "responses": { + "200": { + "description": "A Responseµ response", + "schema": { + "$ref": "#/definitions/Response%C2%B5" + } + } + } + } + }, + "/specialChars/v1/paramSpecialChar2": { + "post": { + "tags": [ + "specialChars:v1" + ], + "operationId": "specialChars:v1.paramSpecialChar2", + "parameters": [ + { + "$ref": "#/parameters/%C2%B5_query_parameter" + } + ], + "responses": { + "200": { + "description": "A Responseµ response", + "schema": { + "$ref": "#/definitions/Response%C2%B5" + } + } + } + } + } + }, + "definitions": { + "Requestù": { + "type": "object" + }, + "Responseµ": { + "type": "object" + } + }, + "parameters": { + "µ_query_parameter": { + "name": "µ", + "in": "query", + "required": false, + "type": "integer", + "format": "int32" + } + } +} diff --git a/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/third_party_auth.swagger b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/third_party_auth.swagger index ce360959..2450b4c7 100644 --- a/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/third_party_auth.swagger +++ b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/third_party_auth.swagger @@ -6,6 +6,11 @@ }, "host": "swagger-test.appspot.com", "basePath": "/api", + "tags": [ + { + "name": "thirdparty:v1" + } + ], "schemes": [ "https" ], @@ -18,10 +23,12 @@ "paths": { "/thirdparty/v1/authOverride": { "post": { - "operationId": "ThirdpartyV1AuthOverride", - "parameters": [], + "tags": [ + "thirdparty:v1" + ], + "operationId": "thirdparty:v1.authOverride", "responses": { - "200": { + "204": { "description": "A successful response" } }, @@ -34,10 +41,12 @@ }, "/thirdparty/v1/noOverride": { "post": { - "operationId": "ThirdpartyV1NoOverride", - "parameters": [], + "tags": [ + "thirdparty:v1" + ], + "operationId": "thirdparty:v1.noOverride", "responses": { - "200": { + "204": { "description": "A successful response" } }, diff --git a/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/validation_endpoint.swagger b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/validation_endpoint.swagger new file mode 100644 index 00000000..af383fcc --- /dev/null +++ b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/validation_endpoint.swagger @@ -0,0 +1,136 @@ +{ + "swagger": "2.0", + "info": { + "version": "1.0.0", + "title": "myapi.appspot.com" + }, + "host": "myapi.appspot.com", + "basePath": "/_ah/api", + "tags": [ + { + "name": "validation:v1" + } + ], + "schemes": [ + "https" + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "paths": { + "/validation/v1/{pathParam}": { + "post": { + "tags": [ + "validation:v1" + ], + "operationId": "validation:v1.create", + "parameters": [ + { + "name": "pathParam", + "in": "path", + "required": true, + "type": "string", + "pattern": "^\\d+$" + }, + { + "name": "queryParam", + "in": "query", + "required": true, + "type": "string", + "pattern": "^[a-z]{2}$" + }, + { + "name" : "minMaxParam", + "in" : "query", + "required" : true, + "type" : "integer", + "format" : "int64", + "minimum": 10, + "maximum": 20 + }, + { + "name" : "decimalMinMaxParam", + "in" : "query", + "required" : true, + "type" : "number", + "format" : "double", + "minimum": 2.3, + "exclusiveMinimum" : true, + "maximum": 4, + "exclusiveMaximum" : false + }, + { + "name" : "sizeParam", + "in" : "query", + "required" : true, + "type" : "string", + "minLength": 3, + "maxLength" : 6 + }, + { + "name" : "arraySizeParam", + "in" : "query", + "required" : false, + "type" : "array", + "items" : { + "type" : "string" + }, + "collectionFormat" : "multi", + "maxItems" : 3, + "minItems" : 2 + }, + { + "in" : "body", + "name" : "ValidationBean", + "required" : true, + "schema" : { + "$ref" : "#/definitions/ValidationBean" + } + } + ], + "responses": { + "204": { + "description" : "A successful response" + } + } + } + } + }, + "definitions" : { + "ValidationBean" : { + "type" : "object", + "properties" : { + "arrayTest" : { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 3 + }, + "decimalMinMaxTest" : { + "type" : "number", + "format" : "double", + "minimum": 3.4, + "exclusiveMinimum" : true, + "maximum": 4.5, + "exclusiveMaximum" : false + }, + "minMaxTest" : { + "type" : "integer", + "format" : "int64", + "minimum": 2, + "maximum": 6 + }, + "myPatternTest" : { + "type" : "string", + "pattern": "^[0-9]{2}$", + "minLength": 2, + "maxLength": 2 + } + } + } + } +} diff --git a/gradle.properties b/gradle.properties index 11845479..c5b5cd08 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,25 +1,32 @@ -version=2.2.1 - -sourceCompatibility=1.7 -targetCompatibility=1.7 -group=com.google.endpoints +version=2.5.10-SNAPSHOT +sourceCompatibility=1.8 +targetCompatibility=1.8 +group=com.aodocs.endpoints servletVersion=2.5 javaxinjectVersion=1 -guavaVersion=20.0 -jacksonVersion=2.9.6 +autoValueVersion=1.8.2 +guavaVersion=28.1-jre +jacksonVersion=2.12.5 gradleAppenginePluginVersion=1.9.59 -appengineVersion=1.9.60 -apiclientVersion=1.25.0 -fileUploadVersion=1.3.3 -findbugsVersion=3.0.1 -swaggerVersion=1.5.9 -slf4jVersion=1.7.21 -guiceVersion=4.0 -objectifyVersion=5.1.21 -floggerVersion=0.3.1 +appengineVersion=1.9.91 +httpClientVersion=1.40.0 +apiclientVersion=1.32.1 +fileUploadVersion=1.4 +findbugsVersion=3.0.2 +swaggerVersion=1.6.2 +slf4jVersion=1.7.32 +guiceVersion=5.0.1 +objectifyVersion=5.1.24 +floggerVersion=0.6 +hibernateValidatorVersion=7.0.5.Final +validationApiVersion=3.0.2 + +junitVersion=4.13.2 +mockitoVersion=3.6.28 +jsonassertVersion=1.5.0 +truthVersion=1.1.3 +springtestVersion=3.2.18.RELEASE -junitVersion=4.12 -mockitoVersion=1.10.19 -truthVersion=0.28 -springtestVersion=3.2.16.RELEASE +#enable this to get detailed warnings for Gradle +#org.gradle.warning.mode=all diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index c519bfa9..cb447b5f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Wed Aug 03 14:48:55 PDT 2016 +#Wed Aug 21 16:54:33 CEST 2019 +distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-bin.zip +zipStoreBase=GRADLE_USER_HOME diff --git a/settings.gradle b/settings.gradle index b3db3bba..a663f75e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -include ':endpoints-framework', 'endpoints-framework-all', ':endpoints-framework-tools', ':endpoints-framework-guice', ':test-utils', ':discovery-client', ':test-compat', ':test-compat:legacy-app', ':test-compat:new-app', ':test-compat:new-app-guice' +include ':endpoints-framework', 'endpoints-framework-all', ':endpoints-framework-tools', ':endpoints-framework-guice', ':test-utils', ':discovery-client' diff --git a/test-compat/build.gradle b/test-compat/build.gradle index 819725cb..102176a1 100644 --- a/test-compat/build.gradle +++ b/test-compat/build.gradle @@ -127,21 +127,13 @@ dependencies { compile group: 'javax.servlet', name: 'servlet-api', version: servletVersion compileOnly group: 'com.google.appengine', name: 'appengine-endpoints', version: appengineVersion - testlibCompile(group: 'com.compat_tests', name: 'wax', version: 'v1-+') { - exclude group: 'com.google.guava', module: 'guava-jdk5' - } - testlibCompile(group: 'com.compat_tests', name: 'tictactoe', version: 'v1-+') { - exclude group: 'com.google.guava', module: 'guava-jdk5' - } + testlibCompile(group: 'com.compat_tests', name: 'wax', version: 'v1-+') + testlibCompile(group: 'com.compat_tests', name: 'tictactoe', version: 'v1-+') testlibCompile group: 'junit', name: 'junit', version: junitVersion testlibCompile group: 'com.google.truth', name: 'truth', version: truthVersion testlibCompile group: 'javax.servlet', name: 'servlet-api', version: servletVersion - testlibCompile(group: 'com.google.http-client', name: 'google-http-client-jackson', version: apiclientVersion) { - exclude group: 'com.google.guava', module: 'guava-jdk5' - } - testlibCompile(group: 'com.google.api-client', name: 'google-api-client', version: apiclientVersion) { - exclude group: 'com.google.guava', module: 'guava-jdk5' - } + testlibCompile(group: 'com.google.http-client', name: 'google-http-client-jackson', version: httpClientVersion) + testlibCompile(group: 'com.google.api-client', name: 'google-api-client', version: apiclientVersion) appengineSdk "com.google.appengine:appengine-java-sdk:${appengineVersion}" } diff --git a/test-compat/legacy-app-guice/build.gradle b/test-compat/legacy-app-guice/build.gradle index 058ec81d..fedad4ea 100644 --- a/test-compat/legacy-app-guice/build.gradle +++ b/test-compat/legacy-app-guice/build.gradle @@ -15,8 +15,8 @@ */ dependencies { - compile project(':endpoints-framework-guice') - compile project(':endpoints-framework') - compile project(':test-compat') + implementation project(':endpoints-framework-guice') + implementation project(':endpoints-framework') + implementation project(':test-compat') appengineSdk "com.google.appengine:appengine-java-sdk:${appengineVersion}" } diff --git a/test-compat/legacy-app/build.gradle b/test-compat/legacy-app/build.gradle index 26f54965..b7a3a254 100644 --- a/test-compat/legacy-app/build.gradle +++ b/test-compat/legacy-app/build.gradle @@ -15,7 +15,7 @@ */ dependencies { - compile project(':test-compat') + implementation project(':test-compat') appengineSdk "com.google.appengine:appengine-java-sdk:${appengineVersion}" - compile group: 'com.google.appengine', name: 'appengine-endpoints', version: appengineVersion + implementation group: 'com.google.appengine', name: 'appengine-endpoints', version: appengineVersion } diff --git a/test-compat/new-app-guice/build.gradle b/test-compat/new-app-guice/build.gradle index 058ec81d..fedad4ea 100644 --- a/test-compat/new-app-guice/build.gradle +++ b/test-compat/new-app-guice/build.gradle @@ -15,8 +15,8 @@ */ dependencies { - compile project(':endpoints-framework-guice') - compile project(':endpoints-framework') - compile project(':test-compat') + implementation project(':endpoints-framework-guice') + implementation project(':endpoints-framework') + implementation project(':test-compat') appengineSdk "com.google.appengine:appengine-java-sdk:${appengineVersion}" } diff --git a/test-compat/new-app/build.gradle b/test-compat/new-app/build.gradle index 41580d07..26f60547 100644 --- a/test-compat/new-app/build.gradle +++ b/test-compat/new-app/build.gradle @@ -15,7 +15,7 @@ */ dependencies { - compile project(':endpoints-framework') - compile project(':test-compat') + implementation project(':endpoints-framework') + implementation project(':test-compat') appengineSdk "com.google.appengine:appengine-java-sdk:${appengineVersion}" } diff --git a/test-utils/build.gradle b/test-utils/build.gradle index 7841286f..f16cd361 100644 --- a/test-utils/build.gradle +++ b/test-utils/build.gradle @@ -14,9 +14,9 @@ * limitations under the License. */ dependencies { - compile project(':endpoints-framework') - compile group: 'javax.inject', name: 'javax.inject', version: javaxinjectVersion - compile group: 'junit', name: 'junit', version: junitVersion - compile group: 'org.mockito', name: 'mockito-core', version: mockitoVersion - compile group: 'com.google.truth', name: 'truth', version: truthVersion + implementation project(':endpoints-framework') + implementation group: 'javax.inject', name: 'javax.inject', version: javaxinjectVersion + implementation group: 'junit', name: 'junit', version: junitVersion + implementation group: 'org.mockito', name: 'mockito-core', version: mockitoVersion + implementation group: 'com.google.truth', name: 'truth', version: truthVersion } diff --git a/test-utils/src/main/java/com/google/api/server/spi/BaseSystemServiceTest.java b/test-utils/src/main/java/com/google/api/server/spi/BaseSystemServiceTest.java index d4c61670..d5b818ec 100644 --- a/test-utils/src/main/java/com/google/api/server/spi/BaseSystemServiceTest.java +++ b/test-utils/src/main/java/com/google/api/server/spi/BaseSystemServiceTest.java @@ -18,7 +18,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -import static org.mockito.Matchers.argThat; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.times; import com.google.api.server.spi.config.Api; @@ -45,6 +45,8 @@ import java.util.Map; import java.util.Set; +import javax.servlet.http.HttpServletResponse; + /** * Base test class for {@link SystemService}. */ @@ -143,7 +145,7 @@ public void testOverrideMethod() throws Exception { @Test public void testInvokeServiceMethod() throws Exception { - systemService.invokeServiceMethod(service, succeed, + systemService.invokeServiceMethod(service, succeed, HttpServletResponse.SC_OK, new FakeParamReader("string", true, 99, 9999999999L, 9.9f, .99, false, 99, 9999999999L, 9.9f, .99, null, null, null, null), new SuccessResultWriter(TestEndpoint.RESULT)); } @@ -151,33 +153,34 @@ public void testInvokeServiceMethod() throws Exception { @Test public void testServiceException() throws Exception { systemService - .invokeServiceMethod(service, fail, new FakeParamReader("string", 99, null, null, null), + .invokeServiceMethod(service, fail, HttpServletResponse.SC_OK, new FakeParamReader("string", 99, null, null, null), new ErrorResultWriter(400, TestEndpoint.ERROR_MESSAGE)); } @Test public void testWrappedException() throws Exception { - systemService.invokeServiceMethod(service, getTestServiceMethod("failWrapped"), + systemService.invokeServiceMethod(service, getTestServiceMethod("failWrapped"), HttpServletResponse.SC_OK, new FakeParamReader(), new ErrorResultWriter(401, TestEndpoint.ERROR_MESSAGE)); } @Test public void testOAuthException() throws Exception { - systemService.invokeServiceMethod(service, failOAuth, + systemService.invokeServiceMethod(service, failOAuth, HttpServletResponse.SC_OK, new FakeParamReader("string", 99, null, null, null), new ErrorResultWriter(401, TestEndpoint.ERROR_MESSAGE, true)); } @Test public void testIllegalArgumentExceptionUserError() throws Exception { - systemService.invokeServiceMethod(service, failIllegalArgumentException, new FakeParamReader(), + systemService.invokeServiceMethod(service, failIllegalArgumentException, HttpServletResponse.SC_OK, + new FakeParamReader(), new ErrorResultWriter(400)); } @Test public void testIllegalArgumentExceptionServerError() throws Exception { systemService = getSystemService(new Object[] {service}, true); - systemService.invokeServiceMethod(service, failIllegalArgumentException, new FakeParamReader(), + systemService.invokeServiceMethod(service, failIllegalArgumentException, HttpServletResponse.SC_OK, new FakeParamReader(), new ErrorResultWriter(500)); } @@ -263,7 +266,7 @@ private static Iterable setOf(ApiConfig... configs) { return argThat(new ConfigListMatcher(configs)); } - private static class ConfigListMatcher extends ArgumentMatcher> { + private static class ConfigListMatcher implements ArgumentMatcher> { private Set expectedConfigs; public ConfigListMatcher(ApiConfig... expectedConfigs) { @@ -271,9 +274,9 @@ public ConfigListMatcher(ApiConfig... expectedConfigs) { } @Override - public boolean matches(Object argument) { - return argument instanceof Iterable - && expectedConfigs.equals(Sets.newHashSet((Iterable) argument)); + public boolean matches(Iterable argument) { + return argument != null + && expectedConfigs.equals(Sets.newHashSet(argument)); } } } diff --git a/test-utils/src/main/java/com/google/api/server/spi/response/ErrorResultWriter.java b/test-utils/src/main/java/com/google/api/server/spi/response/ErrorResultWriter.java index a1aa31f0..5cc8b1ab 100644 --- a/test-utils/src/main/java/com/google/api/server/spi/response/ErrorResultWriter.java +++ b/test-utils/src/main/java/com/google/api/server/spi/response/ErrorResultWriter.java @@ -51,7 +51,7 @@ public ErrorResultWriter(int expectedStatus, String expectedMessage, } @Override - public void write(Object result) throws IOException { + public void write(Object result, int status) throws IOException { fail("expected a ServiceException to be thrown"); } diff --git a/test-utils/src/main/java/com/google/api/server/spi/response/SuccessResultWriter.java b/test-utils/src/main/java/com/google/api/server/spi/response/SuccessResultWriter.java index fe8f3d11..1f4ec594 100644 --- a/test-utils/src/main/java/com/google/api/server/spi/response/SuccessResultWriter.java +++ b/test-utils/src/main/java/com/google/api/server/spi/response/SuccessResultWriter.java @@ -33,7 +33,7 @@ public SuccessResultWriter(Object expectedResult) { } @Override - public void write(Object result) throws IOException { + public void write(Object result, int status) throws IOException { assertThat(result).isEqualTo(expectedResult); } diff --git a/test-utils/src/main/java/com/google/api/server/spi/testing/ArrayEndpoint.java b/test-utils/src/main/java/com/google/api/server/spi/testing/ArrayEndpoint.java index d681e637..4e9d008b 100644 --- a/test-utils/src/main/java/com/google/api/server/spi/testing/ArrayEndpoint.java +++ b/test-utils/src/main/java/com/google/api/server/spi/testing/ArrayEndpoint.java @@ -17,6 +17,7 @@ import com.google.api.server.spi.config.Api; import com.google.api.server.spi.config.ApiMethod; +import com.google.api.server.spi.config.Named; import com.google.api.server.spi.response.CollectionResponse; import java.util.Collection; @@ -87,6 +88,33 @@ public ListContainer getListOfString() { return null; } + @ApiMethod + public void setListOfString(@Named("list") List list) {} + + @ApiMethod(path = "setListOfStringAsQueryParam") + public void setListOfStringAsQueryParam(@Named("list") List list) {} + + @ApiMethod + public void setListOfBooleans(@Named("list") List list, @Named("array") boolean[] array) {} + + @ApiMethod + public void setListOfIntegers(@Named("list") List list, @Named("array") int[] array) {} + + @ApiMethod + public void setListOfLongs(@Named("list") List list, @Named("array") long[] array) {} + + @ApiMethod + public void setListOfFloats(@Named("list") List list, @Named("array") float[] array) {} + + @ApiMethod + public void setListOfDoubles(@Named("list") List list, @Named("array") double[] array) {} + + @ApiMethod + public void setListOfByteArrays(@Named("list") List list, @Named("array") byte[][] array) {} + + @ApiMethod + public void setListOfEnums(@Named("list") List list) {} + public static class ListContainer { public List strings; } diff --git a/test-utils/src/main/java/com/google/api/server/spi/testing/Endpoint1.java b/test-utils/src/main/java/com/google/api/server/spi/testing/Endpoint1.java index e133d67e..dfcdae27 100644 --- a/test-utils/src/main/java/com/google/api/server/spi/testing/Endpoint1.java +++ b/test-utils/src/main/java/com/google/api/server/spi/testing/Endpoint1.java @@ -23,7 +23,6 @@ import com.google.api.server.spi.config.ApiFrontendLimits; import com.google.api.server.spi.config.ApiMethod; import com.google.api.server.spi.config.ApiMethod.HttpMethod; -import com.google.api.server.spi.config.ApiMethodCacheControl; import com.google.api.server.spi.config.Named; import com.google.api.server.spi.config.Nullable; @@ -69,7 +68,6 @@ audiences = {"aa0", "aa1"}, clientIds = {"cc0", "cc1"}, authenticators = { PassAuthenticator.class }, - peerAuthenticators = { PassPeerAuthenticator.class }, defaultVersion = AnnotationBoolean.TRUE, transformers = { DumbSerializer1.class }, useDatastoreForAdditionalConfig = AnnotationBoolean.TRUE @@ -80,15 +78,10 @@ public class Endpoint1 { name = "foos.list", path = "foos", httpMethod = HttpMethod.GET, - cacheControl = @ApiMethodCacheControl( - noCache = true, - maxAge = 1 - ), scopes = {"s0", "s1 s2"}, audiences = {"a0", "a1"}, clientIds = {"c0", "c1"}, - authenticators = { FailAuthenticator.class }, - peerAuthenticators = { FailPeerAuthenticator.class } + authenticators = { FailAuthenticator.class } ) public List listFoos() { return null; @@ -97,11 +90,7 @@ public List listFoos() { @ApiMethod( name = "foos.get", path = "foos/{id}", - httpMethod = HttpMethod.GET, - cacheControl = @ApiMethodCacheControl( - noCache = false, - maxAge = 2 - ) + httpMethod = HttpMethod.GET ) public Foo getFoo(@Named("id") String id) { return null; @@ -110,11 +99,7 @@ public Foo getFoo(@Named("id") String id) { @ApiMethod( name = "foos.insert", path = "foos", - httpMethod = HttpMethod.POST, - cacheControl = @ApiMethodCacheControl( - noCache = false, - maxAge = 3 - ) + httpMethod = HttpMethod.POST ) public Foo insertFoo(Foo r) { return null; @@ -123,11 +108,7 @@ public Foo insertFoo(Foo r) { @ApiMethod( name = "foos.update", path = "foos/{id}", - httpMethod = HttpMethod.PUT, - cacheControl = @ApiMethodCacheControl( - noCache = false, - maxAge = 4 - ) + httpMethod = HttpMethod.PUT ) public Foo updateFoo(@Named("id") String id, Foo r) { return null; @@ -136,11 +117,7 @@ public Foo updateFoo(@Named("id") String id, Foo r) { @ApiMethod( name = "foos.remove", path = "foos/{id}", - httpMethod = HttpMethod.DELETE, - cacheControl = @ApiMethodCacheControl( - noCache = false, - maxAge = 5 - ) + httpMethod = HttpMethod.DELETE ) public void removeFoo(@Named("id") String id) { } diff --git a/test-utils/src/main/java/com/google/api/server/spi/testing/FailPeerAuthenticator.java b/test-utils/src/main/java/com/google/api/server/spi/testing/FailPeerAuthenticator.java deleted file mode 100644 index ce6497fe..00000000 --- a/test-utils/src/main/java/com/google/api/server/spi/testing/FailPeerAuthenticator.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2016 Google Inc. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.api.server.spi.testing; - -import com.google.api.server.spi.config.PeerAuthenticator; - -import javax.servlet.http.HttpServletRequest; - -/** - * Simple dumb peer authenticator for tests that always fail. - */ -public class FailPeerAuthenticator implements PeerAuthenticator { - @Override - public boolean authenticate(HttpServletRequest request) { - return false; - } - - public static Class[] testArray = makeTestArray(); - - // Unchecked cast needed to get a generic array type. - @SuppressWarnings("unchecked") - private static Class[] makeTestArray() { - Class[] peerAuthenticators = {FailPeerAuthenticator.class}; - return (Class[]) peerAuthenticators; - } -} diff --git a/test-utils/src/main/java/com/google/api/server/spi/testing/FooCommonParamsEndpoint.java b/test-utils/src/main/java/com/google/api/server/spi/testing/FooCommonParamsEndpoint.java new file mode 100644 index 00000000..4faaffe0 --- /dev/null +++ b/test-utils/src/main/java/com/google/api/server/spi/testing/FooCommonParamsEndpoint.java @@ -0,0 +1,48 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.api.server.spi.testing; + +import com.google.api.server.spi.config.Api; +import com.google.api.server.spi.config.ApiMethod; +import com.google.api.server.spi.config.ApiMethod.HttpMethod; +import com.google.api.server.spi.config.Named; +import com.google.api.server.spi.config.Nullable; +import com.google.api.server.spi.response.CollectionResponse; + +@Api( + name = "foo", + version = "v1", + audiences = {"audience"}, + title = "The Foo API", + description = "Just Foo Things", + documentationLink = "https://example.com", + canonicalName = "CanonicalName") +public class FooCommonParamsEndpoint extends FooEndpoint { + @ApiMethod(name = "fooo.list", path = "fooos", httpMethod = HttpMethod.GET) + public CollectionResponse listFooos(@Named("n") Integer n) { + return null; + } + + @ApiMethod(path = "fooos/{n}", httpMethod = HttpMethod.GET) + public CollectionResponse listFooosInPath(@Named("n") Integer n) { + return null; + } + + @ApiMethod(path = "fooosNotRequired", httpMethod = HttpMethod.GET) + public CollectionResponse listFooosNotRequired(@Named("n") @Nullable Integer n) { + return null; + } +} diff --git a/test-utils/src/main/java/com/google/api/server/spi/testing/FooDescription.java b/test-utils/src/main/java/com/google/api/server/spi/testing/FooDescription.java index cd480cdc..6c9264ca 100644 --- a/test-utils/src/main/java/com/google/api/server/spi/testing/FooDescription.java +++ b/test-utils/src/main/java/com/google/api/server/spi/testing/FooDescription.java @@ -16,10 +16,12 @@ package com.google.api.server.spi.testing; import com.google.api.server.spi.config.ApiResourceProperty; +import com.google.api.server.spi.config.Description; /** * Test resource type with descriptions. */ +@Description("Description at class level") public class FooDescription { @ApiResourceProperty(description = "description of name") diff --git a/test-utils/src/main/java/com/google/api/server/spi/testing/FooDescriptionEndpoint.java b/test-utils/src/main/java/com/google/api/server/spi/testing/FooDescriptionEndpoint.java index 7288e029..21f7bcd3 100644 --- a/test-utils/src/main/java/com/google/api/server/spi/testing/FooDescriptionEndpoint.java +++ b/test-utils/src/main/java/com/google/api/server/spi/testing/FooDescriptionEndpoint.java @@ -32,7 +32,8 @@ public class FooDescriptionEndpoint { @ApiMethod(name = "foo.create", description = "create desc", path = "foos/{id}", httpMethod = HttpMethod.PUT) - public FooDescription createFoo(@Named("id") @Description("id desc") String id, FooDescription foo) { + public FooDescription createFoo(@Named("id") @Description("id desc") String id, + @Description("Description at method parameter level") FooDescription foo) { return null; } @ApiMethod(name = "foo.get", description = "get desc", path = "foos/{id}", diff --git a/test-utils/src/main/java/com/google/api/server/spi/testing/MapEndpoints.java b/test-utils/src/main/java/com/google/api/server/spi/testing/MapEndpoints.java new file mode 100644 index 00000000..018277de --- /dev/null +++ b/test-utils/src/main/java/com/google/api/server/spi/testing/MapEndpoints.java @@ -0,0 +1,53 @@ +/* + * Copyright 2018 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.api.server.spi.testing; + +import java.util.List; +import java.util.Map; + +import com.google.api.server.spi.config.Api; +import com.google.api.server.spi.config.ApiMethod; + +/** + * Tests the proper caching of schema for Maps with array values + */ +public class MapEndpoints { + + @Api(name = "api1") + public class Api1 { + @ApiMethod + public Resource resource() { + return null; + } + } + + @Api(name = "api2") + public class Api2 { + @ApiMethod + public Resource resource() { + return null; + } + } + + public static class Resource { + private Map> mapOfEnums; + + public Map> getMapOfEnums() { + return mapOfEnums; + } + } + +} diff --git a/test-utils/src/main/java/com/google/api/server/spi/testing/OptionalEndpoint.java b/test-utils/src/main/java/com/google/api/server/spi/testing/OptionalEndpoint.java new file mode 100644 index 00000000..e3b4cfe4 --- /dev/null +++ b/test-utils/src/main/java/com/google/api/server/spi/testing/OptionalEndpoint.java @@ -0,0 +1,81 @@ +package com.google.api.server.spi.testing; + +import java.util.Date; +import java.util.List; +import java.util.Optional; +import java.util.OptionalDouble; +import java.util.OptionalInt; +import java.util.OptionalLong; + +import com.google.api.server.spi.config.Api; +import com.google.api.server.spi.types.SimpleDate; + +/** + * Checks that optional types are unwrapped properly. + */ +@Api +public class OptionalEndpoint { + + public OptionalResults getResult() { + return null; + } + + private class OptionalResults { + public Optional getOptionalString() { + return null; + } + public Optional getOptionalDate() { + return null; + } + public Optional getOptionalSimpleDate() { + return null; + } + + //primitive optionals + public OptionalInt getOptionalInt() { + return null; + } + public OptionalLong getOptionalLong() { + return null; + } + public OptionalDouble getOptionalDouble() { + return null; + } + + //numbers + public Optional getOptionalInteger() { + return null; + } + public Optional getOptionalLongObject() { + return null; + } + public Optional getOptionalFloatObject() { + return null; + } + public Optional getOptionalDoubleObject() { + return null; + } + + //enums + public Optional getOptionalEnum() { + return null; + } + public List getEnums() { + return null; + } + + //objects + public Optional getOptionalFoo() { + return null; + } + public List getFoos() { + return null; + } + + //can't be resolved, must be skipped + public Optional getOptionalAny() { + return null; + } + } + +} diff --git a/test-utils/src/main/java/com/google/api/server/spi/testing/PassPeerAuthenticator.java b/test-utils/src/main/java/com/google/api/server/spi/testing/PassPeerAuthenticator.java deleted file mode 100644 index 6beb1665..00000000 --- a/test-utils/src/main/java/com/google/api/server/spi/testing/PassPeerAuthenticator.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2016 Google Inc. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.api.server.spi.testing; - -import com.google.api.server.spi.config.PeerAuthenticator; -import com.google.api.server.spi.config.Singleton; - -import javax.servlet.http.HttpServletRequest; - -/** - * Simple dumb peer authenticator for tests that always pass. - */ -@Singleton -public class PassPeerAuthenticator implements PeerAuthenticator { - @Override - public boolean authenticate(HttpServletRequest request) { - return true; - } - - public static Class[] testArray = makeTestArray(); - - // Unchecked cast needed to get a generic array type. - @SuppressWarnings("unchecked") - private static Class[] makeTestArray() { - Class[] peerAuthenticators = {PassPeerAuthenticator.class}; - return (Class[]) peerAuthenticators; - } -} diff --git a/test-utils/src/main/java/com/google/api/server/spi/testing/ReferenceOverridingEndpoint.java b/test-utils/src/main/java/com/google/api/server/spi/testing/ReferenceOverridingEndpoint.java index 130a97b2..a90ccda4 100644 --- a/test-utils/src/main/java/com/google/api/server/spi/testing/ReferenceOverridingEndpoint.java +++ b/test-utils/src/main/java/com/google/api/server/spi/testing/ReferenceOverridingEndpoint.java @@ -20,7 +20,6 @@ import com.google.api.server.spi.config.ApiFrontendLimits; import com.google.api.server.spi.config.ApiMethod; import com.google.api.server.spi.config.ApiMethod.HttpMethod; -import com.google.api.server.spi.config.ApiMethodCacheControl; import com.google.api.server.spi.config.ApiReference; import javax.inject.Named; @@ -45,11 +44,7 @@ public class ReferenceOverridingEndpoint extends SubclassedOverridingEndpoint { @ApiMethod( name = "foos.get3", path = "foos/{id}", - httpMethod = HttpMethod.GET, - cacheControl = @ApiMethodCacheControl( - noCache = false, - maxAge = 2 - ) + httpMethod = HttpMethod.GET ) @Override public Foo getFoo(@Named("id") String id) { diff --git a/test-utils/src/main/java/com/google/api/server/spi/testing/RequiredProperties.java b/test-utils/src/main/java/com/google/api/server/spi/testing/RequiredProperties.java new file mode 100644 index 00000000..b71c6e10 --- /dev/null +++ b/test-utils/src/main/java/com/google/api/server/spi/testing/RequiredProperties.java @@ -0,0 +1,44 @@ +package com.google.api.server.spi.testing; + +import com.google.api.server.spi.config.AnnotationBoolean; +import com.google.api.server.spi.config.ApiResourceProperty; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +class RequiredProperties { + public String getUndefined() { + return null; + } + @ApiResourceProperty + public String apiResourceProperty_undefined() { + return null; + } + @ApiResourceProperty(required = AnnotationBoolean.TRUE) + public String apiResourceProperty_required() { + return ""; + } + @ApiResourceProperty(required = AnnotationBoolean.FALSE) + public String apiResourceProperty_not_required() { + return null; + } + @Nullable + public String getNullable() { + return null; + } + @Nonnull + public String getNonnull() { + return ""; + } + @ApiResourceProperty(required = AnnotationBoolean.TRUE) @Nullable + public String getPriority1() { + return ""; + } + @Nonnull @Nullable + public String getPriority2() { + return ""; + } + @ApiResourceProperty(required = AnnotationBoolean.FALSE) @Nonnull + public String getPriority3() { + return null; + } + } \ No newline at end of file diff --git a/test-utils/src/main/java/com/google/api/server/spi/testing/RequiredPropertiesEndpoint.java b/test-utils/src/main/java/com/google/api/server/spi/testing/RequiredPropertiesEndpoint.java new file mode 100644 index 00000000..65da7e79 --- /dev/null +++ b/test-utils/src/main/java/com/google/api/server/spi/testing/RequiredPropertiesEndpoint.java @@ -0,0 +1,15 @@ +package com.google.api.server.spi.testing; + +import com.google.api.server.spi.config.Api; + +@Api( + name = "requiredProperties", + version = "v1", + title = "API to test required properties") +public class RequiredPropertiesEndpoint { + + public RequiredProperties getRequiredProperties() { + return null; + } + +} diff --git a/test-utils/src/main/java/com/google/api/server/spi/testing/ResponseStatusEndpoint.java b/test-utils/src/main/java/com/google/api/server/spi/testing/ResponseStatusEndpoint.java new file mode 100644 index 00000000..3ec632e0 --- /dev/null +++ b/test-utils/src/main/java/com/google/api/server/spi/testing/ResponseStatusEndpoint.java @@ -0,0 +1,26 @@ +package com.google.api.server.spi.testing; + +import com.google.api.server.spi.config.Api; +import com.google.api.server.spi.config.ApiMethod; + +@Api(name = "responseStatus", version = "v1") +public class ResponseStatusEndpoint { + + @ApiMethod + public StringValue responseStatusUnsetReturnString() { + return null; + } + + @ApiMethod + public void responseStatusUnsetReturnVoid() { + } + + @ApiMethod(responseStatus = 201) + public StringValue responseStatusCreatedReturnString() { + return null; + } + + @ApiMethod(responseStatus = 201) + public void responseStatusCreatedReturnVoid() { + } +} diff --git a/test-utils/src/main/java/com/google/api/server/spi/testing/SpecialCharsEndpoint.java b/test-utils/src/main/java/com/google/api/server/spi/testing/SpecialCharsEndpoint.java new file mode 100644 index 00000000..64b6ab99 --- /dev/null +++ b/test-utils/src/main/java/com/google/api/server/spi/testing/SpecialCharsEndpoint.java @@ -0,0 +1,28 @@ +package com.google.api.server.spi.testing; + +import com.google.api.server.spi.config.Api; +import com.google.api.server.spi.config.ApiMethod; +import com.google.api.server.spi.config.Named; +import com.google.api.server.spi.config.Nullable; + + +@Api(name = "specialChars", version = "v1") +public class SpecialCharsEndpoint { + + public static class Requestù { + } + + public static class Responseµ { + } + + //checks escaping of param reference + @ApiMethod(path = "paramSpecialChar1") + public Responseµ paramSpecialChar1(@Named("µ") @Nullable Integer µ, Requestù requestù) { + return null; + } + + @ApiMethod(path = "paramSpecialChar2") + public Responseµ paramSpecialChar2(@Named("µ") @Nullable Integer µ) { + return null; + } +} diff --git a/test-utils/src/main/java/com/google/api/server/spi/testing/SubclassedOverridingEndpoint.java b/test-utils/src/main/java/com/google/api/server/spi/testing/SubclassedOverridingEndpoint.java index a9e2718b..8aad066e 100644 --- a/test-utils/src/main/java/com/google/api/server/spi/testing/SubclassedOverridingEndpoint.java +++ b/test-utils/src/main/java/com/google/api/server/spi/testing/SubclassedOverridingEndpoint.java @@ -20,7 +20,6 @@ import com.google.api.server.spi.config.ApiCacheControl; import com.google.api.server.spi.config.ApiFrontendLimits; import com.google.api.server.spi.config.ApiMethod; -import com.google.api.server.spi.config.ApiMethodCacheControl; import java.util.Collections; import java.util.List; @@ -53,8 +52,7 @@ public List listFoos() { // Override a config. @ApiMethod( - name = "foos.get2", - cacheControl = @ApiMethodCacheControl(maxAge = 4) + name = "foos.get2" ) @Override public Foo getFoo(@Named("id") String id) { diff --git a/test-utils/src/main/java/com/google/api/server/spi/testing/TestEndpoint.java b/test-utils/src/main/java/com/google/api/server/spi/testing/TestEndpoint.java index 97ce8f1d..d7f0a28e 100644 --- a/test-utils/src/main/java/com/google/api/server/spi/testing/TestEndpoint.java +++ b/test-utils/src/main/java/com/google/api/server/spi/testing/TestEndpoint.java @@ -29,6 +29,7 @@ import java.util.HashMap; import java.util.Map; +import java.util.Objects; import javax.inject.Named; import javax.servlet.http.HttpServletRequest; @@ -59,6 +60,14 @@ public static class Request { private String string; private Integer integer = -1; + public Request() { + } + + public Request(String string, Integer integer) { + this.string = string; + this.integer = integer; + } + public void setStringValue(String string) { this.string = string; } @@ -74,6 +83,24 @@ public String getStringValue() { public Integer getIntegerValue() { return integer; } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Request request = (Request) o; + return Objects.equals(string, request.string) && + Objects.equals(integer, request.integer); + } + + @Override + public int hashCode() { + return Objects.hash(string, integer); + } } public enum TestEnum { diff --git a/test-utils/src/main/java/com/google/api/server/spi/testing/TestEnum.java b/test-utils/src/main/java/com/google/api/server/spi/testing/TestEnum.java index 40a66fea..8af65b5a 100644 --- a/test-utils/src/main/java/com/google/api/server/spi/testing/TestEnum.java +++ b/test-utils/src/main/java/com/google/api/server/spi/testing/TestEnum.java @@ -15,6 +15,10 @@ */ package com.google.api.server.spi.testing; +import com.fasterxml.jackson.annotation.JsonProperty; + public enum TestEnum { - VALUE1, VALUE2 + VALUE1, + @JsonProperty("value_2") + VALUE2; } diff --git a/test-utils/src/main/java/com/google/api/server/spi/testing/TestEnumDescription.java b/test-utils/src/main/java/com/google/api/server/spi/testing/TestEnumDescription.java index f49941c8..ab6b0560 100644 --- a/test-utils/src/main/java/com/google/api/server/spi/testing/TestEnumDescription.java +++ b/test-utils/src/main/java/com/google/api/server/spi/testing/TestEnumDescription.java @@ -17,6 +17,7 @@ import com.google.api.server.spi.config.Description; +@Description("A list of enum values") public enum TestEnumDescription { @Description("description of value1") VALUE1, diff --git a/test-utils/src/main/java/com/google/api/server/spi/testing/ValidationBean.java b/test-utils/src/main/java/com/google/api/server/spi/testing/ValidationBean.java new file mode 100644 index 00000000..d7f01a2e --- /dev/null +++ b/test-utils/src/main/java/com/google/api/server/spi/testing/ValidationBean.java @@ -0,0 +1,57 @@ +package com.google.api.server.spi.testing; + +import java.util.List; + +import jakarta.validation.constraints.DecimalMax; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; + +import com.google.api.server.spi.config.ApiResourceProperty; + +public class ValidationBean { + + @Pattern(regexp = "^[0-9]{2}$") @Size(min = 2, max = 2) + @ApiResourceProperty(name = "myPatternTest") + private String patternTest; + @Min(2) @Max(6) + private Long minMaxTest; + @DecimalMin(value = "3.4", inclusive = false) @DecimalMax("4.5") + private Double decimalMinMaxTest; + @Size(min = 3) + private List arrayTest; + + public String getPatternTest() { + return patternTest; + } + + public void setPatternTest(String patternTest) { + this.patternTest = patternTest; + } + + public Long getMinMaxTest() { + return minMaxTest; + } + + public void setMinMaxTest(Long minMaxTest) { + this.minMaxTest = minMaxTest; + } + + public Double getDecimalMinMaxTest() { + return decimalMinMaxTest; + } + + public void setDecimalMinMaxTest(Double decimalMinMaxTest) { + this.decimalMinMaxTest = decimalMinMaxTest; + } + + public List getArrayTest() { + return arrayTest; + } + + public void setArrayTest(List arrayTest) { + this.arrayTest = arrayTest; + } +} diff --git a/test-utils/src/main/java/com/google/api/server/spi/testing/ValidationEndpoint.java b/test-utils/src/main/java/com/google/api/server/spi/testing/ValidationEndpoint.java new file mode 100644 index 00000000..86cba62a --- /dev/null +++ b/test-utils/src/main/java/com/google/api/server/spi/testing/ValidationEndpoint.java @@ -0,0 +1,45 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.api.server.spi.testing; + +import java.util.List; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.DecimalMax; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; + +import com.google.api.server.spi.config.Api; +import com.google.api.server.spi.config.ApiMethod; +import com.google.api.server.spi.config.Named; + +@Api(name = "validation", version = "v1") +public class ValidationEndpoint { + @ApiMethod(name = "create", path = "{pathParam}", httpMethod = "POST") + public void create( + @Named("pathParam") @Pattern(regexp = "^\\d+$") String pathParam, + @Named("queryParam") @Pattern(regexp = "^[a-z]{2}$") String queryParam, + @Named("minMaxParam") @Min(10) @Max(20) Long minMaxParam, + @Named("decimalMinMaxParam") @DecimalMin(value = "2.3", inclusive = false) @DecimalMax(value = "4") Double decimalMinMaxParam, + @Named("sizeParam") @Size(min = 3, max = 6) String sizeParam, + @Named("arraySizeParam") @Size(min = 2, max = 3) List arraySizeParam, + @Valid ValidationBean validationBeanParam + ) { + } +}