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