killbill-memoizeit

analytics: add servlet for Kaui and to rebuild accounts Signed-off-by:

4/5/2013 8:49:20 PM

Details

diff --git a/osgi-bundles/bundles/analytics/pom.xml b/osgi-bundles/bundles/analytics/pom.xml
index 751ceb6..a7fa997 100644
--- a/osgi-bundles/bundles/analytics/pom.xml
+++ b/osgi-bundles/bundles/analytics/pom.xml
@@ -29,6 +29,14 @@
     <packaging>bundle</packaging>
     <dependencies>
         <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.datatype</groupId>
+            <artifactId>jackson-datatype-joda</artifactId>
+        </dependency>
+        <dependency>
             <groupId>com.google.guava</groupId>
             <artifactId>guava</artifactId>
         </dependency>
diff --git a/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/AnalyticsActivator.java b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/AnalyticsActivator.java
index a167ff4..0433792 100644
--- a/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/AnalyticsActivator.java
+++ b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/AnalyticsActivator.java
@@ -16,13 +16,23 @@
 
 package com.ning.billing.osgi.bundles.analytics;
 
+import java.util.Hashtable;
+
+import javax.servlet.Servlet;
+import javax.servlet.http.HttpServlet;
+
 import org.osgi.framework.BundleContext;
 
+import com.ning.billing.osgi.api.OSGIPluginProperties;
+import com.ning.billing.osgi.bundles.analytics.api.user.AnalyticsUserApi;
+import com.ning.billing.osgi.bundles.analytics.http.AnalyticsServlet;
 import com.ning.killbill.osgi.libs.killbill.KillbillActivatorBase;
 import com.ning.killbill.osgi.libs.killbill.OSGIKillbillEventDispatcher.OSGIKillbillEventHandler;
 
 public class AnalyticsActivator extends KillbillActivatorBase {
 
+    public static final String PLUGIN_NAME = "killbill-analytics";
+
     private OSGIKillbillEventHandler analyticsListener;
 
     @Override
@@ -30,10 +40,20 @@ public class AnalyticsActivator extends KillbillActivatorBase {
         super.start(context);
 
         analyticsListener = new AnalyticsListener(logService, killbillAPI, dataSource);
+
+        final AnalyticsUserApi analyticsUserApi = new AnalyticsUserApi(logService, killbillAPI, dataSource);
+        final AnalyticsServlet analyticsServlet = new AnalyticsServlet(analyticsUserApi);
+        registerServlet(context, analyticsServlet);
     }
 
     @Override
     public OSGIKillbillEventHandler getOSGIKillbillEventHandler() {
         return analyticsListener;
     }
+
+    private void registerServlet(final BundleContext context, final HttpServlet servlet) {
+        final Hashtable<String, String> props = new Hashtable<String, String>();
+        props.put(OSGIPluginProperties.PLUGIN_NAME_PROP, PLUGIN_NAME);
+        registrar.registerService(context, Servlet.class, servlet, props);
+    }
 }
