From c798c2cc54b7272df8c70cd88513a54f5330abe6 Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Tue, 18 Aug 2015 14:00:10 +0000 Subject: [PATCH] Add log api functionality Change-Id: Ie7025de1bafaf788c8b5950c4a1ee6921b264d8b --- .gitignore | 31 + LICENSE | 175 +++++ README.md | 78 ++ common/build_common.sh | 29 + docs/monasca-log-api-spec.md | 86 +++ java/pom.xml | 439 +++++++++++ java/src/assembly/tar.xml | 29 + java/src/deb/control/control | 9 + java/src/deb/control/prerm | 9 + java/src/deb/etc/log-api-config.yml-sample | 72 ++ .../main/java/monasca/log/api/ApiConfig.java | 37 + .../monasca/log/api/MonApiApplication.java | 221 ++++++ .../java/monasca/log/api/MonApiModule.java | 55 ++ .../log/api/app/ApplicationModule.java | 28 + .../java/monasca/log/api/app/LogService.java | 102 +++ .../log/api/app/command/CreateLogCommand.java | 109 +++ .../monasca/log/api/app/package-info.java | 20 + .../app/validation/DimensionValidation.java | 182 +++++ .../LogApplicationTypeValidator.java | 52 ++ .../log/api/app/validation/Validation.java | 65 ++ .../app/validation/ValueMetaValidation.java | 95 +++ .../middleware/MiddlewareConfiguration.java | 77 ++ .../servlet/MockAuthenticationFilter.java | 93 +++ .../servlet/PostAuthenticationFilter.java | 192 +++++ .../servlet/PreAuthenticationFilter.java | 134 ++++ .../servlet/RoleAuthorizationFilter.java | 70 ++ .../main/java/monasca/log/api/model/Log.java | 111 +++ .../monasca/log/api/model/LogEnvelope.java | 43 ++ .../monasca/log/api/model/LogEnvelopes.java | 52 ++ .../main/java/monasca/log/api/model/Logs.java | 94 +++ .../monasca/log/api/resource/LogResource.java | 79 ++ .../ConstraintViolationExceptionMapper.java | 40 + .../api/resource/exception/ErrorMessage.java | 36 + .../api/resource/exception/Exceptions.java | 169 +++++ .../IllegalArgumentExceptionMapper.java | 31 + .../JsonMappingExceptionManager.java | 40 + .../JsonProcessingExceptionMapper.java | 70 ++ .../exception/ThrowableExceptionMapper.java | 44 ++ .../log/api/MonApiApplicationRunner.java | 23 + .../monasca/log/api/app/LogServiceTest.java | 64 ++ .../api/app/command/CreateLogCommandTest.java | 101 +++ .../LogApplicationTypeValidationTest.java | 91 +++ .../java/monasca/log/api/model/LogTest.java | 263 +++++++ .../resource/AbstractMonApiResourceTest.java | 40 + .../log/api/resource/LogResourceTest.java | 701 ++++++++++++++++++ .../api/resource/exception/ErrorMessages.java | 67 ++ pom.xml | 53 ++ run_maven.sh | 49 ++ 48 files changed, 4750 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100755 common/build_common.sh create mode 100644 docs/monasca-log-api-spec.md create mode 100644 java/pom.xml create mode 100644 java/src/assembly/tar.xml create mode 100644 java/src/deb/control/control create mode 100644 java/src/deb/control/prerm create mode 100644 java/src/deb/etc/log-api-config.yml-sample create mode 100644 java/src/main/java/monasca/log/api/ApiConfig.java create mode 100644 java/src/main/java/monasca/log/api/MonApiApplication.java create mode 100644 java/src/main/java/monasca/log/api/MonApiModule.java create mode 100644 java/src/main/java/monasca/log/api/app/ApplicationModule.java create mode 100644 java/src/main/java/monasca/log/api/app/LogService.java create mode 100644 java/src/main/java/monasca/log/api/app/command/CreateLogCommand.java create mode 100644 java/src/main/java/monasca/log/api/app/package-info.java create mode 100644 java/src/main/java/monasca/log/api/app/validation/DimensionValidation.java create mode 100644 java/src/main/java/monasca/log/api/app/validation/LogApplicationTypeValidator.java create mode 100644 java/src/main/java/monasca/log/api/app/validation/Validation.java create mode 100644 java/src/main/java/monasca/log/api/app/validation/ValueMetaValidation.java create mode 100644 java/src/main/java/monasca/log/api/infrastructure/middleware/MiddlewareConfiguration.java create mode 100644 java/src/main/java/monasca/log/api/infrastructure/servlet/MockAuthenticationFilter.java create mode 100644 java/src/main/java/monasca/log/api/infrastructure/servlet/PostAuthenticationFilter.java create mode 100644 java/src/main/java/monasca/log/api/infrastructure/servlet/PreAuthenticationFilter.java create mode 100644 java/src/main/java/monasca/log/api/infrastructure/servlet/RoleAuthorizationFilter.java create mode 100644 java/src/main/java/monasca/log/api/model/Log.java create mode 100644 java/src/main/java/monasca/log/api/model/LogEnvelope.java create mode 100644 java/src/main/java/monasca/log/api/model/LogEnvelopes.java create mode 100644 java/src/main/java/monasca/log/api/model/Logs.java create mode 100644 java/src/main/java/monasca/log/api/resource/LogResource.java create mode 100644 java/src/main/java/monasca/log/api/resource/exception/ConstraintViolationExceptionMapper.java create mode 100644 java/src/main/java/monasca/log/api/resource/exception/ErrorMessage.java create mode 100644 java/src/main/java/monasca/log/api/resource/exception/Exceptions.java create mode 100644 java/src/main/java/monasca/log/api/resource/exception/IllegalArgumentExceptionMapper.java create mode 100644 java/src/main/java/monasca/log/api/resource/exception/JsonMappingExceptionManager.java create mode 100644 java/src/main/java/monasca/log/api/resource/exception/JsonProcessingExceptionMapper.java create mode 100644 java/src/main/java/monasca/log/api/resource/exception/ThrowableExceptionMapper.java create mode 100644 java/src/test/java/monasca/log/api/MonApiApplicationRunner.java create mode 100644 java/src/test/java/monasca/log/api/app/LogServiceTest.java create mode 100644 java/src/test/java/monasca/log/api/app/command/CreateLogCommandTest.java create mode 100644 java/src/test/java/monasca/log/api/app/validation/LogApplicationTypeValidationTest.java create mode 100644 java/src/test/java/monasca/log/api/model/LogTest.java create mode 100644 java/src/test/java/monasca/log/api/resource/AbstractMonApiResourceTest.java create mode 100644 java/src/test/java/monasca/log/api/resource/LogResourceTest.java create mode 100644 java/src/test/java/monasca/log/api/resource/exception/ErrorMessages.java create mode 100644 pom.xml create mode 100755 run_maven.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..2ee4cace --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ +*.py[co] +*~ +doc/build/* +dist +build +cover +.coverage +*.egg +*.egg-info +.testrepository +.tox +ChangeLog +MANIFEST +monasca.log + + +*.swp +*.iml +.DS_Store +.cache +.classpath +.idea +.project +.target/ +java/debs/* +.settings/ +target +test-output/ +logs/ +*config*.yml +db/config.yml diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..67db8588 --- /dev/null +++ b/LICENSE @@ -0,0 +1,175 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. diff --git a/README.md b/README.md new file mode 100644 index 00000000..bbe38b8c --- /dev/null +++ b/README.md @@ -0,0 +1,78 @@ +# Forked from https://github.com/stackforge/monasca-api +This repository is forked from [monasca-api](https://github.com/stackforge/monasca-api). + +# Overview + +`monasca-log-api` is a RESTful API server that is designed with a layered architecture [layered architecture](http://en.wikipedia.org/wiki/Multilayered_architecture). + +The full API Specification can be found in [docs/monasca-log-api-spec.md](docs/monasca-log-api-spec.md) + +## Java Build + +Requires monasca-common from https://github.com/stackforge/monasca-common. Download and do mvn install. Then: + +``` +cd java +mvn clean package +``` + +# StackForge Java Build + +There is a pom.xml in the base directory that should only be used for the StackForge build. The StackForge build is a rather strange build because of the limitations of the current StackForge java jobs and infrastructure. We have found that the API runs faster if built with maven 3 but the StackForge nodes only have maven 2. This build checks the version of maven and if not maven 3, it downloads a version of maven 3 and uses it. This build depends on jars that are from monasca-common. That StrackForge build uploads the completed jars to http://tarballs.openstack.org/ci/monasca-common, but they are just regular jars, and not in a maven repository and sometimes zuul takes a long time to do the upload. Hence, the first thing the maven build from the base project does is invoke build_common.sh in the common directory. This script clones monasca-common and then invokes maven 3 to build monasca-common in the common directory and install the jars in the local maven repository. + +Since this is all rather complex, that part of the build only works on StackForge so follow the simple instruction above if you are building your own monasca-log-api. + +Currently this build is executed on the bare-precise nodes in StackForge and they only have maven 2. So, this build must be kept compatible with Maven 2. If another monasca-common jar is added as a dependency to java/pom.xml, it must also be added to download/download.sh. + +## Usage + +``` +java -jar target/monasca-log-api.jar server config-file.yml +``` + +## Keystone Configuration + +For secure operation of the Monasca API, the API must be configured to use Keystone in the configuration file under the middleware section. Monasca only works with a Keystone v3 server. The important parts of the configuration are explained below: + +* serverVIP - This is the hostname or IP Address of the Keystone server +* serverPort - The port for the Keystone server +* useHttps - Whether to use https when making requests of the Keystone API +* truststore - If useHttps is true and the Keystone server is not using a certificate signed by a public CA recognized by Java, the CA certificate can be placed in a truststore so the Monasca API will trust it, otherwise it will reject the https connection. This must be a JKS truststore +* truststorePassword - The password for the above truststore +* connSSLClientAuth - If the Keystone server requires the SSL client used by the Monasca server to have a specific client certificate, this should be true, false otherwise +* keystore - The keystore holding the SSL Client certificate if connSSLClientAuth is true +* keystorePassword - The password for the keystore +* defaultAuthorizedRoles - An array of roles that authorize a user to access the complete Monasca API. User must have at least one of these roles. See below +* agentAuthorizedRoles - An array of roles that authorize only the posting of logs. See Keystone Roles below +* adminAuthMethod - "password" if the Monasca API should adminUser and adminPassword to login to the Keystone server to check the user's token, "token" if the Monasca API should use adminToken +* adminUser - Admin user name +* adminPassword - Admin user password +* adminProjectId - Specify the project ID the api should use to request an admin token. Defaults to the admin user's default project. The adminProjectId option takes precedence over adminProjectName. +* adminProjectName - Specify the project name the api should use to request an admin token. Defaults to the admin user's default project. The adminProjectId option takes precedence over adminProjectName. +* adminToken - A valid admin user token if adminAuthMethod is token +* timeToCacheToken - How long the Monasca API should cache the user's token before checking it again + +### Keystone Roles + +See [Monasca API documentation](https://github.com/stackforge/monasca-api/blob/master/README.md#keystone-roles) for the levels of access description. + +## Design Overview + +### Architectural layers + +Requests flow through the following architectural layers from top to bottom: + +* Resource + * Serves as the entrypoint into the service. + * Responsible for handling web service requests, and performing structural request validation. +* Application + * Responsible for providing application level implementations for specific use cases. +* Domain + * Contains the technology agnostic core domain model and domain service definitions. + * Responsible for upholding invariants and defining state transitions. +* Infrastructure + * Contains technology specific implementations of domain services. + +## Documentation + +* API Specification: [/docs/monasca-log-api-spec.md](/docs/monasca-log-api-spec.md). diff --git a/common/build_common.sh b/common/build_common.sh new file mode 100755 index 00000000..70801fe0 --- /dev/null +++ b/common/build_common.sh @@ -0,0 +1,29 @@ +#!/bin/sh +set -x +ME=`whoami` +echo "Running as user: $ME" +MVN=$1 +VERSION=$2 + +check_user() { + ME=$1 + if [ "${ME}" != "jenkins" ]; then + echo "\nERROR: Download monasca-common and do a mvn install to install the monasca-commom jars\n" 1>&2 + exit 1 + fi +} + +BUILD_COMMON=false +POM_FILE=~/.m2/repository/monasca-common/monasca-common/${VERSION}/monasca-common-${VERSION}.pom +if [ ! -r "${POM_FILE}" ]; then + check_user ${ME} + BUILD_COMMON=true +fi + +# This should only be done on the stack forge system +if [ "${BUILD_COMMON}" = "true" ]; then + git clone https://github.com/stackforge/monasca-common + cd monasca-common + ${MVN} clean + ${MVN} install +fi diff --git a/docs/monasca-log-api-spec.md b/docs/monasca-log-api-spec.md new file mode 100644 index 00000000..cab2e215 --- /dev/null +++ b/docs/monasca-log-api-spec.md @@ -0,0 +1,86 @@ +# Monasca Log API + +Date: August 19, 2015 + +Document Version: v2.0 + +# Log +The log resource allows logs to be created. + +## Create Log +Create log. + +### POST /v2.0/log/single + +#### Headers +* X-Auth-Token (string, required) - Keystone auth token +* Content-Type (string, required) - application/json; text/plain +* X-Application-Type (string(255), optional) - Type of application +* X-Dimensions ({string(255):string(255)}, optional) - A dictionary consisting of (key, value) pairs used to structure logs. + +#### Path Parameters +None. + +#### Query Parameters +* tenant_id (string, optional, restricted) - Tenant ID to create log on behalf of. Usage of this query parameter requires the `monitoring-delegate` role. + +#### Request Body +Consists of a single plain text message or a JSON object which can have a maximum length of 1048576 characters. + +#### Request Examples + +##### Plain text log - single line +POST a single line of plain text log. + +``` +POST /v2.0/log/single HTTP/1.1 +Host: 192.168.10.4:8080 +Content-Type: text/plain +X-Auth-Token: 27feed73a0ce4138934e30d619b415b0 +X-Application-Type: apache +X-Dimensions: applicationname:WebServer01,environment:production +Cache-Control: no-cache + +Hello World +``` + +##### Plain text log - multi lines +POST a multiple lines of plain text log. + +``` +POST /v2.0/log/single HTTP/1.1 +Host: 192.168.10.4:8080 +Content-Type: text/plain +X-Auth-Token: 27feed73a0ce4138934e30d619b415b0 +X-Application-Type: apache +X-Dimensions: applicationname:WebServer01,environment:production +Cache-Control: no-cache + +Hello\nWorld +``` + +##### JSON log +POST a JSON log + +``` +POST /v2.0/log/single HTTP/1.1 +Host: 192.168.10.4:8080 +Content-Type: application/json +X-Auth-Token: 27feed73a0ce4138934e30d619b415b0 +X-Application-Type: apache +X-Dimensions: applicationname:WebServer01,environment:production +Cache-Control: no-cache + +{ + "message":"Hello World!", + "from":"hoover" +} + +``` + +### Response +#### Status Code +* 204 - No content + +#### Response Body +This request does not return a response body. diff --git a/java/pom.xml b/java/pom.xml new file mode 100644 index 00000000..6db88ef0 --- /dev/null +++ b/java/pom.xml @@ -0,0 +1,439 @@ + + 4.0.0 + + monasca-log-api + monasca-log-api + 1.1.0-SNAPSHOT + http://github.com/stackforge/monasca-log-api + jar + + + 3.0 + + + + + ${maven.build.timestamp} + yyyy-MM-dd'T'HH:mm:ss + ${project.version}-${timestamp}-${gitRevision} + ${project.artifactId}-${computedVersion} + 1.1.0-SNAPSHOT + 0.7.0 + + false + UTF-8 + UTF-8 + ${project.artifactId}-${project.version}-shaded + + + + + scm:git:git@github.com:stackforge/monasca-log-api + scm:git:git@github.com:stackforge/monasca-log-api + + + + + release-deploy-url-override + + + BUILD_NUM + + + + ${versionNumber}.${BUILD_NUM} + + + + + + + monasca-common + monasca-common-model + ${mon.common.version} + + + commons-validator + commons-validator + 1.4.0 + + + monasca-common + monasca-common-kafka + ${mon.common.version} + + + monasca-common + monasca-common-middleware + ${mon.common.version} + + + io.dropwizard + dropwizard-core + ${dropwizard.version} + + + io.dropwizard + dropwizard-assets + ${dropwizard.version} + + + io.dropwizard + dropwizard-jersey + ${dropwizard.version} + + + com.google.code.findbugs + jsr305 + 2.0.0 + + + org.apache.curator + curator-recipes + 2.2.0-incubating + + + org.slf4j + slf4j-log4j12 + + + + + org.apache.kafka + kafka_2.9.2 + 0.8.0 + + + com.sun.jdmk + jmxtools + + + com.sun.jmx + jmxri + + + org.slf4j + slf4j-simple + + + + + + + monasca-common + monasca-common-testing + ${mon.common.version} + test + + + monasca-common + monasca-common-dropwizard + ${mon.common.version} + test-jar + test + + + io.dropwizard + dropwizard-testing + ${dropwizard.version} + test + + + org.mockito + mockito-all + 1.9.5 + test + + + com.github.docker-java + docker-java + 0.9.1 + test + + + com.jayway.restassured + rest-assured + 2.3.2 + + + commons-io + commons-io + 2.4 + + + org.testng + testng + 6.8.8 + test + + + org.apache.httpcomponents + httpclient + 4.4 + + + org.mockito + mockito-all + 1.10.19 + + + + + + + maven-clean-plugin + 2.5 + + + + ${project.basedir}/debs + + + + + + org.codehaus.mojo + buildnumber-maven-plugin + 1.1 + + + validate + + create + + + + + false + 6 + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.1 + + 1.7 + 1.7 + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.17 + + + org.apache.maven.surefire + surefire-testng + 2.17 + + + + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + 2.17 + + + ${skipITs} + methods + 4 + + + + + integration-test + verify + + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 1.2 + + ${computedName} + true + + + + org.eclipse.jetty.orbit:javax.servlet + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + org.hamcrest:hamcrest-core + org.hamcrest:hamcrest-library + + + + + + package + + shade + + + + + + monasca.api.MonApiApplication + + + true + + + + + + org.apache.maven.plugins + maven-jar-plugin + 2.4 + + + + monasca.api + + + ${project.artifactId}-${computedVersion} + + + + + + maven-assembly-plugin + 2.4.1 + + + src/assembly/tar.xml + + ${artifactNamedVersion} + + + + make-assembly + package + + single + + + + + + jdeb + org.vafer + 1.0 + + + package + + jdeb + + + ${project.basedir}/debs/binaries/${computedName}.deb + + + file + ${project.build.directory}/${shadedJarName}.jar + /opt/monasca/monasca-api.jar + + + file + ${project.basedir}/src/deb/etc/log-api-config.yml-sample + + /etc/monasca/log-api-config.yml-sample + + + + + + + + org.apache.maven.plugins + maven-source-plugin + 2.3 + + + attach-sources + + jar + + + + + + org.apache.maven.plugins + maven-scm-plugin + 1.9.2 + + ${project.version} + + + + org.jacoco + jacoco-maven-plugin + 0.7.5.201505241946 + + ${basedir}/target/coverage-reports/jacoco-unit.exec + ${basedir}/target/coverage-reports/jacoco-unit.exec + + + + jacoco-initialize + + prepare-agent + + + + jacoco-site + package + + report + + + + + + org.jacoco + jacoco-maven-plugin + 0.7.5.201505241946 + + ${basedir}/target/coverage-reports/jacoco-unit.exec + ${basedir}/target/coverage-reports/jacoco-unit.exec + + + + jacoco-initialize + + prepare-agent + + + + jacoco-site + package + + report + + + + + + + + diff --git a/java/src/assembly/tar.xml b/java/src/assembly/tar.xml new file mode 100644 index 00000000..49c4c052 --- /dev/null +++ b/java/src/assembly/tar.xml @@ -0,0 +1,29 @@ + + tar + + tar.gz + + + + ${project.basedir} + / + + README* + LICENSE* + + + + + + ${project.build.directory}/${shadedJarName}.jar + / + monasca-log-api.jar + + + ${project.basedir}/src/deb/etc/log-api-config.yml-sample + examples + + + diff --git a/java/src/deb/control/control b/java/src/deb/control/control new file mode 100644 index 00000000..c9616f48 --- /dev/null +++ b/java/src/deb/control/control @@ -0,0 +1,9 @@ +Package: [[name]] +Section: misc +Priority: optional +Architecture: all +Depends: openjdk-7-jre-headless | openjdk-7-jre +Version: [[version]]-[[timestamp]]-[[buildNumber]] +Maintainer: Monasca Team +Description: Monasca-LOG-API + RESTful API for Monasca logs. diff --git a/java/src/deb/control/prerm b/java/src/deb/control/prerm new file mode 100644 index 00000000..be388d12 --- /dev/null +++ b/java/src/deb/control/prerm @@ -0,0 +1,9 @@ +#!/bin/sh + +case "$1" in + remove) + stop monasca-log-api + ;; +esac + +exit 0 diff --git a/java/src/deb/etc/log-api-config.yml-sample b/java/src/deb/etc/log-api-config.yml-sample new file mode 100644 index 00000000..99d8d42d --- /dev/null +++ b/java/src/deb/etc/log-api-config.yml-sample @@ -0,0 +1,72 @@ +# The region for which all metrics passing through this server will be persisted +region: useast + +kafka: + brokerUris: + - 192.168.10.4:9092 + zookeeperUris: + - 192.168.10.4:2181 + healthCheckTopic: healthcheck + +middleware: + enabled: true + serverVIP: 192.168.10.5 + serverPort: 5000 + connTimeout: 500 + connSSLClientAuth: false + connPoolMaxActive: 3 + connPoolMaxIdle: 3 + connPoolEvictPeriod: 600000 + connPoolMinIdleTime: 600000 + connRetryTimes: 2 + connRetryInterval: 50 + defaultAuthorizedRoles: [user, domainuser, domainadmin, monasca-user] + agentAuthorizedRoles: [monasca-agent] + adminAuthMethod: password + adminUser: admin + adminPassword: admin + adminProjectId: + adminProjectName: + adminToken: + timeToCacheToken: 600 + maxTokenCacheSize: 1048576 + +server: + applicationConnectors: + - type: http + maxRequestHeaderSize: 16KiB # Allow large headers used by keystone tokens + +# Logging settings. +logging: + + # The default level of all loggers. Can be OFF, ERROR, WARN, INFO, DEBUG, TRACE, or ALL. + level: debug + + # Logger-specific levels. + loggers: + + # Sets the level for 'com.example.app' to DEBUG. + com.example.app: DEBUG + + appenders: + - type: console + threshold: ALL + timeZone: UTC + target: stdout + logFormat: # TODO + + - type: file + currentLogFilename: /var/log/monasca/monasca-api.log + threshold: ALL + archive: true + archivedLogFilenamePattern: /var/log/monasca/monasca-api-%d.log.gz + archivedFileCount: 5 + timeZone: UTC + logFormat: # TODO + + - type: syslog + host: 192.168.10.4 + port: 514 + facility: local0 + threshold: ALL + logFormat: # TODO diff --git a/java/src/main/java/monasca/log/api/ApiConfig.java b/java/src/main/java/monasca/log/api/ApiConfig.java new file mode 100644 index 00000000..8eaadd29 --- /dev/null +++ b/java/src/main/java/monasca/log/api/ApiConfig.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. + * + * 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 monasca.log.api; + +import io.dropwizard.Configuration; + +import javax.validation.Valid; +import javax.validation.constraints.NotNull; + +import org.hibernate.validator.constraints.NotEmpty; + +import monasca.common.messaging.kafka.KafkaConfiguration; +import monasca.log.api.infrastructure.middleware.MiddlewareConfiguration; + +public class ApiConfig extends Configuration { + @NotEmpty + public String region; + @NotEmpty + public String logTopic = "log"; + @Valid + @NotNull + public KafkaConfiguration kafka; + @Valid + @NotNull + public MiddlewareConfiguration middleware; +} diff --git a/java/src/main/java/monasca/log/api/MonApiApplication.java b/java/src/main/java/monasca/log/api/MonApiApplication.java new file mode 100644 index 00000000..f8ff14ef --- /dev/null +++ b/java/src/main/java/monasca/log/api/MonApiApplication.java @@ -0,0 +1,221 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. + * + * 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 monasca.log.api; + +import io.dropwizard.Application; +import io.dropwizard.setup.Bootstrap; +import io.dropwizard.setup.Environment; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +import javax.servlet.FilterRegistration.Dynamic; +import javax.ws.rs.ext.ExceptionMapper; + +import org.eclipse.jetty.servlets.CrossOriginFilter; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.PropertyNamingStrategy; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.module.SimpleModule; + +import monasca.common.middleware.AuthConstants; +import monasca.common.middleware.TokenAuth; +import monasca.common.util.Injector; +import monasca.log.api.infrastructure.servlet.MockAuthenticationFilter; +import monasca.log.api.infrastructure.servlet.PostAuthenticationFilter; +import monasca.log.api.infrastructure.servlet.PreAuthenticationFilter; +import monasca.log.api.infrastructure.servlet.RoleAuthorizationFilter; +import monasca.log.api.resource.LogResource; +import monasca.log.api.resource.exception.ConstraintViolationExceptionMapper; +import monasca.log.api.resource.exception.IllegalArgumentExceptionMapper; +import monasca.log.api.resource.exception.JsonMappingExceptionManager; +import monasca.log.api.resource.exception.JsonProcessingExceptionMapper; +import monasca.log.api.resource.exception.ThrowableExceptionMapper; + +/** + * Monitoring API application. + */ +public class MonApiApplication extends Application { + public static void main(String[] args) throws Exception { + /* + * This should allow command line options to show the current version + * java -jar monasca-log-api.jar --version + * java -jar monasca-log-api.jar -version + * java -jar monasca-log-api.jar version + * Really anything with the word version in it will show the + * version as long as there is only one argument + * */ + if (args.length == 1 && args[0].toLowerCase().contains("version")) { + showVersion(); + System.exit(0); + } + + new MonApiApplication().run(args); + } + + private static void showVersion() { + Package pkg; + pkg = Package.getPackage("monasca.log.api"); + + System.out.println("-------- Version Information --------"); + System.out.println(pkg.getImplementationVersion()); + } + + @Override + public void initialize(Bootstrap bootstrap) { + } + + @Override + public String getName() { + return "Fujitsu LOG service"; + } + + @Override + @SuppressWarnings("unchecked") + public void run(ApiConfig config, Environment environment) throws Exception { + /** Wire services */ + Injector.registerModules(new MonApiModule(config)); + + /** Configure resources */ + environment.jersey().register(Injector.getInstance(LogResource.class)); + + /** Configure providers */ + removeExceptionMappers(environment.jersey().getResourceConfig().getSingletons()); + environment.jersey().register(new IllegalArgumentExceptionMapper()); + environment.jersey().register(new JsonProcessingExceptionMapper()); + environment.jersey().register(new JsonMappingExceptionManager()); + environment.jersey().register(new ConstraintViolationExceptionMapper()); + environment.jersey().register(new ThrowableExceptionMapper() {}); + + /** Configure Jackson */ + environment.getObjectMapper().setPropertyNamingStrategy( + PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES); + environment.getObjectMapper().enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY); + environment.getObjectMapper().disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + SimpleModule module = new SimpleModule("SerializationModule"); + environment.getObjectMapper().registerModule(module); + + + /** Configure CORS filter */ + Dynamic corsFilter = environment.servlets().addFilter("cors", CrossOriginFilter.class); + corsFilter.addMappingForUrlPatterns(null, true, "/*"); + corsFilter.setInitParameter("allowedOrigins", "*"); + corsFilter.setInitParameter("allowedHeaders", + "X-Requested-With,Content-Type,Accept,Origin,X-Auth-Token"); + corsFilter.setInitParameter("allowedMethods", "OPTIONS,GET,HEAD"); + + if (config.middleware.enabled) { + ensureHasValue(config.middleware.serverVIP, "serverVIP", "enabled", "true"); + ensureHasValue(config.middleware.serverPort, "serverPort", "enabled", "true"); + ensureHasValue(config.middleware.adminAuthMethod, "adminAuthMethod", "enabled", "true"); + if ("password".equalsIgnoreCase(config.middleware.adminAuthMethod)) { + ensureHasValue(config.middleware.adminUser, "adminUser", "adminAuthMethod", "password"); + ensureHasValue(config.middleware.adminPassword, "adminPassword", "adminAuthMethod", "password"); + } else if ("token".equalsIgnoreCase(config.middleware.adminAuthMethod)) { + ensureHasValue(config.middleware.adminToken, "adminToken", "adminAuthMethod", "token"); + } else { + throw new Exception(String.format( + "Invalid value '%s' for adminAuthMethod. Must be either password or token", + config.middleware.adminAuthMethod)); + } + if (config.middleware.defaultAuthorizedRoles == null || config.middleware.defaultAuthorizedRoles.isEmpty()) { + ensureHasValue(null, "defaultAuthorizedRoles", "enabled", "true"); + } + if (config.middleware.connSSLClientAuth) { + ensureHasValue(config.middleware.keystore, "keystore", "connSSLClientAuth", "true"); + ensureHasValue(config.middleware.keystorePassword, "keystorePassword", "connSSLClientAuth", "true"); + } + Map authInitParams = new HashMap(); + authInitParams.put("ServerVIP", config.middleware.serverVIP); + authInitParams.put("ServerPort", config.middleware.serverPort); + authInitParams.put(AuthConstants.USE_HTTPS, String.valueOf(config.middleware.useHttps)); + authInitParams.put("ConnTimeout", config.middleware.connTimeout); + authInitParams.put("ConnSSLClientAuth", String.valueOf(config.middleware.connSSLClientAuth)); + authInitParams.put("ConnPoolMaxActive", config.middleware.connPoolMaxActive); + authInitParams.put("ConnPoolMaxIdle", config.middleware.connPoolMaxActive); + authInitParams.put("ConnPoolEvictPeriod", config.middleware.connPoolEvictPeriod); + authInitParams.put("ConnPoolMinIdleTime", config.middleware.connPoolMinIdleTime); + authInitParams.put("ConnRetryTimes", config.middleware.connRetryTimes); + authInitParams.put("ConnRetryInterval", config.middleware.connRetryInterval); + authInitParams.put("AdminToken", config.middleware.adminToken); + authInitParams.put("TimeToCacheToken", config.middleware.timeToCacheToken); + authInitParams.put("AdminAuthMethod", config.middleware.adminAuthMethod); + authInitParams.put("AdminUser", config.middleware.adminUser); + authInitParams.put("AdminPassword", config.middleware.adminPassword); + authInitParams.put(AuthConstants.ADMIN_PROJECT_ID, config.middleware.adminProjectId); + authInitParams.put(AuthConstants.ADMIN_PROJECT_NAME, config.middleware.adminProjectName); + authInitParams.put("MaxTokenCacheSize", config.middleware.maxTokenCacheSize); + setIfNotNull(authInitParams, AuthConstants.TRUSTSTORE, config.middleware.truststore); + setIfNotNull(authInitParams, AuthConstants.TRUSTSTORE_PASS, config.middleware.truststorePassword); + setIfNotNull(authInitParams, AuthConstants.KEYSTORE, config.middleware.keystore); + setIfNotNull(authInitParams, AuthConstants.KEYSTORE_PASS, config.middleware.keystorePassword); + + /** Configure auth filters */ + Dynamic preAuthenticationFilter = + environment.servlets().addFilter("pre-auth", new PreAuthenticationFilter()); + preAuthenticationFilter.addMappingForUrlPatterns(null, true, "/"); + preAuthenticationFilter.addMappingForUrlPatterns(null, true, "/v2.0/*"); + + Dynamic tokenAuthFilter = environment.servlets().addFilter("token-auth", new TokenAuth()); + tokenAuthFilter.addMappingForUrlPatterns(null, true, "/"); + tokenAuthFilter.addMappingForUrlPatterns(null, true, "/v2.0/*"); + tokenAuthFilter.setInitParameters(authInitParams); + + Dynamic postAuthenticationFilter = + environment.servlets().addFilter( + "post-auth", + new PostAuthenticationFilter(config.middleware.defaultAuthorizedRoles, + config.middleware.agentAuthorizedRoles)); + postAuthenticationFilter.addMappingForUrlPatterns(null, true, "/"); + postAuthenticationFilter.addMappingForUrlPatterns(null, true, "/v2.0/*"); + + environment.jersey().getResourceConfig().getContainerRequestFilters() + .add(new RoleAuthorizationFilter()); + } else { + Dynamic mockAuthenticationFilter = + environment.servlets().addFilter("mock-auth", new MockAuthenticationFilter()); + mockAuthenticationFilter.addMappingForUrlPatterns(null, true, "/"); + mockAuthenticationFilter.addMappingForUrlPatterns(null, true, "/v2.0/*"); + } + } + + private void ensureHasValue(final String value, final String what, final String control, + final String controlValue) throws Exception { + if (value == null || value.isEmpty()) { + final String message = + String + .format( + "Since %s in middleware section of configuration file is set to %s, %s must have a value", + control, controlValue, what); + throw new Exception(message); + } + } + + private void setIfNotNull(Map authInitParams, String name, String value) { + if (value != null) { + authInitParams.put(name, value); + } + } + + private void removeExceptionMappers(Set items) { + for (Iterator i = items.iterator(); i.hasNext();) { + Object o = i.next(); + if (o instanceof ExceptionMapper) + i.remove(); + } + } +} diff --git a/java/src/main/java/monasca/log/api/MonApiModule.java b/java/src/main/java/monasca/log/api/MonApiModule.java new file mode 100644 index 00000000..28ef50b0 --- /dev/null +++ b/java/src/main/java/monasca/log/api/MonApiModule.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. + * + * 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 monasca.log.api; + +import java.util.Properties; + +import javax.inject.Singleton; + +import kafka.javaapi.producer.Producer; +import kafka.producer.ProducerConfig; + +import com.google.common.base.Joiner; +import com.google.inject.AbstractModule; +import com.google.inject.Provides; + +import monasca.log.api.app.ApplicationModule; + +/** + * Monitoring API server bindings. + */ +public class MonApiModule extends AbstractModule { + private final ApiConfig config; + + public MonApiModule(ApiConfig config) { + this.config = config; + } + + @Override + protected void configure() { + bind(ApiConfig.class).toInstance(config); + install(new ApplicationModule()); + } + + @Provides + @Singleton + public Producer getProducer() { + Properties props = new Properties(); + props.put("metadata.broker.list", Joiner.on(',').join(config.kafka.brokerUris)); + props.put("serializer.class", "kafka.serializer.StringEncoder"); + props.put("request.required.acks", "1"); + ProducerConfig config = new ProducerConfig(props); + return new Producer(config); + } +} diff --git a/java/src/main/java/monasca/log/api/app/ApplicationModule.java b/java/src/main/java/monasca/log/api/app/ApplicationModule.java new file mode 100644 index 00000000..ee57ccdc --- /dev/null +++ b/java/src/main/java/monasca/log/api/app/ApplicationModule.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. + * + * 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 monasca.log.api.app; + +import javax.inject.Singleton; + +import com.google.inject.AbstractModule; + +/** + * Application layer bindings. + */ +public class ApplicationModule extends AbstractModule { + @Override + protected void configure() { + bind(LogService.class).in(Singleton.class); + } +} diff --git a/java/src/main/java/monasca/log/api/app/LogService.java b/java/src/main/java/monasca/log/api/app/LogService.java new file mode 100644 index 00000000..82ce4164 --- /dev/null +++ b/java/src/main/java/monasca/log/api/app/LogService.java @@ -0,0 +1,102 @@ +/* + * Copyright 2015 Fujitsu Limited + * + * 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 monasca.log.api.app; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import javax.inject.Inject; + +import kafka.javaapi.producer.Producer; +import kafka.producer.KeyedMessage; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableMap.Builder; + +import monasca.log.api.ApiConfig; +import monasca.log.api.model.Log; +import monasca.log.api.model.LogEnvelope; +import monasca.log.api.model.LogEnvelopes; + +/** + * Log service implementation. + */ +public class LogService { + private static final Comparator> comparator; + private final ApiConfig config; + private final Producer producer; + + static { + comparator = new Comparator>() { + @Override + public int compare(Entry o1, Entry o2) { + int nameCmp = o1.getKey().compareTo(o2.getKey()); + return (nameCmp != 0 ? nameCmp : o1.getValue().compareTo(o2.getValue())); + } + }; + } + + @Inject + public LogService(ApiConfig config, Producer producer) { + this.config = config; + this.producer = producer; + } + + public void create(Log log, String tenantId) { + Builder metaBuilder = new ImmutableMap.Builder().put("tenantId", tenantId).put("region", this.config.region); + ImmutableMap meta = metaBuilder.build(); + + LogEnvelope envelope = new LogEnvelope(log, meta); + String key = buildKey(tenantId, log); + String json = LogEnvelopes.toJson(envelope); + String topic = this.config.logTopic; + this.producer.send(new KeyedMessage<>(topic, key, json)); + } + + private String buildKey(String tenantId, Log log) { + final StringBuilder key = new StringBuilder(); + + // appends tenantId + key.append(tenantId); + + // appends applicationType + if (log.applicationType != null && !log.applicationType.isEmpty()) { + key.append(log.applicationType); + } + + // appends dimensions + if (log.dimensions != null && !log.dimensions.isEmpty()) { + for (final Map.Entry dim : buildSortedDimSet(log.dimensions)) { + key.append(dim.getKey()); + key.append(dim.getValue()); + } + } + + return key.toString(); + } + + // Key must be the same for the same log so sort the dimensions so they will + // be + // in a known order + private List> buildSortedDimSet(final Map dimMap) { + final List> dims = new ArrayList<>(dimMap.entrySet()); + Collections.sort(dims, comparator); + return dims; + } + +} diff --git a/java/src/main/java/monasca/log/api/app/command/CreateLogCommand.java b/java/src/main/java/monasca/log/api/app/command/CreateLogCommand.java new file mode 100644 index 00000000..9dde061a --- /dev/null +++ b/java/src/main/java/monasca/log/api/app/command/CreateLogCommand.java @@ -0,0 +1,109 @@ +/* + * Copyright 2015 Fujitsu Limited + * + * 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 monasca.log.api.app.command; + +import java.util.Map; + +import javax.annotation.Nullable; +import javax.validation.constraints.Size; + +import org.hibernate.validator.constraints.NotEmpty; + +import monasca.log.api.app.validation.DimensionValidation; +import monasca.log.api.app.validation.LogApplicationTypeValidator; +import monasca.log.api.model.Log; +import monasca.log.api.resource.exception.Exceptions; + +public class CreateLogCommand { + public static final int MAX_NAME_LENGTH = 255; + public static final int MAX_LOG_LENGTH = 1024 * 1024; + + @NotEmpty + @Size(min = 1, max = MAX_NAME_LENGTH) + public String applicationType; + public Map dimensions; + public String message; + + public CreateLogCommand() {} + + public CreateLogCommand(@Nullable String applicationType, @Nullable Map dimensions, String message) { + setApplicationType(applicationType); + setDimensions(dimensions); + this.message = message; + } + + public void setDimensions(Map dimensions) { + // White spaces have been already trimmed, but normalize just in case. + this.dimensions = DimensionValidation.normalize(dimensions); + } + + public void setApplicationType(String applicationType) { + this.applicationType = LogApplicationTypeValidator.normalize(applicationType); + } + + public Log toLog() { + return new Log(applicationType, dimensions, message); + } + + public void validate() { + // Validate applicationType + if (applicationType != null && !applicationType.isEmpty()) { + LogApplicationTypeValidator.validate(applicationType); + } + + // Validate dimensions + if (dimensions != null) { + DimensionValidation.validate(dimensions, null); + } + + // Validate log message + if (message.length() > MAX_LOG_LENGTH) + throw Exceptions.unprocessableEntity("Log must be %d characters or less", MAX_LOG_LENGTH); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + CreateLogCommand other = (CreateLogCommand) obj; + if (dimensions == null) { + if (other.dimensions != null) + return false; + } else if (!dimensions.equals(other.dimensions)) + return false; + if (applicationType == null) { + if (other.applicationType != null) + return false; + } else if (!applicationType.equals(other.applicationType)) + return false; + if (!message.equals(other.message)) + return false; + return true; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((dimensions == null) ? 0 : dimensions.hashCode()); + result = prime * result + ((applicationType == null) ? 0 : applicationType.hashCode()); + result = prime * result * message.hashCode(); + return result; + } + +} diff --git a/java/src/main/java/monasca/log/api/app/package-info.java b/java/src/main/java/monasca/log/api/app/package-info.java new file mode 100644 index 00000000..06692c82 --- /dev/null +++ b/java/src/main/java/monasca/log/api/app/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. + * + * 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. + */ + +/** + * Houses the application/service layer. + * + * @see http://martinfowler.com/eaaCatalog/serviceLayer.html + */ +package monasca.log.api.app; diff --git a/java/src/main/java/monasca/log/api/app/validation/DimensionValidation.java b/java/src/main/java/monasca/log/api/app/validation/DimensionValidation.java new file mode 100644 index 00000000..edcc5d8d --- /dev/null +++ b/java/src/main/java/monasca/log/api/app/validation/DimensionValidation.java @@ -0,0 +1,182 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. + * + * 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 monasca.log.api.app.validation; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +import javax.annotation.Nullable; +import javax.ws.rs.WebApplicationException; + +import com.google.common.base.CharMatcher; +import com.google.common.base.Strings; +import com.google.common.primitives.Ints; + +import monasca.common.model.Services; +import monasca.log.api.resource.exception.Exceptions; + +/** + * Utilities for validating dimensions. + */ +public final class DimensionValidation { + private static final Map VALIDATORS; + private static final Pattern UUID_PATTERN = Pattern + .compile("\\w{8}-\\w{4}-\\w{4}-\\w{4}-\\w{12}"); + private static final Pattern VALID_DIMENSION_NAME = Pattern.compile("[^><={}(), '\";&]+$"); + private static final String INVALID_CHAR_STRING = "> < = { } ( ) ' \" , ; &"; + + private DimensionValidation() {} + + interface DimensionValidator { + boolean isValidDimension(String name, String value); + } + + static { + VALIDATORS = new HashMap(); + + // Compute validator + VALIDATORS.put(Services.COMPUTE_SERVICE, new DimensionValidator() { + @Override + public boolean isValidDimension(String name, String value) { + if ("instance_id".equals(name)) + return value.length() != 36 || UUID_PATTERN.matcher(value).matches(); + if ("az".equals(name)) + return Ints.tryParse(value) != null; + return true; + } + }); + + // Objectstore validator + VALIDATORS.put(Services.OBJECT_STORE_SERVICE, new DimensionValidator() { + @Override + public boolean isValidDimension(String name, String value) { + if ("container".equals(name)) + return value.length() < 256 || !value.contains("/"); + return true; + } + }); + + // Volume validator + VALIDATORS.put(Services.VOLUME_SERVICE, new DimensionValidator() { + @Override + public boolean isValidDimension(String name, String value) { + if ("instance_id".equals(name)) + return value.length() != 36 || UUID_PATTERN.matcher(value).matches(); + if ("az".equals(name)) + return Ints.tryParse(value) != null; + return true; + } + }); + } + + /** + * Normalizes dimensions by stripping whitespace. + */ + public static Map normalize(Map dimensions) { + if (dimensions == null) + return null; + Map result = new HashMap<>(); + for (Map.Entry dimension : dimensions.entrySet()) { + String dimensionKey = null; + if (dimension.getKey() != null) { + dimensionKey = CharMatcher.WHITESPACE.trimFrom(dimension.getKey()); + if (dimensionKey.isEmpty()) + dimensionKey = null; + } + String dimensionValue = null; + if (dimension.getValue() != null) { + dimensionValue = CharMatcher.WHITESPACE.trimFrom(dimension.getValue()); + if (dimensionValue.isEmpty()) + dimensionValue = null; + } + result.put(dimensionKey, dimensionValue); + } + + return result; + } + + /** + * Validates that the given {@code dimensions} are valid. + * + * @throws WebApplicationException if validation fails + */ + public static void validate(Map dimensions, @Nullable String service) { + // Validate dimension names and values + for (Map.Entry dimension : dimensions.entrySet()) { + String name = dimension.getKey(); + String value = dimension.getValue(); + + // General validations + if (Strings.isNullOrEmpty(name)) + throw Exceptions.unprocessableEntity("Dimension name cannot be empty"); + if (Strings.isNullOrEmpty(value)) + throw Exceptions.unprocessableEntity("Dimension %s cannot have an empty value", name); + if (name.length() > 255) + throw Exceptions.unprocessableEntity("Dimension name %s must be 255 characters or less", + name); + if (value.length() > 255) + throw Exceptions.unprocessableEntity("Dimension value %s must be 255 characters or less", + value); + // Dimension names that start with underscores are reserved for internal use only. + if (name.startsWith("_")) { + throw Exceptions.unprocessableEntity("Dimension name cannot start with underscore (_)", + name); + } + if (!VALID_DIMENSION_NAME.matcher(name).matches()) + throw Exceptions.unprocessableEntity( + "Dimension name %s may not contain: %s", name, INVALID_CHAR_STRING); + + // Service specific validations + if (service != null) { + if (!name.equals(Services.SERVICE_DIMENSION) + && !Services.isValidDimensionName(service, name)) + throw Exceptions.unprocessableEntity("%s is not a valid dimension name for service %s", + name, service); + DimensionValidator validator = VALIDATORS.get(service); + if (validator != null && !validator.isValidDimension(name, value)) + throw Exceptions.unprocessableEntity("%s is not a valid dimension value for service %s", + value, service); + } + } + } + + /** + * Validates a list of dimension names + * @param names + */ + + public static void validateNames(List names) { + if(names != null) { + for (String name : names) { + if (Strings.isNullOrEmpty(name)) { + throw Exceptions.unprocessableEntity("Dimension name cannot be empty"); + } + if (name.length() > 255) { + throw Exceptions.unprocessableEntity("Dimension name '%s' must be 255 characters or less", + name); + } + // Dimension names that start with underscores are reserved for internal use only. + if (name.startsWith("_")) { + throw Exceptions.unprocessableEntity("Dimension name '%s' cannot start with underscore (_)", + name); + } + if (!VALID_DIMENSION_NAME.matcher(name).matches()) + throw Exceptions.unprocessableEntity( + "Dimension name '%s' may not contain: %s", name, INVALID_CHAR_STRING); + } + } + } +} diff --git a/java/src/main/java/monasca/log/api/app/validation/LogApplicationTypeValidator.java b/java/src/main/java/monasca/log/api/app/validation/LogApplicationTypeValidator.java new file mode 100644 index 00000000..bac07b80 --- /dev/null +++ b/java/src/main/java/monasca/log/api/app/validation/LogApplicationTypeValidator.java @@ -0,0 +1,52 @@ +/* + * Copyright 2015 Fujitsu Limited + * + * 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 monasca.log.api.app.validation; + +import java.util.regex.Pattern; + +import javax.ws.rs.WebApplicationException; + +import com.google.common.base.CharMatcher; + +import monasca.log.api.app.command.CreateLogCommand; +import monasca.log.api.resource.exception.Exceptions; + +/** + * Utilities for validating log application types. + */ +public class LogApplicationTypeValidator { + private static final Pattern VALID_APPLICATION_TYPE = Pattern.compile("^[a-zA-Z0-9_\\.\\-]+$"); + + private LogApplicationTypeValidator() {} + + /** + * Normalizes the {@code applicationType} by removing whitespace. + */ + public static String normalize(String applicationType) { + return applicationType == null ? null : CharMatcher.WHITESPACE.trimFrom(applicationType); + } + + /** + * Validates the {@code applicationType} for the character constraints. + * + * @throws WebApplicationException if validation fails + */ + public static void validate(String applicationType) { + if (applicationType.length() > CreateLogCommand.MAX_NAME_LENGTH) + throw Exceptions.unprocessableEntity("Application type %s must be %d characters or less", applicationType, CreateLogCommand.MAX_NAME_LENGTH); + + if (!VALID_APPLICATION_TYPE.matcher(applicationType).matches()) + throw Exceptions.unprocessableEntity("Application type %s may only contain: a-z A-Z 0-9 _ - .", applicationType); + } +} diff --git a/java/src/main/java/monasca/log/api/app/validation/Validation.java b/java/src/main/java/monasca/log/api/app/validation/Validation.java new file mode 100644 index 00000000..66bc672a --- /dev/null +++ b/java/src/main/java/monasca/log/api/app/validation/Validation.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. + * + * 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 monasca.log.api.app.validation; + +import java.util.HashMap; +import java.util.Map; + +import javax.ws.rs.WebApplicationException; + +import com.google.common.base.Splitter; +import com.google.common.base.Strings; + +import monasca.log.api.resource.exception.Exceptions; + +/** + * Validation related utilities. + */ +public final class Validation { + + private static final Splitter COMMA_SPLITTER_FOR_LOG = Splitter.on(',').trimResults(); + + private Validation() {} + + /** + * @throws WebApplicationException if the {@code value} is null or empty. + */ + public static Map parseLogDimensions(String dimensionsStr) { + Validation.validateNotNullOrEmpty(dimensionsStr, "dimensions"); + + Map dimensions = new HashMap(); + for (String dimensionStr : COMMA_SPLITTER_FOR_LOG.split(dimensionsStr)) { + if (dimensionStr.isEmpty()) + throw Exceptions.unprocessableEntity("Dimension cannot be empty"); + + int index = dimensionStr.indexOf(':'); + if (index == -1) + throw Exceptions.unprocessableEntity("%s is not a valid dimension", dimensionStr); + String dimensionKey = dimensionStr.substring(0, index); + String dimensionValue = dimensionStr.substring(index + 1); + + dimensions.put(dimensionKey, dimensionValue); + } + + return dimensions; + } + + /** + * @throws WebApplicationException if the {@code value} is null or empty. + */ + public static void validateNotNullOrEmpty(String value, String parameterName) { + if (Strings.isNullOrEmpty(value)) + throw Exceptions.unprocessableEntity("%s is required", parameterName); + } +} diff --git a/java/src/main/java/monasca/log/api/app/validation/ValueMetaValidation.java b/java/src/main/java/monasca/log/api/app/validation/ValueMetaValidation.java new file mode 100644 index 00000000..ff08d88c --- /dev/null +++ b/java/src/main/java/monasca/log/api/app/validation/ValueMetaValidation.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2015 Hewlett-Packard Development Company, L.P. + * + * 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 monasca.log.api.app.validation; + +import com.google.common.base.CharMatcher; +import com.google.common.base.Strings; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import javax.ws.rs.WebApplicationException; + +import monasca.log.api.resource.exception.Exceptions; + +/** + * Utilities for validating valueMeta. + */ +public final class ValueMetaValidation { + private static final int VALUE_META_MAX_NUMBER = 16; + private static final int VALUE_META_VALUE_MAX_LENGTH = 2048; + private static final int VALUE_META_NAME_MAX_LENGTH = 255; + private static final Map EMPTY_VALUE_META = Collections + .unmodifiableMap(new HashMap()); + + private ValueMetaValidation() {} + + /** + * Normalizes valueMeta by stripping whitespace from name. validate() must + * already have been called on the valueMeta + */ + public static Map normalize(Map valueMeta) { + if (valueMeta == null || valueMeta.isEmpty()) { + return EMPTY_VALUE_META; + } + final Map result = new HashMap<>(); + for (Map.Entry entry : valueMeta.entrySet()) { + final String key = CharMatcher.WHITESPACE.trimFrom(entry.getKey()); + result.put(key, entry.getValue()); + } + + return result; + } + + /** + * Validates that the given {@code valueMetas} are valid. + * + * @throws WebApplicationException if validation fails + */ + public static void validate(Map valueMetas) { + if (valueMetas.size() > VALUE_META_MAX_NUMBER) { + throw Exceptions.unprocessableEntity("Maximum number of valueMeta key/value parirs is %d", + VALUE_META_MAX_NUMBER); + } + + // Validate valueMeta names and values + for (Map.Entry valueMeta : valueMetas.entrySet()) { + // Have to check for null first because later check is for trimmed name + if (valueMeta.getKey() == null) { + throw Exceptions.unprocessableEntity("valueMeta name cannot be empty"); + } + final String name = CharMatcher.WHITESPACE.trimFrom(valueMeta.getKey()); + String value = valueMeta.getValue(); + if (value == null) { + // Store nulls as empty strings + value = ""; + } + + // General validations + if (Strings.isNullOrEmpty(name)) { + throw Exceptions.unprocessableEntity("valueMeta name cannot be empty"); + } + if (name.length() > VALUE_META_NAME_MAX_LENGTH) { + throw Exceptions.unprocessableEntity("valueMeta name %s must be %d characters or less", + name, VALUE_META_NAME_MAX_LENGTH); + } + if (value.length() > VALUE_META_VALUE_MAX_LENGTH) { + throw Exceptions.unprocessableEntity("valueMeta value %s must be %d characters or less", + value, VALUE_META_VALUE_MAX_LENGTH); + } + } + } +} + diff --git a/java/src/main/java/monasca/log/api/infrastructure/middleware/MiddlewareConfiguration.java b/java/src/main/java/monasca/log/api/infrastructure/middleware/MiddlewareConfiguration.java new file mode 100644 index 00000000..851ad574 --- /dev/null +++ b/java/src/main/java/monasca/log/api/infrastructure/middleware/MiddlewareConfiguration.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. + * + * 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 monasca.log.api.infrastructure.middleware; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * CS Middleware configuration. + */ +public class MiddlewareConfiguration { + public Boolean enabled = false; + @JsonProperty + public String serverVIP; + @JsonProperty + public String serverPort; + @JsonProperty + public Boolean useHttps = Boolean.FALSE; + @JsonProperty + public String connTimeout = "500"; + @JsonProperty + public Boolean connSSLClientAuth = Boolean.FALSE; + @JsonProperty + public String connPoolMaxActive = "3"; + @JsonProperty + public String connPoolMaxIdle = "3"; + @JsonProperty + public String connPoolEvictPeriod = "600000"; + @JsonProperty + public String connPoolMinIdleTime = "600000"; + @JsonProperty + public String connRetryTimes = "2"; + @JsonProperty + public String connRetryInterval = "50"; + @JsonProperty + public List defaultAuthorizedRoles; + @JsonProperty + public List agentAuthorizedRoles; + @JsonProperty + public String delegateAuthorizedRole; + @JsonProperty + public String timeToCacheToken = "600"; + @JsonProperty + public String adminAuthMethod; + @JsonProperty + public String adminUser; + @JsonProperty + public String adminToken; + @JsonProperty + public String adminPassword; + @JsonProperty + public String adminProjectId = ""; + @JsonProperty + public String adminProjectName = ""; + @JsonProperty + public String maxTokenCacheSize = "1048576"; + @JsonProperty + public String truststore; + @JsonProperty + public String truststorePassword; + @JsonProperty + public String keystore; + @JsonProperty + public String keystorePassword; +} diff --git a/java/src/main/java/monasca/log/api/infrastructure/servlet/MockAuthenticationFilter.java b/java/src/main/java/monasca/log/api/infrastructure/servlet/MockAuthenticationFilter.java new file mode 100644 index 00000000..b28105d2 --- /dev/null +++ b/java/src/main/java/monasca/log/api/infrastructure/servlet/MockAuthenticationFilter.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. + * + * 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 monasca.log.api.infrastructure.servlet; + +import java.io.IOException; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; + +/** + * Mocks authentication by converting X-Auth-Token headers to X-Tenant-Ids. + */ +public class MockAuthenticationFilter implements Filter { + private static final String X_AUTH_TOKEN_HEADER = "X-Auth-Token"; + + @Override + public void destroy() {} + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + final HttpServletRequest req = (HttpServletRequest) request; + HttpServletRequestWrapper wrapper = requestWrapperFor(req); + chain.doFilter(wrapper, response); + } + + @Override + public void init(FilterConfig filterConfig) throws ServletException {} + + /** + * Returns an HttpServletRequestWrapper that serves tenant id headers from request attributes. + */ + private HttpServletRequestWrapper requestWrapperFor(final HttpServletRequest request) { + return new HttpServletRequestWrapper(request) { + @Override + public Object getAttribute(String name) { + if (name.equalsIgnoreCase(PostAuthenticationFilter.X_TENANT_ID_HEADER)) { + String tenantId = request.getHeader(PostAuthenticationFilter.X_TENANT_ID_HEADER); + return tenantId == null ? request.getHeader(X_AUTH_TOKEN_HEADER) : tenantId; + } + if (name.equalsIgnoreCase(PostAuthenticationFilter.X_IDENTITY_STATUS_ATTRIBUTE)) + return PostAuthenticationFilter.CONFIRMED_STATUS; + if (name.equalsIgnoreCase(PostAuthenticationFilter.X_ROLES_ATTRIBUTE)) + return "user"; + return super.getAttribute(name); + } + + @Override + public String getHeader(String name) { + if (name.equalsIgnoreCase(PostAuthenticationFilter.X_TENANT_ID_HEADER)) + return request.getHeader(X_AUTH_TOKEN_HEADER); + return super.getHeader(name); + } + + @Override + public Enumeration getHeaderNames() { + List names = Collections.list(super.getHeaderNames()); + names.add(PostAuthenticationFilter.X_TENANT_ID_HEADER); + return Collections.enumeration(names); + } + + @Override + public Enumeration getHeaders(String name) { + if (name.equalsIgnoreCase(PostAuthenticationFilter.X_TENANT_ID_HEADER)) { + String authToken = request.getHeader(X_AUTH_TOKEN_HEADER); + return authToken == null ? Collections.emptyEnumeration() : Collections + .enumeration(Collections.singleton(authToken)); + } + return super.getHeaders(name); + } + }; + } +} diff --git a/java/src/main/java/monasca/log/api/infrastructure/servlet/PostAuthenticationFilter.java b/java/src/main/java/monasca/log/api/infrastructure/servlet/PostAuthenticationFilter.java new file mode 100644 index 00000000..f50f5439 --- /dev/null +++ b/java/src/main/java/monasca/log/api/infrastructure/servlet/PostAuthenticationFilter.java @@ -0,0 +1,192 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. + * + * 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 monasca.log.api.infrastructure.servlet; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; + +import javax.annotation.Nullable; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.core.MediaType; + +import monasca.log.api.infrastructure.servlet.PreAuthenticationFilter.ErrorCapturingServletResponseWrapper; + +/** + * Authenticates requests using header information from the CsMiddleware. Provides the X-TENANT-ID + * servlet attribute as a request header. Intended to be added to a servlet filter chain after the + * CsMiddleware TokenAuth filter. + */ +public class PostAuthenticationFilter implements Filter { + static final String CONFIRMED_STATUS = "CONFIRMED"; + static final String X_ROLES_ATTRIBUTE = "X-ROLES"; + static final String X_MONASCA_AGENT = "X-MONASCA_AGENT"; + static final String X_IDENTITY_STATUS_ATTRIBUTE = "X-IDENTITY-STATUS"; + private static final String X_TENANT_ID_ATTRIBUTE = "X-PROJECT-ID"; + static final String X_TENANT_ID_HEADER = "X-Tenant-Id"; + static final String X_ROLES_HEADER = "X-Roles"; + + private final List defaultAuthorizedRoles = new ArrayList(); + private final List agentAuthorizedRoles = new ArrayList(); + + public PostAuthenticationFilter(List defaultAuthorizedRoles, + List agentAuthorizedRoles) { + for (String defaultRole : defaultAuthorizedRoles) { + this.defaultAuthorizedRoles.add(defaultRole.toLowerCase()); + } + for (String agentRole : agentAuthorizedRoles) { + this.agentAuthorizedRoles.add(agentRole.toLowerCase()); + } + } + + @Override + public void destroy() {} + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) { + final HttpServletRequest req = (HttpServletRequest) request; + ErrorCapturingServletResponseWrapper res = (ErrorCapturingServletResponseWrapper) response; + String tenantIdStr = null; + + try { + // According to CORS spec OPTIONS method does not pass auth info + if (req.getMethod().equals("OPTIONS")) { + chain.doFilter(request, response); + return; + } + + Object tenantId = request.getAttribute(X_TENANT_ID_ATTRIBUTE); + + if (tenantId == null) { + sendAuthError(res, null, null, null); + return; + } + tenantIdStr = tenantId.toString(); + + boolean authenticated = isAuthenticated(req); + boolean authorized = isAuthorized(req); + + if (authenticated && authorized) { + HttpServletRequestWrapper wrapper = requestWrapperFor(req); + chain.doFilter(wrapper, response); + return; + } + + if (authorized) + sendAuthError(res, tenantIdStr, null, null); + else + sendAuthError(res, tenantIdStr, "Tenant is missing a required role to access this service", + null); + } catch (Exception e) { + try { + sendAuthError(res, tenantIdStr, null, e); + } catch (IOException ignore) { + } + } + } + + @Override + public void init(FilterConfig filterConfig) throws ServletException {} + + /** + * @return true if the request is authenticated else false + */ + private boolean isAuthenticated(HttpServletRequest request) { + Object identityStatus = request.getAttribute(X_IDENTITY_STATUS_ATTRIBUTE); + return identityStatus != null && CONFIRMED_STATUS.equalsIgnoreCase(identityStatus.toString()); + } + + /** + * @return true if the request is authorized else false + */ + private boolean isAuthorized(HttpServletRequest request) { + Object rolesFromKeystone = request.getAttribute(X_ROLES_ATTRIBUTE); + if (rolesFromKeystone == null) + return false; + + boolean agentUser = false; + for (String role : rolesFromKeystone.toString().split(",")) { + String lowerCaseRole = role.toLowerCase(); + if ((defaultAuthorizedRoles != null) && defaultAuthorizedRoles.contains(lowerCaseRole)) { + return true; + } + if ((agentAuthorizedRoles != null) && agentAuthorizedRoles.contains(lowerCaseRole)) { + agentUser = true; + } + } + if (agentUser) { + request.setAttribute(X_MONASCA_AGENT, true); + return true; + } + return false; + } + + /** + * Returns an HttpServletRequestWrapper that serves tenant id headers from request attributes. + */ + private HttpServletRequestWrapper requestWrapperFor(final HttpServletRequest request) { + return new HttpServletRequestWrapper(request) { + @Override + public String getHeader(String name) { + if (name.equalsIgnoreCase(X_TENANT_ID_HEADER)) + return request.getAttribute(X_TENANT_ID_ATTRIBUTE).toString(); + else if (name.equalsIgnoreCase(X_ROLES_HEADER)) + return request.getAttribute(X_ROLES_ATTRIBUTE).toString(); + return super.getHeader(name); + } + + @Override + public Enumeration getHeaderNames() { + List names = Collections.list(super.getHeaderNames()); + names.add(X_TENANT_ID_HEADER); + names.add(X_ROLES_HEADER); + return Collections.enumeration(names); + } + + @Override + public Enumeration getHeaders(String name) { + if (name.equalsIgnoreCase(X_TENANT_ID_HEADER)) + return Collections.enumeration(Collections.singleton(request.getAttribute( + X_TENANT_ID_ATTRIBUTE).toString())); + else if (name.equalsIgnoreCase(X_ROLES_HEADER)) + return Collections.enumeration(Collections.singleton(request.getAttribute( + X_ROLES_ATTRIBUTE).toString())); + return super.getHeaders(name); + } + }; + } + + private void sendAuthError(ErrorCapturingServletResponseWrapper response, + @Nullable String tenantId, @Nullable String message, @Nullable Exception exception) + throws IOException { + response.setContentType(MediaType.APPLICATION_JSON); + + if (message == null) + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, + tenantId == null ? "Failed to authenticate request" + : "Failed to authenticate request for " + tenantId, exception); + else + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, String.format(message, tenantId)); + } +} diff --git a/java/src/main/java/monasca/log/api/infrastructure/servlet/PreAuthenticationFilter.java b/java/src/main/java/monasca/log/api/infrastructure/servlet/PreAuthenticationFilter.java new file mode 100644 index 00000000..d759ac60 --- /dev/null +++ b/java/src/main/java/monasca/log/api/infrastructure/servlet/PreAuthenticationFilter.java @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. + * + * 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 monasca.log.api.infrastructure.servlet; + +import java.io.IOException; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletOutputStream; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServletResponseWrapper; +import javax.ws.rs.core.MediaType; + +import org.eclipse.jetty.server.Response; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import monasca.log.api.resource.exception.Exceptions; +import monasca.log.api.resource.exception.Exceptions.FaultType; + +/** + * Authenticates requests using header information from the CsMiddleware. Provides the X-TENANT-ID + * servlet attribute as a request header. Intended to be added to a servlet filter chain after the + * CsMiddleware TokenAuth filter. + */ +public class PreAuthenticationFilter implements Filter { + private static final Logger LOG = LoggerFactory.getLogger(PreAuthenticationFilter.class); + + static class ErrorCapturingServletResponseWrapper extends HttpServletResponseWrapper { + private int statusCode; + private String errorMessage; + private Exception exception; + + public ErrorCapturingServletResponseWrapper(HttpServletResponse response) { + super(response); + } + + @Override + public void sendError(int statusCode) throws IOException { + this.statusCode = statusCode; + } + + @Override + public void sendError(int statusCode, String msg) throws IOException { + this.statusCode = statusCode; + errorMessage = msg; + } + + void sendError(int statusCode, String msg, Exception exception) throws IOException { + sendError(statusCode, msg); + this.exception = exception; + } + } + + @Override + public void destroy() {} + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) { + HttpServletResponse res = (HttpServletResponse) response; + ErrorCapturingServletResponseWrapper responseWrapper = + new ErrorCapturingServletResponseWrapper(res); + + boolean caughtException = false; + ServletOutputStream out = null; + try { + out = res.getOutputStream(); + chain.doFilter(request, responseWrapper); + if (responseWrapper.statusCode != 401 && responseWrapper.statusCode != 500) + return; + + } catch (Exception e) { + LOG.error("Error while executing pre authentication filter", e); + caughtException = true; + } + + try { + res.setContentType(MediaType.APPLICATION_JSON); + if (caughtException) { + res.setStatus(Response.SC_INTERNAL_SERVER_ERROR); + } + else { + res.setStatus(responseWrapper.statusCode); + FaultType faultType; + if (responseWrapper.statusCode == 500) { + faultType = FaultType.SERVER_ERROR; + } + else { + faultType = FaultType.UNAUTHORIZED; + } + String output = Exceptions.buildLoggedErrorMessage(faultType, responseWrapper.errorMessage, + null, responseWrapper.exception); + out.print(output); + } + } catch (IllegalArgumentException e) { + // CSMiddleware is throwing this error for invalid tokens. + // This problem appears to be fixed in other versions, but they are not approved yet. + try { + String output = + Exceptions.buildLoggedErrorMessage(FaultType.UNAUTHORIZED, "invalid authToken", null, + responseWrapper.exception); + out.print(output); + } catch (Exception x) { + LOG.error("Error while writing failed authentication HTTP response", x); + } + } catch (Exception e) { + LOG.error("Error while writing failed authentication HTTP response", e); + } finally { + if (out != null) + try { + out.close(); + } catch (IOException ignore) { + } + } + } + + @Override + public void init(FilterConfig filterConfig) throws ServletException {} +} diff --git a/java/src/main/java/monasca/log/api/infrastructure/servlet/RoleAuthorizationFilter.java b/java/src/main/java/monasca/log/api/infrastructure/servlet/RoleAuthorizationFilter.java new file mode 100644 index 00000000..120f9aca --- /dev/null +++ b/java/src/main/java/monasca/log/api/infrastructure/servlet/RoleAuthorizationFilter.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. + * + * 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 monasca.log.api.infrastructure.servlet; + +import monasca.common.middleware.AuthConstants; +import monasca.log.api.resource.exception.Exceptions; + +import com.sun.jersey.spi.container.ContainerRequest; +import com.sun.jersey.spi.container.ContainerRequestFilter; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.core.Context; + +import static monasca.log.api.infrastructure.servlet.PostAuthenticationFilter.X_MONASCA_AGENT; + +public class RoleAuthorizationFilter implements ContainerRequestFilter { + private static final Logger logger = LoggerFactory.getLogger + (ContainerRequestFilter.class); + @Context + private HttpServletRequest httpServletRequest; + private static final String[] VALID_MONASCA_AGENT_POST_PATHS = new String[] { "/v2.0/metrics", "/v2.0/log/single" }; + private static final String[] VALID_MONASCA_AGENT_GET_PATHS = new String[] { "/", "/v2.0" }; + + @Override + public ContainerRequest filter(ContainerRequest containerRequest) { + String method = containerRequest.getMethod(); + Object isAgent = httpServletRequest.getAttribute(X_MONASCA_AGENT); + String pathInfo = httpServletRequest.getPathInfo(); + + // X_MONASCA_AGENT is only set if the only valid role for this user is an agent role + if (isAgent != null) { + if (!(method.equals("POST") && validPath(pathInfo, VALID_MONASCA_AGENT_POST_PATHS)) && + !(method.equals("GET") && validPath(pathInfo, VALID_MONASCA_AGENT_GET_PATHS))) { + logger.warn("User {} is missing a required role to {} on {}", + httpServletRequest.getAttribute(AuthConstants.AUTH_USER_NAME), + method, pathInfo); + throw Exceptions.badRequest("User is missing a required role to perform this request"); + } + } + return containerRequest; + } + + private boolean validPath(String pathInfo, String[] paths) { + // Make the comparison easier by getting rid of trailing slashes + while (!pathInfo.isEmpty() && !"/".equals(pathInfo) && pathInfo.endsWith("/")) { + pathInfo = pathInfo.substring(0, pathInfo.length() - 1); + } + for (final String validPath : paths) { + if (validPath.equals(pathInfo)) { + return true; + } + } + return false; + } +} diff --git a/java/src/main/java/monasca/log/api/model/Log.java b/java/src/main/java/monasca/log/api/model/Log.java new file mode 100644 index 00000000..21d6feb7 --- /dev/null +++ b/java/src/main/java/monasca/log/api/model/Log.java @@ -0,0 +1,111 @@ +/* + * Copyright 2015 FUJITSU LIMITED + * + * 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 monasca.log.api.model; + +import java.io.Serializable; +import java.util.Map; + +import javax.annotation.Nullable; + +/** + * Represents a log. + */ +public class Log + implements Serializable { + + private static final long serialVersionUID = 685205295808136758L; + + public String applicationType; + public Map dimensions; + public String message; + + public Log() { + } + + public Log(@Nullable String applicationType, @Nullable Map dimensions, + String message) { + this.applicationType = applicationType; + this.dimensions = dimensions; + this.message = message; + } + + @Override + public String toString() { + return "Log{" + "applicationType='" + applicationType + '\'' + ", dimensions=" + dimensions + + ", message=" + message + '}'; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (!(obj instanceof Log)) + return false; + Log other = (Log) obj; + if (dimensions == null) { + if (other.dimensions != null) + return false; + } else if (!dimensions.equals(other.dimensions)) + return false; + // internally handles null and empty strings as same for applicationType + if (applicationType == null) { + if (other.applicationType != null && !other.applicationType.isEmpty()) + return false; + } else if (applicationType.isEmpty()) { + if (other.applicationType != null && !other.applicationType.isEmpty()) + return false; + } else if (!applicationType.equals(other.applicationType)) + return false; + if (!message.equals(other.message)) + return false; + return true; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((dimensions == null) ? 0 : dimensions.hashCode()); + result = prime * result + ((applicationType == null) ? 0 : applicationType.hashCode()); + result = prime * result + message.hashCode(); + return result; + } + + public String getApplicationType() { + return applicationType; + } + + public void setApplicationType(String applicationType) { + this.applicationType = applicationType; + } + + public Map getDimensions() { + return dimensions; + } + + public void setDimensions(Map dimensions) { + this.dimensions = dimensions; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} diff --git a/java/src/main/java/monasca/log/api/model/LogEnvelope.java b/java/src/main/java/monasca/log/api/model/LogEnvelope.java new file mode 100644 index 00000000..15813134 --- /dev/null +++ b/java/src/main/java/monasca/log/api/model/LogEnvelope.java @@ -0,0 +1,43 @@ +/* + * Copyright 2015 FUJITSU LIMITED + * + * 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 monasca.log.api.model; + +import java.util.Map; + +import com.google.common.base.Preconditions; + +/** + * A log envelope. + */ +public class LogEnvelope { + public Log log; + public Map meta; + public long creationTime; + + protected LogEnvelope() { + } + + public LogEnvelope(Log log) { + Preconditions.checkNotNull(log, "log"); + this.log = log; + this.creationTime = System.currentTimeMillis() / 1000; + } + + public LogEnvelope(Log log, Map meta) { + this(log); + Preconditions.checkNotNull(meta, "meta"); + this.meta = meta; + } +} diff --git a/java/src/main/java/monasca/log/api/model/LogEnvelopes.java b/java/src/main/java/monasca/log/api/model/LogEnvelopes.java new file mode 100644 index 00000000..b5c154eb --- /dev/null +++ b/java/src/main/java/monasca/log/api/model/LogEnvelopes.java @@ -0,0 +1,52 @@ +/* + * Copyright 2015 FUJITSU LIMITED + * + * 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 monasca.log.api.model; + +import com.fasterxml.jackson.core.JsonProcessingException; +import monasca.common.util.Exceptions; + +/** + * Utilities for working with LogEnvelopes. + */ +public final class LogEnvelopes { + private LogEnvelopes() { + } + + /** + * Returns the LogEnvelope for the {@code logJson}. + * + * @throws RuntimeException if an error occurs while parsing {@code logJson} + */ + public static LogEnvelope fromJson(byte[] logJson) { + try { + String jsonStr = new String(logJson, "UTF-8"); + return Logs.OBJECT_MAPPER.readValue(jsonStr, LogEnvelope.class); + } catch (Exception e) { + throw Exceptions.uncheck(e, "Failed to parse log json: %s", new String(logJson)); + } + } + + /** + * Returns the JSON representation of the {@code envelope} else null if it could not be converted + * to JSON. + */ + public static String toJson(LogEnvelope envelope) { + try { + return Logs.OBJECT_MAPPER.writeValueAsString(envelope); + } catch (JsonProcessingException e) { + return null; + } + } +} diff --git a/java/src/main/java/monasca/log/api/model/Logs.java b/java/src/main/java/monasca/log/api/model/Logs.java new file mode 100644 index 00000000..2c95ebf1 --- /dev/null +++ b/java/src/main/java/monasca/log/api/model/Logs.java @@ -0,0 +1,94 @@ +/* + * Copyright 2015 FUJITSU LIMITED + * + * 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 monasca.log.api.model; + +import java.io.IOException; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategy; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.module.SimpleModule; +import monasca.common.util.Exceptions; +import org.apache.commons.lang3.StringEscapeUtils; + +/** + * Utilities for working with Logs. + */ +public final class Logs { + static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + static { + OBJECT_MAPPER.setPropertyNamingStrategy(PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES); + SimpleModule module = new SimpleModule(); + module.addSerializer(new LogSerializer()); + OBJECT_MAPPER.registerModule(module); + } + + private Logs() { + } + + /** + * Returns the Log for the {@code logJson}. + * + * @throws RuntimeException if an error occurs while parsing {@code logJson} + */ + public static Log fromJson(byte[] logJson) { + try { + String jsonStr = StringEscapeUtils.unescapeJava(new String(logJson, "UTF-8")); + return OBJECT_MAPPER.readValue(jsonStr, Log.class); + } catch (Exception e) { + throw Exceptions.uncheck(e, "Failed to parse log json: %s", new String(logJson)); + } + } + + /** + * Returns the JSON representation of the {@code log} else null if it could not be converted to + * JSON. + */ + public static String toJson(Log log) { + try { + return OBJECT_MAPPER.writeValueAsString(log); + } catch (JsonProcessingException e) { + return null; + } + } + + /** Log serializer */ + private static class LogSerializer + extends JsonSerializer { + @Override + public Class handledType() { + return Log.class; + } + + public void serialize(Log log, JsonGenerator jgen, SerializerProvider provider) + throws IOException, JsonProcessingException { + jgen.writeStartObject(); + + if (log.applicationType != null && !log.applicationType.isEmpty()) { + jgen.writeStringField("application_type", log.applicationType); + } + if (log.dimensions != null && !log.dimensions.isEmpty()) { + jgen.writeObjectField("dimensions", log.dimensions); + } + jgen.writeStringField("message", log.message); + + jgen.writeEndObject(); + } + } +} diff --git a/java/src/main/java/monasca/log/api/resource/LogResource.java b/java/src/main/java/monasca/log/api/resource/LogResource.java new file mode 100644 index 00000000..dae97d53 --- /dev/null +++ b/java/src/main/java/monasca/log/api/resource/LogResource.java @@ -0,0 +1,79 @@ +/* + * Copyright 2015 Fujitsu Limited + * + * 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 monasca.log.api.resource; + +import java.util.Map; + +import javax.inject.Inject; +import javax.ws.rs.Consumes; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.UriInfo; + +import com.codahale.metrics.annotation.Timed; +import com.google.common.base.Splitter; +import com.google.common.base.Strings; + +import monasca.log.api.app.LogService; +import monasca.log.api.app.command.CreateLogCommand; +import monasca.log.api.app.validation.Validation; +import monasca.log.api.resource.exception.Exceptions; + +/** + * Log resource implementation. + */ +@Path("/v2.0/log") +public class LogResource { + private static final String MONITORING_DELEGATE_ROLE = "monitoring-delegate"; + private static final Splitter COMMA_SPLITTER = Splitter.on(',').omitEmptyStrings().trimResults(); + + private final LogService service; + + @Inject + public LogResource(LogService service) { + this.service = service; + } + + @POST + @Timed + @Consumes({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN}) + @Path("/single") + public void single(@Context UriInfo uriInfo, + @HeaderParam("X-Tenant-Id") String tenantId, + @HeaderParam("X-Roles") String roles, + @HeaderParam("X-Application-Type") String applicationType, + @HeaderParam("X-Dimensions") String dimensionsStr, + @QueryParam("tenant_id") String crossTenantId, String message) { + + boolean isDelegate = !Strings.isNullOrEmpty(roles) && COMMA_SPLITTER.splitToList(roles).contains(MONITORING_DELEGATE_ROLE); + + Map dimensions = Strings.isNullOrEmpty(dimensionsStr) ? null : Validation.parseLogDimensions(dimensionsStr); + + CreateLogCommand command = new CreateLogCommand(applicationType, dimensions, message); + + if (!isDelegate) { + if (!Strings.isNullOrEmpty(crossTenantId)) { + throw Exceptions.forbidden("Project %s cannot POST cross tenant metrics", tenantId); + } + } + + command.validate(); + + service.create(command.toLog(), Strings.isNullOrEmpty(crossTenantId) ? tenantId : crossTenantId); + } +} diff --git a/java/src/main/java/monasca/log/api/resource/exception/ConstraintViolationExceptionMapper.java b/java/src/main/java/monasca/log/api/resource/exception/ConstraintViolationExceptionMapper.java new file mode 100644 index 00000000..82e1b05b --- /dev/null +++ b/java/src/main/java/monasca/log/api/resource/exception/ConstraintViolationExceptionMapper.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. + * + * 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 monasca.log.api.resource.exception; + +import io.dropwizard.jersey.validation.ValidationErrorMessage; + +import javax.validation.ConstraintViolationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + +import monasca.log.api.resource.exception.Exceptions.FaultType; + +@Provider +public class ConstraintViolationExceptionMapper implements + ExceptionMapper { + private static final int UNPROCESSABLE_ENTITY = 422; + + @Override + public Response toResponse(ConstraintViolationException exception) { + final ValidationErrorMessage message = + new ValidationErrorMessage(exception.getConstraintViolations()); + String msg = + message.getErrors().isEmpty() ? exception.getMessage() : message.getErrors().toString(); + return Response.status(UNPROCESSABLE_ENTITY).type(MediaType.APPLICATION_JSON) + .entity(Exceptions.buildLoggedErrorMessage(FaultType.UNPROCESSABLE_ENTITY, msg)).build(); + } +} diff --git a/java/src/main/java/monasca/log/api/resource/exception/ErrorMessage.java b/java/src/main/java/monasca/log/api/resource/exception/ErrorMessage.java new file mode 100644 index 00000000..f28719ea --- /dev/null +++ b/java/src/main/java/monasca/log/api/resource/exception/ErrorMessage.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. + * + * 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 monasca.log.api.resource.exception; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.Preconditions; + +public class ErrorMessage { + public int code; + public String message; + public String details; + @JsonProperty("internal_code") + public String internalCode; + + ErrorMessage() {} + + public ErrorMessage(int code, String message, String details, String internalCode) { + Preconditions.checkNotNull(internalCode, "internalCode"); + + this.code = code; + this.message = message == null ? "" : message; + this.details = details == null ? "" : details; + this.internalCode = internalCode; + } +} diff --git a/java/src/main/java/monasca/log/api/resource/exception/Exceptions.java b/java/src/main/java/monasca/log/api/resource/exception/Exceptions.java new file mode 100644 index 00000000..20860ffa --- /dev/null +++ b/java/src/main/java/monasca/log/api/resource/exception/Exceptions.java @@ -0,0 +1,169 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. + * + * 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 monasca.log.api.resource.exception; + +import java.util.Random; + +import javax.annotation.Nullable; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategy; +import com.google.common.base.Splitter; + +/** + * Exception factory methods. + */ +public final class Exceptions { + private static final Logger LOG = LoggerFactory.getLogger(Exceptions.class); + private static final ObjectMapper OBJECT_MAPPER; + private static final Splitter LINE_SPLITTER = Splitter.on("\n").trimResults(); + private static final Random RANDOM = new Random(); + + static { + OBJECT_MAPPER = new ObjectMapper(); + OBJECT_MAPPER + .setPropertyNamingStrategy(PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES); + } + + public enum FaultType { + + SERVER_ERROR(Status.INTERNAL_SERVER_ERROR, true), + BAD_REQUEST(Status.BAD_REQUEST, true), + UNAUTHORIZED(Status.UNAUTHORIZED, false), + NOT_FOUND(Status.NOT_FOUND, true), + CONFLICT(Status.CONFLICT, true), + UNPROCESSABLE_ENTITY(422, true), + FORBIDDEN(Status.FORBIDDEN, true); + + public final int statusCode; + public final boolean loggable; + + FaultType(int statusCode, boolean loggable) { + this.statusCode = statusCode; + this.loggable = loggable; + } + + FaultType(Status status, boolean loggable) { + this.statusCode = status.getStatusCode(); + this.loggable = loggable; + } + + @Override + public String toString() { + return name().toLowerCase(); + } + } + + private static class WebAppException extends WebApplicationException { + private static final long serialVersionUID = 1L; + + public WebAppException(FaultType faultType, String message) { + super(Response.status(faultType.statusCode).entity(message).type(MediaType.APPLICATION_JSON) + .build()); + } + } + + private Exceptions() {} + + public static WebApplicationException badRequest(String msg, Object... args) { + return new WebAppException(FaultType.BAD_REQUEST, buildLoggedErrorMessage( + FaultType.BAD_REQUEST, msg, args)); + } + + /** + * Builds and returns an error message containing an error code, and logs the message with the + * corresponding error code. + */ + public static String buildLoggedErrorMessage(FaultType faultType, String message, Object... args) { + return buildLoggedErrorMessage(faultType, + args == null || args.length == 0 ? message : String.format(message, args), null, null); + } + + /** + * Builds and returns an error message containing an error code, and logs the message with the + * corresponding error code. + */ + public static String buildLoggedErrorMessage(FaultType faultType, String message, + @Nullable String details, @Nullable Throwable exception) { + String errorCode = Long.toHexString(RANDOM.nextLong()); + + if (faultType.loggable) { + String withoutDetails = "{} {} - {}"; + String withDetails = "{} {} - {} {}"; + + if (details == null) { + if (exception == null) + LOG.error(withoutDetails, faultType.name(), errorCode, message); + else + LOG.error(withoutDetails, faultType.name(), errorCode, message, exception); + } else { + if (exception == null) + LOG.error(withDetails, faultType.name(), errorCode, message, details); + else + LOG.error(withDetails, faultType.name(), errorCode, message, details, exception); + } + } + + try { + StringBuilder str = new StringBuilder("{\""); + str.append(faultType.toString()); + str.append("\":"); + str.append(OBJECT_MAPPER.writeValueAsString(new ErrorMessage(faultType.statusCode, message, + details, errorCode))); + str.append("}"); + return str.toString(); + } catch (JsonProcessingException bestEffort) { + return null; + } + } + + public static WebApplicationException forbidden(String msg, Object... args) { + return new WebAppException(FaultType.FORBIDDEN, buildLoggedErrorMessage(FaultType.FORBIDDEN, + msg, args)); + } + + /** + * Returns the first line off of a stacktrace message. + */ + public static String stripLocationFromStacktrace(String message) { + for (String s : LINE_SPLITTER.split(message)) + return s; + return message; + } + + /** + * Indicates that the content of a a POSTed request entity is invalid. + */ + public static WebApplicationException unprocessableEntity(String msg, Object... args) { + return new WebAppException(FaultType.UNPROCESSABLE_ENTITY, buildLoggedErrorMessage( + FaultType.UNPROCESSABLE_ENTITY, msg, args)); + } + + /** + * Indicates that the content of a a POSTed request entity is invalid. + */ + public static WebApplicationException unprocessableEntityDetails(String msg, String details, + Exception exception) { + return new WebAppException(FaultType.UNPROCESSABLE_ENTITY, buildLoggedErrorMessage( + FaultType.UNPROCESSABLE_ENTITY, msg, details, exception)); + } +} diff --git a/java/src/main/java/monasca/log/api/resource/exception/IllegalArgumentExceptionMapper.java b/java/src/main/java/monasca/log/api/resource/exception/IllegalArgumentExceptionMapper.java new file mode 100644 index 00000000..723aa28a --- /dev/null +++ b/java/src/main/java/monasca/log/api/resource/exception/IllegalArgumentExceptionMapper.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. + * + * 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 monasca.log.api.resource.exception; + +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + +import monasca.log.api.resource.exception.Exceptions.FaultType; + +@Provider +public class IllegalArgumentExceptionMapper implements ExceptionMapper { + @Override + public Response toResponse(IllegalArgumentException e) { + return Response.status(Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON) + .entity(Exceptions.buildLoggedErrorMessage(FaultType.BAD_REQUEST, e.getMessage())).build(); + } +} diff --git a/java/src/main/java/monasca/log/api/resource/exception/JsonMappingExceptionManager.java b/java/src/main/java/monasca/log/api/resource/exception/JsonMappingExceptionManager.java new file mode 100644 index 00000000..67ce6508 --- /dev/null +++ b/java/src/main/java/monasca/log/api/resource/exception/JsonMappingExceptionManager.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. + * + * 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 monasca.log.api.resource.exception; + +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + +import com.fasterxml.jackson.databind.JsonMappingException; + +import monasca.log.api.resource.exception.Exceptions.FaultType; + +/** + * Adapted from Dropwizard's JsonMappingExceptionManager. + */ +@Provider +public class JsonMappingExceptionManager implements ExceptionMapper { + @Override + public Response toResponse(JsonMappingException exception) { + return Response + .status(FaultType.BAD_REQUEST.statusCode) + .type(MediaType.APPLICATION_JSON) + .entity( + Exceptions.buildLoggedErrorMessage(FaultType.BAD_REQUEST, + "Unable to process the provided JSON", + Exceptions.stripLocationFromStacktrace(exception.getMessage()), null)).build(); + } +} diff --git a/java/src/main/java/monasca/log/api/resource/exception/JsonProcessingExceptionMapper.java b/java/src/main/java/monasca/log/api/resource/exception/JsonProcessingExceptionMapper.java new file mode 100644 index 00000000..e400ea3c --- /dev/null +++ b/java/src/main/java/monasca/log/api/resource/exception/JsonProcessingExceptionMapper.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. + * + * 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 monasca.log.api.resource.exception; + +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + +import com.fasterxml.jackson.core.JsonGenerationException; +import com.fasterxml.jackson.core.JsonProcessingException; + +import monasca.log.api.resource.exception.Exceptions.FaultType; + +/** + * Adapted from Dropwizard's JsonProcessingExceptionMapper. + */ +@Provider +public class JsonProcessingExceptionMapper implements ExceptionMapper { + @Override + public Response toResponse(JsonProcessingException exception) { + /* + * If the error is in the JSON generation, it's a server error. + */ + if (exception instanceof JsonGenerationException) + return Response + .status(Status.INTERNAL_SERVER_ERROR) + .type(MediaType.APPLICATION_JSON) + .entity( + Exceptions.buildLoggedErrorMessage(FaultType.SERVER_ERROR, "Error generating JSON", + null, exception)).build(); + + final String message = exception.getMessage(); + + /* + * If we can't deserialize the JSON because someone forgot a no-arg constructor, it's a server + * error and we should inform the developer. + */ + if (message.startsWith("No suitable constructor found")) + return Response + .status(Status.INTERNAL_SERVER_ERROR) + .type(MediaType.APPLICATION_JSON) + .entity( + Exceptions.buildLoggedErrorMessage(FaultType.SERVER_ERROR, + "Unable to deserialize the provided JSON", null, exception)).build(); + + /* + * Otherwise, it's those pesky users. + */ + return Response + .status(Status.BAD_REQUEST) + .type(MediaType.APPLICATION_JSON) + .entity( + Exceptions.buildLoggedErrorMessage(FaultType.BAD_REQUEST, + "Unable to process the provided JSON", + Exceptions.stripLocationFromStacktrace(message), exception)).build(); + } +} diff --git a/java/src/main/java/monasca/log/api/resource/exception/ThrowableExceptionMapper.java b/java/src/main/java/monasca/log/api/resource/exception/ThrowableExceptionMapper.java new file mode 100644 index 00000000..5ea58457 --- /dev/null +++ b/java/src/main/java/monasca/log/api/resource/exception/ThrowableExceptionMapper.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. + * + * 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 monasca.log.api.resource.exception; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + +import monasca.log.api.resource.exception.Exceptions.FaultType; + +/** + * Adapted from Dropwizard's LoggingExceptionMapper. + * + * @param Exception type + */ +@Provider +public class ThrowableExceptionMapper implements ExceptionMapper { + @Override + public Response toResponse(E exception) { + if (exception instanceof WebApplicationException) + return ((WebApplicationException) exception).getResponse(); + + return Response + .status(Status.INTERNAL_SERVER_ERROR) + .type(MediaType.APPLICATION_JSON) + .entity( + Exceptions.buildLoggedErrorMessage(FaultType.SERVER_ERROR, + "An internal server error occurred", null, exception)).build(); + } +} diff --git a/java/src/test/java/monasca/log/api/MonApiApplicationRunner.java b/java/src/test/java/monasca/log/api/MonApiApplicationRunner.java new file mode 100644 index 00000000..4e62d34b --- /dev/null +++ b/java/src/test/java/monasca/log/api/MonApiApplicationRunner.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. + * + * 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 monasca.log.api; + +import monasca.log.api.MonApiApplication; + +public class MonApiApplicationRunner { + public static void main(String... args) throws Exception { + MonApiApplication.main(new String[] {"server", "config-local.yml"}); + } +} diff --git a/java/src/test/java/monasca/log/api/app/LogServiceTest.java b/java/src/test/java/monasca/log/api/app/LogServiceTest.java new file mode 100644 index 00000000..cb2f6337 --- /dev/null +++ b/java/src/test/java/monasca/log/api/app/LogServiceTest.java @@ -0,0 +1,64 @@ +/* + * Copyright 2015 Fujitsu Limited + * + * 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 monasca.log.api.app; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import kafka.javaapi.producer.Producer; +import kafka.producer.KeyedMessage; + +import org.mockito.Mockito; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import monasca.log.api.ApiConfig; +import monasca.log.api.model.Log; + +@Test +public class LogServiceTest { + private final String APPLICATION_TYPE = "Application/Json"; + private final Map dimensions = new HashMap(); + private final String MESSAGE = "message"; + private final String TENANT_ID = "Fujitsu"; + private final String TOPIC = "topic"; + private final String REGION = "region"; + + private ApiConfig config; + private Producer producer; + LogService logService; + + @BeforeMethod + protected void beforeMethod() { + + dimensions.clear(); + dimensions.put("a", "b"); + config = new ApiConfig(); + config.region = REGION; + config.logTopic = TOPIC; + producer = Mockito.mock(Producer.class); + } + + public void testCreate() { + String key = "FujitsuApplication/Jsonab"; + String date = Long.toString(new Date().getTime()).substring(0, 10); + String json = "{\"log\":{\"application_type\":\"Application/Json\",\"dimensions\":{\"a\":\"b\"},\"message\":\"message\"},\"meta\":{\"tenantId\":\"Fujitsu\",\"region\":\"region\"},\"creation_time\":"+ date +"}"; + KeyedMessage keyMessage = new KeyedMessage<>("topic", key, json); + Log log = new Log(APPLICATION_TYPE, dimensions, MESSAGE); + logService = new LogService(config, producer); + logService.create(log, TENANT_ID); + Mockito.verify(producer, Mockito.times(1)).send(keyMessage); + } +} diff --git a/java/src/test/java/monasca/log/api/app/command/CreateLogCommandTest.java b/java/src/test/java/monasca/log/api/app/command/CreateLogCommandTest.java new file mode 100644 index 00000000..92e6b398 --- /dev/null +++ b/java/src/test/java/monasca/log/api/app/command/CreateLogCommandTest.java @@ -0,0 +1,101 @@ +/* + * Copyright 2015 Fujitsu Limited + * + * 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 monasca.log.api.app.command; + +import static org.testng.AssertJUnit.assertEquals; +import static org.testng.AssertJUnit.assertFalse; +import static org.testng.AssertJUnit.assertTrue; + +import java.util.HashMap; +import java.util.Map; + +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import monasca.log.api.app.command.CreateLogCommand; +import monasca.log.api.model.Log; + +@Test +public class CreateLogCommandTest { + private final String APPLICATION_TYPE = "Application/Json"; + private final Map dimensions = new HashMap(); + private final String MESSAGE = "message"; + CreateLogCommand createLogCommand; + + @BeforeMethod + protected void beforeMethod() { + dimensions.clear(); + dimensions.put("a", "b"); + createLogCommand = new CreateLogCommand(APPLICATION_TYPE, dimensions, MESSAGE); + } + + public void testHashEquals() { + CreateLogCommand createLogCommandTmp1 = new CreateLogCommand(APPLICATION_TYPE, dimensions, MESSAGE); + CreateLogCommand createLogCommandTmp2 = new CreateLogCommand(APPLICATION_TYPE, dimensions, ""); + CreateLogCommand createLogCommandTmp3 = new CreateLogCommand(APPLICATION_TYPE, dimensions, null); + CreateLogCommand createLogCommandTmp4 = new CreateLogCommand(APPLICATION_TYPE, null, MESSAGE); + dimensions.clear(); + dimensions.put("1", "2"); + CreateLogCommand createLogCommandTmp5 = new CreateLogCommand(APPLICATION_TYPE, dimensions, MESSAGE); + CreateLogCommand createLogCommandTmp6 = new CreateLogCommand(null, dimensions, MESSAGE); + CreateLogCommand createLogCommandTmp7 = new CreateLogCommand("", dimensions, MESSAGE); + + assertFalse(createLogCommand.equals(new String())); + assertFalse(createLogCommand.equals(null)); + assertTrue(createLogCommand.equals(createLogCommand)); + assertTrue(createLogCommand.equals(createLogCommandTmp1)); + assertFalse(createLogCommand.equals(createLogCommandTmp2)); + assertFalse(createLogCommand.equals(createLogCommandTmp3)); + assertFalse(createLogCommand.equals(createLogCommandTmp4)); + assertFalse(createLogCommand.equals(createLogCommandTmp5)); + assertFalse(createLogCommand.equals(createLogCommandTmp6)); + assertFalse(createLogCommand.equals(createLogCommandTmp7)); + + assertEquals(createLogCommand.hashCode(), -446078499); + + createLogCommand = new CreateLogCommand(); + createLogCommand.message = "a"; + assertFalse(createLogCommand.equals(new CreateLogCommand(APPLICATION_TYPE, null, "a"))); + assertFalse(createLogCommand.equals(new CreateLogCommand(null, dimensions, "a"))); + assertTrue(createLogCommand.equals(new CreateLogCommand(null, null, "a"))); + assertEquals(createLogCommand.hashCode(), 2889727); + assertFalse(new CreateLogCommand(APPLICATION_TYPE, dimensions, "a").equals(new CreateLogCommand("", dimensions, "a"))); + + + } + + public void testSetDimensions() { + Map validatedDimensions = new HashMap(); + validatedDimensions.put("1", "2"); + dimensions.clear(); + dimensions.put(" 1", "2 "); + createLogCommand.setDimensions(dimensions); + assertEquals(createLogCommand.dimensions, validatedDimensions); + } + + public void testSetApplicationType() { + createLogCommand.setApplicationType(" aa "); + assertEquals(createLogCommand.applicationType, "aa"); + } + + public void testToLog() { + Log log = new Log(APPLICATION_TYPE, dimensions, MESSAGE); + assertEquals(createLogCommand.toLog(), log); + } + + @Test(expectedExceptions = Exception.class) + public void testValidate() { + createLogCommand.validate(); + } +} diff --git a/java/src/test/java/monasca/log/api/app/validation/LogApplicationTypeValidationTest.java b/java/src/test/java/monasca/log/api/app/validation/LogApplicationTypeValidationTest.java new file mode 100644 index 00000000..33877b43 --- /dev/null +++ b/java/src/test/java/monasca/log/api/app/validation/LogApplicationTypeValidationTest.java @@ -0,0 +1,91 @@ +/* + * Copyright 2015 Fujitsu Limited + * + * 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 monasca.log.api.app.validation; + +import static org.testng.AssertJUnit.assertEquals; +import static org.testng.AssertJUnit.assertNull; +import static org.testng.AssertJUnit.assertTrue; + +import java.io.IOException; + +import javax.ws.rs.WebApplicationException; + +import org.testng.annotations.Test; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; + +import monasca.log.api.app.validation.LogApplicationTypeValidator; + +@Test +public class LogApplicationTypeValidationTest { + + private final String toNormalize = " Json Application "; + private final String normalized = "Json Application"; + + public void testNormalize() { + + String result = LogApplicationTypeValidator.normalize(toNormalize); + assertEquals(normalized, result); + result = LogApplicationTypeValidator.normalize(null); + assertNull(result); + } + + public void testValidateWrongFormat() throws JsonParseException, IOException { + int exceptionCount = 0; + try { + LogApplicationTypeValidator.validate(normalized); + } catch (WebApplicationException e) { + exceptionCount++; + String msg = getMessage((String) e.getResponse().getEntity()); + assertEquals(msg, "Application type Json Application may only contain: a-z A-Z 0-9 _ - ."); + } + + assertEquals("Method throws Exception with correct message", 1, exceptionCount); + } + + public void testValidateWrongLength() throws JsonParseException, IOException { + StringBuilder message = new StringBuilder(); + int exceptionCount = 0; + for (int i = 0; i < 256; i++) { + message.append('a'); + } + try { + LogApplicationTypeValidator.validate(message.toString()); + } catch (WebApplicationException e) { + exceptionCount++; + String msg = getMessage((String) e.getResponse().getEntity()); + assertTrue(msg.contains("must be 255 characters or less")); + } + assertEquals("Method throws Exception with correct message", 1, exceptionCount); + } + + private String getMessage(String json) throws JsonParseException, IOException { + JsonFactory factory = new JsonFactory(); + JsonParser jp = factory.createParser(json); + jp.nextToken(); + while (jp.nextToken() != JsonToken.END_OBJECT) { + String fieldname = jp.getCurrentName(); + jp.nextToken(); + if ("message".equals(fieldname)) { + + return jp.getText(); + } + } + jp.close(); + return null; + } +} diff --git a/java/src/test/java/monasca/log/api/model/LogTest.java b/java/src/test/java/monasca/log/api/model/LogTest.java new file mode 100644 index 00000000..c74fabf9 --- /dev/null +++ b/java/src/test/java/monasca/log/api/model/LogTest.java @@ -0,0 +1,263 @@ +/* + * Copyright 2015 FUJITSU LIMITED + * + * 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 monasca.log.api.model; + +import static org.testng.Assert.assertEquals; + +import java.io.UnsupportedEncodingException; +import java.util.SortedMap; +import java.util.TreeMap; + +import org.testng.annotations.Test; + +@Test +public class LogTest { + public void shouldSerializeValue() { + String applicationType = "apache"; + SortedMap dimensions = new TreeMap(); + dimensions.put("app_name", "WebService01"); + dimensions.put("environment", "production"); + String message = "Hello, world!"; + Log log = new Log(applicationType, dimensions, message); + String json = Logs.toJson(log); + assertEquals( + json, + "{\"application_type\":\"apache\",\"dimensions\":{\"app_name\":\"WebService01\",\"environment\":\"production\"}," + + "\"message\":\"Hello, world!\"}"); + } + + public void shouldSerializeValueWithNull() { + String applicationType = null; + SortedMap dimensions = new TreeMap(); + dimensions.put("app_name", "WebService01"); + dimensions.put("environment", "production"); + String message = "Hello, world!"; + Log log = new Log(applicationType, dimensions, message); + String json = Logs.toJson(log); + assertEquals( + json, + "{\"dimensions\":{\"app_name\":\"WebService01\",\"environment\":\"production\"}," + + "\"message\":\"Hello, world!\"}"); + } + + public void shouldSerializeValueWithNull_2() { + String applicationType = "apache"; + SortedMap dimensions = null; + String message = "Hello, world!"; + Log log = new Log(applicationType, dimensions, message); + String json = Logs.toJson(log); + assertEquals( + json, + "{\"application_type\":\"apache\"," + "\"message\":\"Hello, world!\"}"); + } + + public void shouldSerializeValueWithNull_3() { + String applicationType = null; + SortedMap dimensions = null; + String message = "Hello, world!"; + Log log = new Log(applicationType, dimensions, message); + String json = Logs.toJson(log); + assertEquals( + json, + "{\"message\":\"Hello, world!\"}"); + } + + public void shouldSerializeValueWithEmpty() { + String applicationType = ""; + SortedMap dimensions = new TreeMap(); + dimensions.put("app_name", "WebService01"); + dimensions.put("environment", "production"); + String message = "Hello, world!"; + Log log = new Log(applicationType, dimensions, message); + String json = Logs.toJson(log); + assertEquals( + json, + "{\"dimensions\":{\"app_name\":\"WebService01\",\"environment\":\"production\"}," + + "\"message\":\"Hello, world!\"}"); + } + + public void shouldSerializeValueWithEmpty_2() { + String applicationType = "apache"; + SortedMap dimensions = new TreeMap(); + dimensions.put("app_name", "WebService01"); + dimensions.put("environment", "production"); + String message = ""; + Log log = new Log(applicationType, dimensions, message); + String json = Logs.toJson(log); + assertEquals( + json, + "{\"application_type\":\"apache\",\"dimensions\":{\"app_name\":\"WebService01\",\"environment\":\"production\"}," + + "\"message\":\"\"}"); + } + + public void shouldSerializeValueWithEmpty_3() { + String applicationType = ""; + SortedMap dimensions = new TreeMap(); + dimensions.put("app_name", "WebService01"); + dimensions.put("environment", "production"); + String message = ""; + Log log = new Log(applicationType, dimensions, message); + String json = Logs.toJson(log); + assertEquals( + json, + "{\"dimensions\":{\"app_name\":\"WebService01\",\"environment\":\"production\"}," + + "\"message\":\"\"}"); + } + + public void shouldSerializeAndDeserialize() { + String applicationType = "apache"; + SortedMap dimensions = new TreeMap(); + dimensions.put("app_name", "WebService01"); + dimensions.put("environment", "production"); + dimensions.put("instance_id", "123"); + String message = "Hello, world!"; + Log expected = new Log(applicationType, dimensions, message); + + Log log = Logs.fromJson(Logs.toJson(expected).getBytes()); + assertEquals(log, expected); + } + + public void shouldSerializeAndDeserializeWithNull() { + String applicationType = null; + SortedMap dimensions = new TreeMap(); + dimensions.put("app_name", "WebService01"); + dimensions.put("environment", "production"); + dimensions.put("instance_id", "123"); + String message = "Hello, world!"; + Log expected = new Log(applicationType, dimensions, message); + + Log log = Logs.fromJson(Logs.toJson(expected).getBytes()); + assertEquals(log, expected); + } + + public void shouldSerializeAndDeserializeWithNull_2() { + String applicationType = "apache"; + SortedMap dimensions = null; + String message = "Hello, world!"; + Log expected = new Log(applicationType, dimensions, message); + + Log log = Logs.fromJson(Logs.toJson(expected).getBytes()); + assertEquals(log, expected); + } + + public void shouldSerializeAndDeserializeWithNull_3() { + String applicationType = null; + SortedMap dimensions = null; + String message = "Hello, world!"; + Log expected = new Log(applicationType, dimensions, message); + + Log log = Logs.fromJson(Logs.toJson(expected).getBytes()); + assertEquals(log, expected); + } + + public void shouldSerializeAndDeserializeWithEmpty() { + String applicationType = ""; + SortedMap dimensions = new TreeMap(); + dimensions.put("app_name", "WebService01"); + dimensions.put("environment", "production"); + dimensions.put("instance_id", "123"); + String message = "Hello, world!"; + Log expected = new Log(applicationType, dimensions, message); + + Log log = Logs.fromJson(Logs.toJson(expected).getBytes()); + assertEquals(log, expected); + } + + public void shouldSerializeAndDeserializeWithEmpty_2() { + String applicationType = "apache"; + SortedMap dimensions = new TreeMap(); + dimensions.put("app_name", "WebService01"); + dimensions.put("environment", "production"); + dimensions.put("instance_id", "123"); + String message = ""; + Log expected = new Log(applicationType, dimensions, message); + + Log log = Logs.fromJson(Logs.toJson(expected).getBytes()); + assertEquals(log, expected); + } + + public void shouldSerializeAndDeserializeWithEmpty_3() { + String applicationType = ""; + SortedMap dimensions = new TreeMap(); + dimensions.put("app_name", "WebService01"); + dimensions.put("environment", "production"); + dimensions.put("instance_id", "123"); + String message = ""; + Log expected = new Log(applicationType, dimensions, message); + + Log log = Logs.fromJson(Logs.toJson(expected).getBytes()); + assertEquals(log, expected); + } + + public void shouldSerializeValueUTF() { + String applicationType = "foôbár"; + SortedMap dimensions = new TreeMap(); + dimensions.put("app_name", "foôbár"); + dimensions.put("instance_id", "123"); + String message = "boôbár"; + Log log = new Log(applicationType, dimensions, message); + String json = Logs.toJson(log); + assertEquals( + json, + "{\"application_type\":\"foôbár\",\"dimensions\":{\"app_name\":\"foôbár\",\"instance_id\":\"123\"}," + + "\"message\":\"boôbár\"}"); + } + + public void shouldSerializeAndDeserializeUTF8() throws UnsupportedEncodingException { + String applicationType = "foôbár"; + SortedMap dimensions = new TreeMap(); + dimensions.put("app_name", "foôbár"); + dimensions.put("environment", "production"); + dimensions.put("instance_id", "123"); + String message = "foôbár"; + Log expected = new Log(applicationType, dimensions, message); + + Log log = Logs.fromJson(Logs.toJson(expected).getBytes("UTF-8")); + assertEquals(log, expected); + } + + public void shouldSerializeAndDeserializeUTF8_2() throws UnsupportedEncodingException { + String applicationType = "fo\u00f4b\u00e1r"; + SortedMap dimensions = new TreeMap(); + dimensions.put("app_name", "fo\u00f4b\u00e1r"); + dimensions.put("environment", "production"); + dimensions.put("instance_id", "123"); + String message = "fo\u00f4b\u00e1r"; + Log expected = new Log(applicationType, dimensions, message); + + Log log = Logs.fromJson(Logs.toJson(expected).getBytes("UTF-8")); + assertEquals(log, expected); + } + + public void shouldSerializeAndDeserializeUTF8_3() throws UnsupportedEncodingException { + String applicationType = "fo\u00f4b\u00e1r"; + String applicationType2 = "foôbár"; + SortedMap dimensions = new TreeMap(); + dimensions.put("app_name", "fo\u00f4b\u00e1r"); + dimensions.put("environment", "production"); + dimensions.put("instance_id", "123"); + SortedMap dimensions2 = new TreeMap(); + dimensions2.put("app_name", "foôbár"); + dimensions2.put("environment", "production"); + dimensions2.put("instance_id", "123"); + String message = "fo\u00f4b\u00e1r"; + String message2 = "foôbár"; + Log expected_escaped = new Log(applicationType, dimensions, message); + Log expected_nonescaped = new Log(applicationType2, dimensions2, message2); + + Log log = Logs.fromJson(Logs.toJson(expected_escaped).getBytes("UTF-8")); + assertEquals(log, expected_nonescaped); + } +} diff --git a/java/src/test/java/monasca/log/api/resource/AbstractMonApiResourceTest.java b/java/src/test/java/monasca/log/api/resource/AbstractMonApiResourceTest.java new file mode 100644 index 00000000..53364b3c --- /dev/null +++ b/java/src/test/java/monasca/log/api/resource/AbstractMonApiResourceTest.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. + * + * 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 monasca.log.api.resource; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.PropertyNamingStrategy; + +import monasca.common.dropwizard.AbstractResourceTest; +import monasca.log.api.resource.exception.ConstraintViolationExceptionMapper; +import monasca.log.api.resource.exception.IllegalArgumentExceptionMapper; +import monasca.log.api.resource.exception.JsonMappingExceptionManager; +import monasca.log.api.resource.exception.JsonProcessingExceptionMapper; +import monasca.log.api.resource.exception.ThrowableExceptionMapper; + + +/** + * Support class for monitoring resource tests. + */ +public abstract class AbstractMonApiResourceTest extends AbstractResourceTest { + @Override + protected void setupResources() throws Exception { + addSingletons(new IllegalArgumentExceptionMapper(), new JsonProcessingExceptionMapper(), new JsonMappingExceptionManager(), + new ConstraintViolationExceptionMapper(), new ThrowableExceptionMapper() {}); + + objectMapper.setPropertyNamingStrategy(PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES); + objectMapper.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY); + } +} diff --git a/java/src/test/java/monasca/log/api/resource/LogResourceTest.java b/java/src/test/java/monasca/log/api/resource/LogResourceTest.java new file mode 100644 index 00000000..1967c8be --- /dev/null +++ b/java/src/test/java/monasca/log/api/resource/LogResourceTest.java @@ -0,0 +1,701 @@ +/* + * Copyright 2015 Fujitsu Limited + * + * 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 monasca.log.api.resource; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.testng.Assert.assertEquals; + +import java.util.HashMap; +import java.util.Map; + +import javax.ws.rs.core.MediaType; + +import org.testng.annotations.Test; + +import com.sun.jersey.api.client.ClientResponse; +import com.sun.jersey.api.client.WebResource; + +import monasca.log.api.app.LogService; +import monasca.log.api.app.command.CreateLogCommand; +import monasca.log.api.model.Log; +import monasca.log.api.resource.exception.ErrorMessages; + +@Test +public class LogResourceTest extends AbstractMonApiResourceTest { + private String applicationType; + private String dimensionsStr; + private Map dimensionsMap; + private String jsonMessage; + private String textMessage; + private String tenantId; + private String longString; + private LogService service; + + @Override + @SuppressWarnings("unchecked") + protected void setupResources() throws Exception { + super.setupResources(); + applicationType = "apache"; + dimensionsStr = "app_name:WebService01,environment:production"; + dimensionsMap = new HashMap(); + dimensionsMap.put("app_name", "WebService01"); + dimensionsMap.put("environment", "production"); + jsonMessage = "{\n \"message\":\"Hello, world!\",\n \"from\":\"hoover\"\n}"; + textMessage = "Hello, world!"; + tenantId = "abc"; + longString = + "12345678901234567890123456789012345678901234567890" + "12345678901234567890123456789012345678901234567890" + + "12345678901234567890123456789012345678901234567890" + "12345678901234567890123456789012345678901234567890" + + "12345678901234567890123456789012345678901234567890123456"; + + service = mock(LogService.class); + doNothing().when(service).create(any(Log.class), anyString()); + + addResources(new LogResource(service)); + } + + @SuppressWarnings("unchecked") + public void shouldCreateJsonMessage() { + ClientResponse response = createResponseForJson(null, null, jsonMessage); + Log log = new Log(null, null, jsonMessage); + + assertEquals(response.getStatus(), 204); + verify(service).create(eq(log), eq(tenantId)); + } + + @SuppressWarnings("unchecked") + public void shouldCreateJsonMessageByText() { + ClientResponse response = createResponseForText(null, null, jsonMessage); + Log log = new Log(null, null, jsonMessage); + + assertEquals(response.getStatus(), 204); + verify(service).create(eq(log), eq(tenantId)); + } + + @SuppressWarnings("unchecked") + public void shouldCreateTextMessage() { + ClientResponse response = createResponseForText(null, null, textMessage); + Log log = new Log(null, null, textMessage); + + assertEquals(response.getStatus(), 204); + verify(service).create(eq(log), eq(tenantId)); + } + + @SuppressWarnings("unchecked") + public void shouldCreateTextMessageByJson() { + ClientResponse response = createResponseForJson(null, null, textMessage); + Log log = new Log(null, null, textMessage); + + assertEquals(response.getStatus(), 204); + verify(service).create(eq(log), eq(tenantId)); + } + + @SuppressWarnings("unchecked") + public void shouldCreateEmptyMessageByJson() { + ClientResponse response = createResponseForJson(null, null, ""); + Log log = new Log(null, null, ""); + + assertEquals(response.getStatus(), 204); + verify(service).create(eq(log), eq(tenantId)); + } + + @SuppressWarnings("unchecked") + public void shouldCreateEmptyMessageByText() { + ClientResponse response = createResponseForText(null, null, ""); + Log log = new Log(null, null, ""); + + assertEquals(response.getStatus(), 204); + verify(service).create(eq(log), eq(tenantId)); + } + + @SuppressWarnings("unchecked") + public void shouldCreateJsonMessageWithType() { + ClientResponse response = createResponseForJson(applicationType, null, jsonMessage); + Log log = new Log(applicationType, null, jsonMessage); + + assertEquals(response.getStatus(), 204); + verify(service).create(eq(log), eq(tenantId)); + } + + @SuppressWarnings("unchecked") + public void shouldCreateTextMessageWithType() { + ClientResponse response = createResponseForText(applicationType, null, textMessage); + Log log = new Log(applicationType, null, textMessage); + + assertEquals(response.getStatus(), 204); + verify(service).create(eq(log), eq(tenantId)); + } + + @SuppressWarnings("unchecked") + public void shouldCreateJsonMessageWithEmptyType() { + ClientResponse response = createResponseForJson("", null, jsonMessage); + Log log = new Log("", null, jsonMessage); + + assertEquals(response.getStatus(), 204); + verify(service).create(eq(log), eq(tenantId)); + } + + @SuppressWarnings("unchecked") + public void shouldCreateTextMessageWithEmptyType() { + ClientResponse response = createResponseForText("", null, textMessage); + Log log = new Log("", null, textMessage); + + assertEquals(response.getStatus(), 204); + verify(service).create(eq(log), eq(tenantId)); + } + + @SuppressWarnings("unchecked") + public void shouldCreateJsonMessageWithNonNumericAZType() { + ClientResponse response = createResponseForJson("azAZ19.-_", null, jsonMessage); + Log log = new Log("azAZ19.-_", null, jsonMessage); + + assertEquals(response.getStatus(), 204); + verify(service).create(eq(log), eq(tenantId)); + } + + @SuppressWarnings("unchecked") + public void shouldCreateTextMessageWithNonNumericAZType() { + ClientResponse response = createResponseForText("azAZ19.-_", null, textMessage); + Log log = new Log("azAZ19.-_", null, textMessage); + + assertEquals(response.getStatus(), 204); + verify(service).create(eq(log), eq(tenantId)); + } + + @SuppressWarnings("unchecked") + public void shouldCreateJsonMessageWithSpaceType() { + ClientResponse response = createResponseForJson(" apache ", null, jsonMessage); + Log log = new Log("apache", null, jsonMessage); + + assertEquals(response.getStatus(), 204); + verify(service).create(eq(log), eq(tenantId)); + } + + @SuppressWarnings("unchecked") + public void shouldCreateTextMessageWithSpaceType() { + ClientResponse response = createResponseForText(" apache ", null, textMessage); + Log log = new Log("apache", null, textMessage); + + assertEquals(response.getStatus(), 204); + verify(service).create(eq(log), eq(tenantId)); + } + + @SuppressWarnings("unchecked") + public void shouldErrorOnCreateJsonMessageWithIllegalCharsInType() { + ClientResponse response = createResponseForJson("@apache", null, jsonMessage); + + ErrorMessages.assertThat(response.getEntity(String.class)).matches("unprocessable_entity", 422, + "Application type @apache may only contain: a-z A-Z 0-9 _ - ."); + } + + @SuppressWarnings("unchecked") + public void shouldErrorOnCreateTextMessageWithIllegalCharsInType() { + ClientResponse response = createResponseForText("@apache", null, textMessage); + + ErrorMessages.assertThat(response.getEntity(String.class)).matches("unprocessable_entity", 422, + "Application type @apache may only contain: a-z A-Z 0-9 _ - ."); + } + + @SuppressWarnings("unchecked") + public void shouldErrorOnCreateJsonMessageWithTooLongType() { + String tooLongType = longString; + ClientResponse response = createResponseForJson(tooLongType, null, jsonMessage); + + ErrorMessages.assertThat(response.getEntity(String.class)).matches("unprocessable_entity", 422, + String.format("Application type %s must be %d characters or less", tooLongType, CreateLogCommand.MAX_NAME_LENGTH)); + } + + @SuppressWarnings("unchecked") + public void shouldErrorOnCreateTextMessageWithTooLongType() { + String tooLongType = longString; + ClientResponse response = createResponseForText(tooLongType, null, textMessage); + + ErrorMessages.assertThat(response.getEntity(String.class)).matches("unprocessable_entity", 422, + String.format("Application type %s must be %d characters or less", tooLongType, CreateLogCommand.MAX_NAME_LENGTH)); + } + + @SuppressWarnings("unchecked") + public void shouldCreateJsonMessageWithDimensions() { + ClientResponse response = createResponseForJson(null, dimensionsStr, jsonMessage); + Log log = new Log(null, dimensionsMap, jsonMessage); + + assertEquals(response.getStatus(), 204); + verify(service).create(eq(log), eq(tenantId)); + } + + @SuppressWarnings("unchecked") + public void shouldCreateTextMessageWithDimensions() { + ClientResponse response = createResponseForText(null, dimensionsStr, textMessage); + Log log = new Log(null, dimensionsMap, textMessage); + + assertEquals(response.getStatus(), 204); + verify(service).create(eq(log), eq(tenantId)); + } + + @SuppressWarnings("unchecked") + public void shouldCreateJsonMessageWithEmptyDimensions() { + ClientResponse response = createResponseForJson(null, "", jsonMessage); + Log log = new Log(null, null, jsonMessage); + + assertEquals(response.getStatus(), 204); + verify(service).create(eq(log), eq(tenantId)); + } + + @SuppressWarnings("unchecked") + public void shouldCreateTextMessageWithEmptyDimensions() { + ClientResponse response = createResponseForText(null, "", textMessage); + Log log = new Log(null, null, textMessage); + + assertEquals(response.getStatus(), 204); + verify(service).create(eq(log), eq(tenantId)); + } + + @SuppressWarnings("unchecked") + public void shouldErrorOnCreateJsonMessageWithPartiallyEmptyDimensions() { + String partiallyEmptyDimensions = ",environment:production"; + Map expectedDimensionsMap = new HashMap(); + expectedDimensionsMap.put("environment", "production"); + + ClientResponse response = createResponseForJson(null, partiallyEmptyDimensions, jsonMessage); + + ErrorMessages.assertThat(response.getEntity(String.class)).matches("unprocessable_entity", 422, "Dimension cannot be empty"); + } + + @SuppressWarnings("unchecked") + public void shouldErrorOnCreateTextMessageWithPartiallyEmptyDimensions() { + String partiallyEmptyDimensions = ",environment:production"; + Map expectedDimensionsMap = new HashMap(); + expectedDimensionsMap.put("environment", "production"); + + ClientResponse response = createResponseForText(null, partiallyEmptyDimensions, textMessage); + + ErrorMessages.assertThat(response.getEntity(String.class)).matches("unprocessable_entity", 422, "Dimension cannot be empty"); + } + + @SuppressWarnings("unchecked") + public void shouldErrorOnCreateJsonMessageWithPartiallyEmptyDimensions_2() { + String partiallyEmptyDimensions = "environment:production,"; + Map expectedDimensionsMap = new HashMap(); + expectedDimensionsMap.put("environment", "production"); + + ClientResponse response = createResponseForJson(null, partiallyEmptyDimensions, jsonMessage); + + ErrorMessages.assertThat(response.getEntity(String.class)).matches("unprocessable_entity", 422, "Dimension cannot be empty"); + } + + @SuppressWarnings("unchecked") + public void shouldErrorOnCreateTextMessageWithPartiallyEmptyDimensions_2() { + String partiallyEmptyDimensions = "environment:production,"; + Map expectedDimensionsMap = new HashMap(); + expectedDimensionsMap.put("environment", "production"); + + ClientResponse response = createResponseForText(null, partiallyEmptyDimensions, textMessage); + + ErrorMessages.assertThat(response.getEntity(String.class)).matches("unprocessable_entity", 422, "Dimension cannot be empty"); + } + + @SuppressWarnings("unchecked") + public void shouldCreateJsonMessageWithDimensionsWithSpaceInKey() { + String spaceInKeyDimensions = " app_name :WebService01, environment :production"; + + ClientResponse response = createResponseForJson(null, spaceInKeyDimensions, jsonMessage); + Log log = new Log(null, dimensionsMap, jsonMessage); + + assertEquals(response.getStatus(), 204); + verify(service).create(eq(log), eq(tenantId)); + } + + @SuppressWarnings("unchecked") + public void shouldCreateTextMessageWithDimensionsWithSpaceInKey() { + String spaceInKeyDimensions = " app_name :WebService01, environment :production"; + + ClientResponse response = createResponseForText(null, spaceInKeyDimensions, textMessage); + Log log = new Log(null, dimensionsMap, textMessage); + + assertEquals(response.getStatus(), 204); + verify(service).create(eq(log), eq(tenantId)); + } + + @SuppressWarnings("unchecked") + public void shouldCreateJsonMessageWithDimensionsWithSpaceInValue() { + String spaceInValueDimensions = "app_name: WebService01 ,environment: production "; + + ClientResponse response = createResponseForJson(null, spaceInValueDimensions, jsonMessage); + Log log = new Log(null, dimensionsMap, jsonMessage); + + assertEquals(response.getStatus(), 204); + verify(service).create(eq(log), eq(tenantId)); + } + + @SuppressWarnings("unchecked") + public void shouldCreateTextMessageWithDimensionsWithSpaceInValue() { + String spaceInValueDimensions = "app_name: WebService01 ,environment: production "; + + ClientResponse response = createResponseForText(null, spaceInValueDimensions, textMessage); + Log log = new Log(null, dimensionsMap, textMessage); + + assertEquals(response.getStatus(), 204); + verify(service).create(eq(log), eq(tenantId)); + } + + @SuppressWarnings("unchecked") + public void shouldErrorOnCreateJsonMessageWithEmptyKeyDimensions() { + String emptyKeyDimensionsStr = ":WebService01,environment:production"; + ClientResponse response = createResponseForJson(null, emptyKeyDimensionsStr, jsonMessage); + + ErrorMessages.assertThat(response.getEntity(String.class)).matches("unprocessable_entity", 422, "Dimension name cannot be empty"); + } + + @SuppressWarnings("unchecked") + public void shouldErrorOnCreateTextMessageWithEmptyKeyDimensions() { + String emptyKeyDimensionsStr = ":WebService01,environment:production"; + ClientResponse response = createResponseForText(null, emptyKeyDimensionsStr, textMessage); + + ErrorMessages.assertThat(response.getEntity(String.class)).matches("unprocessable_entity", 422, "Dimension name cannot be empty"); + } + + @SuppressWarnings("unchecked") + public void shouldErrorOnCreateJsonMessageWithEmptyValueDimensions() { + String emptyKeyDimensionsStr = "app_name:,environment:production"; + ClientResponse response = createResponseForJson(null, emptyKeyDimensionsStr, jsonMessage); + + ErrorMessages.assertThat(response.getEntity(String.class)).matches("unprocessable_entity", 422, "Dimension app_name cannot have an empty value"); + } + + @SuppressWarnings("unchecked") + public void shouldErrorOnCreateTextMessageWithEmptyValueDimensions() { + String emptyKeyDimensionsStr = "app_name:,environment:production"; + ClientResponse response = createResponseForText(null, emptyKeyDimensionsStr, textMessage); + + ErrorMessages.assertThat(response.getEntity(String.class)).matches("unprocessable_entity", 422, "Dimension app_name cannot have an empty value"); + } + + @SuppressWarnings("unchecked") + public void shouldErrorOnCreateJsonMessageWithDimensionsWithoutColon() { + String emptyKeyDimensionsStr = "app_name,environment:production"; + ClientResponse response = createResponseForJson(null, emptyKeyDimensionsStr, jsonMessage); + + ErrorMessages.assertThat(response.getEntity(String.class)).matches("unprocessable_entity", 422, "app_name is not a valid dimension"); + } + + @SuppressWarnings("unchecked") + public void shouldErrorOnCreateTextMessageWithDimensionsWithoutColon() { + String emptyKeyDimensionsStr = "app_name,environment:production"; + ClientResponse response = createResponseForText(null, emptyKeyDimensionsStr, textMessage); + + ErrorMessages.assertThat(response.getEntity(String.class)).matches("unprocessable_entity", 422, "app_name is not a valid dimension"); + } + + @SuppressWarnings("unchecked") + public void shouldCreateJsonMessageWithMultipleColonDimensions() { + String multipleColonDimensionsStr = "app_name:WebService01:abc,environment:production"; + Map multipleColonDimensionsMap = new HashMap(); + multipleColonDimensionsMap.put("app_name", "WebService01:abc"); + multipleColonDimensionsMap.put("environment", "production"); + ClientResponse response = createResponseForJson(null, multipleColonDimensionsStr, jsonMessage); + Log log = new Log(null, multipleColonDimensionsMap, jsonMessage); + + assertEquals(response.getStatus(), 204); + verify(service).create(eq(log), eq(tenantId)); + } + + @SuppressWarnings("unchecked") + public void shouldCreateTextMessageWithMultipleColonDimensions() { + String multipleColonDimensionsStr = "app_name:WebService01:abc,environment:production"; + Map multipleColonDimensionsMap = new HashMap(); + multipleColonDimensionsMap.put("app_name", "WebService01:abc"); + multipleColonDimensionsMap.put("environment", "production"); + ClientResponse response = createResponseForText(null, multipleColonDimensionsStr, textMessage); + Log log = new Log(null, multipleColonDimensionsMap, textMessage); + + assertEquals(response.getStatus(), 204); + verify(service).create(eq(log), eq(tenantId)); + } + + @SuppressWarnings("unchecked") + public void shouldCreateJsonMessageWithDimensionsWithoutIllgalCharsInKey() { + String legalCharsInKeyDimensionsStr = "azAZ19.-_:WebService01,environment:production"; + Map expectedDimensionsMap = new HashMap(); + expectedDimensionsMap.put("azAZ19.-_", "WebService01"); + expectedDimensionsMap.put("environment", "production"); + + ClientResponse response = createResponseForJson(null, legalCharsInKeyDimensionsStr, jsonMessage); + Log log = new Log(null, expectedDimensionsMap, jsonMessage); + + assertEquals(response.getStatus(), 204); + verify(service).create(eq(log), eq(tenantId)); + } + + @SuppressWarnings("unchecked") + public void shouldCreateTextMessageWithDimensionsWithoutIllegalCharsInKey() { + String legalCharsInKeyDimensionsStr = "azAZ19.-_:WebService01,environment:production"; + Map expectedDimensionsMap = new HashMap(); + expectedDimensionsMap.put("azAZ19.-_", "WebService01"); + expectedDimensionsMap.put("environment", "production"); + + ClientResponse response = createResponseForText(null, legalCharsInKeyDimensionsStr, textMessage); + Log log = new Log(null, expectedDimensionsMap, textMessage); + + assertEquals(response.getStatus(), 204); + verify(service).create(eq(log), eq(tenantId)); + } + + @SuppressWarnings("unchecked") + public void shouldErrorOnCreateJsonMessageWithIllegalCharsInKey() { + String illegalCharsInKeyDimensionsStr = "{app_name:WebService01,environment:production"; + ClientResponse response = createResponseForJson(null, illegalCharsInKeyDimensionsStr, jsonMessage); + + ErrorMessages.assertThat(response.getEntity(String.class)).matches("unprocessable_entity", 422, + "Dimension name {app_name may not contain: > < = { } ( ) ' \" , ; &"); + } + + @SuppressWarnings("unchecked") + public void shouldErrorOnCreateTextMessageWithIllegalCharsInKey() { + String illegalCharsInKeyDimensionsStr = "{app_name:WebService01,environment:production"; + ClientResponse response = createResponseForText(null, illegalCharsInKeyDimensionsStr, textMessage); + + ErrorMessages.assertThat(response.getEntity(String.class)).matches("unprocessable_entity", 422, + "Dimension name {app_name may not contain: > < = { } ( ) ' \" , ; &"); + } + + @SuppressWarnings("unchecked") + public void shouldErrorOnCreateJsonMessageWithIllegalCharsInKey_2() { + String illegalCharsInKeyDimensionsStr = "app=name:WebService01,environment:production"; + ClientResponse response = createResponseForJson(null, illegalCharsInKeyDimensionsStr, jsonMessage); + + ErrorMessages.assertThat(response.getEntity(String.class)).matches("unprocessable_entity", 422, + "Dimension name app=name may not contain: > < = { } ( ) ' \" , ; &"); + } + + @SuppressWarnings("unchecked") + public void shouldErrorOnCreateTextMessageWithIllegalCharsInKey_2() { + String illegalCharsInKeyDimensionsStr = "app=name:WebService01,environment:production"; + ClientResponse response = createResponseForText(null, illegalCharsInKeyDimensionsStr, textMessage); + + ErrorMessages.assertThat(response.getEntity(String.class)).matches("unprocessable_entity", 422, + "Dimension name app=name may not contain: > < = { } ( ) ' \" , ; &"); + } + + @SuppressWarnings("unchecked") + public void shouldErrorOnCreateJsonMessageWithLegalCharsInValue() { + String legalCharsInValueDimensionsStr = "app_name:@WebService01,environment:production"; + Map legalCharsInValueDimensionsMap = new HashMap(); + legalCharsInValueDimensionsMap.put("app_name", "@WebService01"); + legalCharsInValueDimensionsMap.put("environment", "production"); + ClientResponse response = createResponseForJson(null, legalCharsInValueDimensionsStr, jsonMessage); + Log log = new Log(null, legalCharsInValueDimensionsMap, jsonMessage); + + assertEquals(response.getStatus(), 204); + verify(service).create(eq(log), eq(tenantId)); + } + + @SuppressWarnings("unchecked") + public void shouldErrorOnCreateTextMessageWithLegalCharsInValue() { + String legalCharsInValueDimensionsStr = "app_name:@WebService01,environment:production"; + Map legalCharsInValueDimensionsMap = new HashMap(); + legalCharsInValueDimensionsMap.put("app_name", "@WebService01"); + legalCharsInValueDimensionsMap.put("environment", "production"); + ClientResponse response = createResponseForText(null, legalCharsInValueDimensionsStr, textMessage); + Log log = new Log(null, legalCharsInValueDimensionsMap, textMessage); + + assertEquals(response.getStatus(), 204); + verify(service).create(eq(log), eq(tenantId)); + } + + @SuppressWarnings("unchecked") + public void shouldErrorOnCreateJsonMessageWithLegalCharsInValue_2() { + String legalCharsInValueDimensionsStr = "app_name:Web Service01,environment:production"; + Map legalCharsInValueDimensionsMap = new HashMap(); + legalCharsInValueDimensionsMap.put("app_name", "Web Service01"); + legalCharsInValueDimensionsMap.put("environment", "production"); + ClientResponse response = createResponseForJson(null, legalCharsInValueDimensionsStr, jsonMessage); + Log log = new Log(null, legalCharsInValueDimensionsMap, jsonMessage); + + assertEquals(response.getStatus(), 204); + verify(service).create(eq(log), eq(tenantId)); + } + + @SuppressWarnings("unchecked") + public void shouldErrorOnCreateTextMessageWithLegalCharsInValue_2() { + String legalCharsInValueDimensionsStr = "app_name:Web Service01,environment:production"; + Map legalCharsInValueDimensionsMap = new HashMap(); + legalCharsInValueDimensionsMap.put("app_name", "Web Service01"); + legalCharsInValueDimensionsMap.put("environment", "production"); + ClientResponse response = createResponseForText(null, legalCharsInValueDimensionsStr, textMessage); + Log log = new Log(null, legalCharsInValueDimensionsMap, textMessage); + + assertEquals(response.getStatus(), 204); + verify(service).create(eq(log), eq(tenantId)); + } + + @SuppressWarnings("unchecked") + public void shouldErrorOnCreateJsonMessageWithTooLongKey() { + String tooLongKey = longString; + ClientResponse response = createResponseForJson(null, tooLongKey + ":abc", jsonMessage); + + ErrorMessages.assertThat(response.getEntity(String.class)).matches("unprocessable_entity", 422, + String.format("Dimension name %s must be 255 characters or less", tooLongKey)); + } + + @SuppressWarnings("unchecked") + public void shouldErrorOnCreateTextMessageWithTooLongKey() { + String tooLongKey = longString; + ClientResponse response = createResponseForText(null, tooLongKey + ":abc", textMessage); + + ErrorMessages.assertThat(response.getEntity(String.class)).matches("unprocessable_entity", 422, + String.format("Dimension name %s must be 255 characters or less", tooLongKey)); + } + + @SuppressWarnings("unchecked") + public void shouldErrorOnCreateJsonMessageWithTooLongValue() { + String tooLongValue = longString; + ClientResponse response = createResponseForJson(null, "abc:" + tooLongValue, jsonMessage); + + ErrorMessages.assertThat(response.getEntity(String.class)).matches("unprocessable_entity", 422, + String.format("Dimension value %s must be 255 characters or less", tooLongValue)); + } + + @SuppressWarnings("unchecked") + public void shouldErrorOnCreateTextMessageWithTooLongValue() { + String tooLongValue = longString; + ClientResponse response = createResponseForText(null, "abc:" + tooLongValue, textMessage); + + ErrorMessages.assertThat(response.getEntity(String.class)).matches("unprocessable_entity", 422, + String.format("Dimension value %s must be 255 characters or less", tooLongValue)); + } + + @SuppressWarnings("unchecked") + public void shouldCreateJsonMessageWithTypeAndDimensions() { + ClientResponse response = createResponseForJson(applicationType, dimensionsStr, jsonMessage); + Log log = new Log(applicationType, dimensionsMap, jsonMessage); + + assertEquals(response.getStatus(), 204); + verify(service).create(eq(log), eq(tenantId)); + } + + @SuppressWarnings("unchecked") + public void shouldCreateTextMessageWithTypeAndDimensions() { + ClientResponse response = createResponseForText(applicationType, dimensionsStr, textMessage); + Log log = new Log(applicationType, dimensionsMap, textMessage); + + assertEquals(response.getStatus(), 204); + verify(service).create(eq(log), eq(tenantId)); + } + + @SuppressWarnings("unchecked") + public void shouldErrorOnCreateTooLongJsonMessage() { + String tmpString = longString + longString + longString + longString; + StringBuffer buf = new StringBuffer(1024 * 1024 + 1); + buf.append("{\"a\":\""); + for (int i = 0; i < 1024; i++) { + buf.append(tmpString); + } + buf.append("\"}"); + String tooLongMessage = buf.toString(); + ClientResponse response = createResponseForJson(null, null, tooLongMessage); + + ErrorMessages.assertThat(response.getEntity(String.class)).matches("unprocessable_entity", 422, "Log must be 1048576 characters or less"); + } + + @SuppressWarnings("unchecked") + public void shouldErrorOnCreateTooLongTextMessage() { + String tmpString = longString + longString + longString + longString; + StringBuffer buf = new StringBuffer(1024 * 1024 + 1); + for (int i = 0; i < 1024; i++) { + buf.append(tmpString); + } + buf.append('0'); + String tooLongMessage = buf.toString(); + ClientResponse response = createResponseForText(null, null, tooLongMessage); + + ErrorMessages.assertThat(response.getEntity(String.class)).matches("unprocessable_entity", 422, "Log must be 1048576 characters or less"); + } + + public void shouldErrorOnCreateJsonMessageWithCrossTenant() { + ClientResponse response = createResponseForJsonWithCrossTenant(null, null, jsonMessage, "illegal-role", "def"); + + ErrorMessages.assertThat(response.getEntity(String.class)).matches("forbidden", 403, "Project abc cannot POST cross tenant"); + } + + public void shouldErrorOnCreateTextMessageWithCrossTenant() { + ClientResponse response = createResponseForTextWithCrossTenant(null, null, textMessage, "illegal-role", "def"); + + ErrorMessages.assertThat(response.getEntity(String.class)).matches("forbidden", 403, "Project abc cannot POST cross tenant"); + } + + public void shouldCreateJsonMessageWithCrossTenant() { + ClientResponse response = createResponseForJsonWithCrossTenant(null, null, jsonMessage, "monitoring-delegate", "def"); + Log log = new Log(null, null, jsonMessage); + + assertEquals(response.getStatus(), 204); + verify(service).create(eq(log), eq("def")); + } + + public void shouldCreateTextMessageWithCrossTenant() { + ClientResponse response = createResponseForTextWithCrossTenant(null, null, textMessage, "monitoring-delegate", "def"); + Log log = new Log(null, null, textMessage); + + assertEquals(response.getStatus(), 204); + verify(service).create(eq(log), eq("def")); + } + + private ClientResponse createResponseForJson(String applicationType, String dimensions, String message) { + WebResource.Builder builder = + client().resource("/v2.0/log/single").header("X-Tenant-Id", tenantId).header("Content-Type", MediaType.APPLICATION_JSON); + if (applicationType != null) + builder = builder.header("X-Application-Type", applicationType); + if (dimensions != null) + builder = builder.header("X-Dimensions", dimensions); + return builder.post(ClientResponse.class, message); + } + + private ClientResponse createResponseForText(String applicationType, String dimensions, String message) { + WebResource.Builder builder = client().resource("/v2.0/log/single").header("X-Tenant-Id", tenantId).header("Content-Type", MediaType.TEXT_PLAIN); + if (applicationType != null) + builder = builder.header("X-Application-Type", applicationType); + if (dimensions != null) + builder = builder.header("X-Dimensions", dimensions); + return builder.post(ClientResponse.class, message); + } + + private ClientResponse createResponseForJsonWithCrossTenant(String applicationType, String dimensions, String message, String roles, + String crossTenantId) { + WebResource.Builder builder = + client().resource("/v2.0/log/single?tenant_id=" + crossTenantId).header("X-Tenant-Id", tenantId).header("X-Roles", roles) + .header("Content-Type", MediaType.APPLICATION_JSON); + if (applicationType != null) + builder = builder.header("X-Application-Type", applicationType); + if (dimensions != null) + builder = builder.header("X-Dimensions", dimensions); + return builder.post(ClientResponse.class, message); + } + + private ClientResponse createResponseForTextWithCrossTenant(String applicationType, String dimensions, String message, String roles, + String crossTenantId) { + WebResource.Builder builder = + client().resource("/v2.0/log/single?tenant_id=" + crossTenantId).header("X-Tenant-Id", tenantId).header("X-Roles", roles) + .header("Content-Type", MediaType.TEXT_PLAIN); + if (applicationType != null) + builder = builder.header("X-Application-Type", applicationType); + if (dimensions != null) + builder = builder.header("X-Dimensions", dimensions); + return builder.post(ClientResponse.class, message); + } +} diff --git a/java/src/test/java/monasca/log/api/resource/exception/ErrorMessages.java b/java/src/test/java/monasca/log/api/resource/exception/ErrorMessages.java new file mode 100644 index 00000000..f9083e30 --- /dev/null +++ b/java/src/test/java/monasca/log/api/resource/exception/ErrorMessages.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. + * + * 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 monasca.log.api.resource.exception; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +import javax.annotation.Nullable; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import monasca.log.api.resource.exception.ErrorMessage; + +/** + * Error message utilities. + */ +public final class ErrorMessages { + private static final ObjectMapper MAPPER = new ObjectMapper(); + + public interface ErrorMessageMatcher { + void matches(String faultType, int code, String messagePrefix); + + void matches(String faultType, int code, String messagePrefix, @Nullable String detailsPrefix); + } + + public static ErrorMessageMatcher assertThat(final String errorMessage) { + try { + JsonNode node = MAPPER.readTree(errorMessage); + final String rootKey = node.fieldNames().next(); + node = node.get(rootKey); + final ErrorMessage message = MAPPER.reader(ErrorMessage.class).readValue(node); + + return new ErrorMessageMatcher() { + @Override + public void matches(String faultType, int code, String messagePrefix) { + matches(faultType, code, messagePrefix, null); + } + + @Override + public void matches(String faultType, int code, String messagePrefix, + @Nullable String detailsPrefix) { + assertEquals(rootKey, faultType); + assertEquals(message.code, code); + assertTrue(message.message.startsWith(messagePrefix), message.message); + if (detailsPrefix != null) + assertTrue(message.details.startsWith(detailsPrefix), message.details); + } + }; + + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/pom.xml b/pom.xml new file mode 100644 index 00000000..51fbe81c --- /dev/null +++ b/pom.xml @@ -0,0 +1,53 @@ + + 4.0.0 + + monasca + monasca-log-api-base + 1.1.0-SNAPSHOT + http://github.com/stackforge/monasca-log-api + pom + + + + ${version} ${sun.java.command} + true + UTF-8 + UTF-8 + + + + scm:git:git@github.com:stackforge/monasca-log-api + scm:git:git@github.com:stackforge/monasca-log-api + + + + + + org.codehaus.mojo + exec-maven-plugin + 1.1.1 + + + package-execution + package + + exec + + + + clean-execution + clean + + exec + + + + + run_maven.sh + + + + + + diff --git a/run_maven.sh b/run_maven.sh new file mode 100755 index 00000000..b5a153ba --- /dev/null +++ b/run_maven.sh @@ -0,0 +1,49 @@ +#!/bin/bash +# Download maven 3 if the system maven isn't maven 3 +VERSION=`mvn -v | grep "Apache Maven 3"` +if [ -z "${VERSION}" ]; then + curl http://archive.apache.org/dist/maven/binaries/apache-maven-3.2.1-bin.tar.gz > apache-maven-3.2.1-bin.tar.gz + tar -xvzf apache-maven-3.2.1-bin.tar.gz + MVN=${PWD}/apache-maven-3.2.1/bin/mvn +else + MVN=mvn +fi + +# Get the expected common version +COMMON_VERSION=$1 +# Get rid of the version argument +shift + +# Get rid of the java property name containing the args +shift + +RUN_BUILD=false +for ARG in $*; do + if [ "$ARG" = "package" ]; then + RUN_BUILD=true + fi + if [ "$ARG" = "install" ]; then + RUN_BUILD=true + fi +done + +if [ $RUN_BUILD = "true" ]; then + ( cd common; ./build_common.sh ${MVN} ${COMMON_VERSION} ) + RC=$? + if [ $RC != 0 ]; then + exit $RC + fi +fi + +# Invoke the maven 3 on the real pom.xml +( cd java; ${MVN} -DgitRevision=`git rev-list HEAD --max-count 1 --abbrev=0 --abbrev-commit` $* ) + +RC=$? + +# Copy the jars where the publisher will find them +if [ $RUN_BUILD = "true" ]; then + ln -sf java/target target +fi + +rm -fr apache-maven-3.2.1* +exit $RC