diff --git a/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/AnalyticsListener.java b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/AnalyticsListener.java
index 2b451c3..c04962f 100644
--- a/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/AnalyticsListener.java
+++ b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/AnalyticsListener.java
@@ -205,7 +205,7 @@ public class AnalyticsListener implements OSGIKillbillEventHandler {
 
         @Override
         public CallOrigin getCallOrigin() {
-            return CallOrigin.EXTERNAL;
+            return CallOrigin.INTERNAL;
         }
 
         @Override
diff --git a/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/http/AnalyticsServlet.java b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/http/AnalyticsServlet.java
new file mode 100644
index 0000000..c025b8b
--- /dev/null
+++ b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/http/AnalyticsServlet.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.osgi.bundles.analytics.http;
+
+import java.io.IOException;
+import java.util.UUID;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+
+import com.ning.billing.osgi.bundles.analytics.AnalyticsRefreshException;
+import com.ning.billing.osgi.bundles.analytics.api.BusinessSnapshot;
+import com.ning.billing.osgi.bundles.analytics.api.user.AnalyticsUserApi;
+import com.ning.billing.util.callcontext.CallContext;
+import com.ning.billing.util.callcontext.CallOrigin;
+import com.ning.billing.util.callcontext.UserType;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.base.Objects;
+
+public class AnalyticsServlet extends HttpServlet {
+
+    private static final String QUERY_TENANT_ID = "tenantId";
+    private static final String HDR_CREATED_BY = "X-Killbill-CreatedBy";
+    private static final String HDR_REASON = "X-Killbill-Reason";
+    private static final String HDR_COMMENT = "X-Killbill-Comment";
+
+    private static final ObjectMapper mapper = ObjectMapperProvider.get();
+
+    private final AnalyticsUserApi analyticsUserApi;
+
+    public AnalyticsServlet(final AnalyticsUserApi analyticsUserApi) {
+        this.analyticsUserApi = analyticsUserApi;
+    }
+
+    @Override
+    protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
+        final UUID kbAccountId = getKbAccountId(req, resp);
+        final CallContext context = createCallContext(req, resp);
+
+        final BusinessSnapshot businessSnapshot = analyticsUserApi.getBusinessSnapshot(kbAccountId, context);
+        resp.getOutputStream().write(mapper.writeValueAsBytes(businessSnapshot));
+        resp.setStatus(HttpServletResponse.SC_OK);
+    }
+
+    @Override
+    protected void doPut(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
+        final UUID kbAccountId = getKbAccountId(req, resp);
+        final CallContext context = createCallContext(req, resp);
+
+        try {
+            analyticsUserApi.rebuildAnalyticsForAccount(kbAccountId, context);
+            resp.setStatus(HttpServletResponse.SC_NO_CONTENT);
+        } catch (AnalyticsRefreshException e) {
+            resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage());
+        }
+    }
+
+    private CallContext createCallContext(final HttpServletRequest req, final HttpServletResponse resp) throws IOException {
+        final String createdBy = Objects.firstNonNull(req.getHeader(HDR_CREATED_BY), req.getRemoteAddr());
+        final String reason = req.getHeader(HDR_REASON);
+        final String comment = Objects.firstNonNull(req.getHeader(HDR_COMMENT), req.getRequestURI());
+
+        final String tenantIdString = req.getParameter(QUERY_TENANT_ID);
+        if (tenantIdString == null) {
+            resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Missing tenantId query parameter in request: " + req.getPathInfo());
+            return null;
+        }
+
+        final UUID tenantId;
+        try {
+            tenantId = UUID.fromString(tenantIdString);
+        } catch (final IllegalArgumentException e) {
+            resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid UUID for tenant id: " + tenantIdString);
+            return null;
+        }
+
+        return new AnalyticsApiCallContext(createdBy, reason, comment, tenantId);
+    }
+
+    private UUID getKbAccountId(final HttpServletRequest req, final HttpServletResponse resp) throws IOException {
+        final String kbAccountIdString;
+        try {
+            kbAccountIdString = req.getPathInfo().substring(1, req.getPathInfo().length());
+        } catch (final StringIndexOutOfBoundsException e) {
+            resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Badly formed kb account id in request: " + req.getPathInfo());
+            return null;
+        }
+
+        final UUID kbAccountId;
+        try {
+            kbAccountId = UUID.fromString(kbAccountIdString);
+        } catch (final IllegalArgumentException e) {
+            resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid UUID for kb account id: " + kbAccountIdString);
+            return null;
+        }
+
+        return kbAccountId;
+    }
+
+    private static final class AnalyticsApiCallContext implements CallContext {
+
+        private final String createdBy;
+        private final String reason;
+        private final String comment;
+        private final UUID tenantId;
+        private final DateTime now;
+
+        private AnalyticsApiCallContext(final String createdBy,
+                                        final String reason,
+                                        final String comment,
+                                        final UUID tenantId) {
+            this.createdBy = createdBy;
+            this.reason = reason;
+            this.comment = comment;
+            this.tenantId = tenantId;
+
+            this.now = new DateTime(DateTimeZone.UTC);
+        }
+
+        @Override
+        public UUID getUserToken() {
+            return UUID.randomUUID();
+        }
+
+        @Override
+        public String getUserName() {
+            return createdBy;
+        }
+
+        @Override
+        public CallOrigin getCallOrigin() {
+            return CallOrigin.EXTERNAL;
+        }
+
+        @Override
+        public UserType getUserType() {
+            return UserType.ADMIN;
+        }
+
+        @Override
+        public String getReasonCode() {
+            return reason;
+        }
+
+        @Override
+        public String getComments() {
+            return comment;
+        }
+
+        @Override
+        public DateTime getCreatedDate() {
+            return now;
+        }
+
+        @Override
+        public DateTime getUpdatedDate() {
+            return now;
+        }
+
+        @Override
+        public UUID getTenantId() {
+            return tenantId;
+        }
+    }
+}
diff --git a/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/http/ObjectMapperProvider.java b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/http/ObjectMapperProvider.java
new file mode 100644
index 0000000..8a09357
--- /dev/null
+++ b/osgi-bundles/bundles/analytics/src/main/java/com/ning/billing/osgi/bundles/analytics/http/ObjectMapperProvider.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.osgi.bundles.analytics.http;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.datatype.joda.JodaModule;
+
+public class ObjectMapperProvider {
+
+    private static final ObjectMapper objectMapper = new ObjectMapper();
+
+    static {
+        objectMapper.registerModule(new JodaModule());
+        objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
+    }
+
+    private ObjectMapperProvider() {}
+
+    public static ObjectMapper get() {
+        return objectMapper;
+    }
+}
diff --git a/osgi-bundles/bundles/analytics/src/test/java/com/ning/billing/osgi/bundles/analytics/api/TestBusinessSnapshot.java b/osgi-bundles/bundles/analytics/src/test/java/com/ning/billing/osgi/bundles/analytics/api/TestBusinessSnapshot.java
index 7952254..0cda5f2 100644
--- a/osgi-bundles/bundles/analytics/src/test/java/com/ning/billing/osgi/bundles/analytics/api/TestBusinessSnapshot.java
+++ b/osgi-bundles/bundles/analytics/src/test/java/com/ning/billing/osgi/bundles/analytics/api/TestBusinessSnapshot.java
@@ -37,6 +37,7 @@ import com.ning.billing.osgi.bundles.analytics.dao.model.BusinessOverdueStatusMo
 import com.ning.billing.osgi.bundles.analytics.dao.model.BusinessSubscription;
 import com.ning.billing.osgi.bundles.analytics.dao.model.BusinessSubscriptionEvent;
 import com.ning.billing.osgi.bundles.analytics.dao.model.BusinessSubscriptionTransitionModelDao;
+import com.ning.billing.osgi.bundles.analytics.http.ObjectMapperProvider;
 
 import com.google.common.collect.ImmutableList;
 
@@ -158,5 +159,9 @@ public class TestBusinessSnapshot extends AnalyticsTestSuiteNoDB {
         Assert.assertEquals(businessSnapshot.getBusinessTags().iterator().next(), businessTag);
         Assert.assertEquals(businessSnapshot.getBusinessFields().size(), 1);
         Assert.assertEquals(businessSnapshot.getBusinessFields().iterator().next(), businessField);
+
+        // We check we can write it out without exception - we can't deserialize it back (no annotation)
+        // but we don't care since the APIs are read-only for Analytics
+        final String asJson = ObjectMapperProvider.get().writeValueAsString(businessSnapshot);
     }
 }