killbill-uncached
Changes
analytics/src/main/java/com/ning/billing/analytics/BusinessSubscriptionTransitionRecorder.java 10(+9 -1)
entitlement/src/main/java/com/ning/billing/entitlement/api/transfer/DefaultEntitlementTransferApi.java 19(+10 -9)
entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/AuditedEntitlementDao.java 2(+1 -1)
pom.xml 16(+16 -0)
usage/pom.xml 110(+110 -0)
usage/src/main/java/com/ning/billing/usage/timeline/aggregator/TimelineAggregatorSqlDao.java 42(+42 -0)
usage/src/main/java/com/ning/billing/usage/timeline/categories/CategoryAndMetricsForSources.java 97(+97 -0)
usage/src/main/java/com/ning/billing/usage/timeline/categories/CategoryIdAndMetricBinder.java 48(+48 -0)
usage/src/main/java/com/ning/billing/usage/timeline/categories/CategoryIdAndMetricMapper.java 33(+33 -0)
usage/src/main/java/com/ning/billing/usage/timeline/codec/EncodedBytesAndSampleCount.java 79(+79 -0)
usage/src/main/java/com/ning/billing/usage/timeline/metrics/SamplesForMetricAndSource.java 108(+108 -0)
usage/src/main/java/com/ning/billing/usage/timeline/persistent/StreamyBytesPersistentOutputStream.java 177(+177 -0)
usage/src/main/java/com/ning/billing/usage/timeline/sources/SourceIdAndMetricIdMapper.java 31(+31 -0)
usage/src/main/java/com/ning/billing/usage/timeline/sources/SourceSamplesForTimestamp.java 131(+131 -0)
usage/src/main/java/com/ning/billing/usage/timeline/TimelineSourceEventAccumulator.java 314(+314 -0)
usage/src/main/resources/com/ning/billing/usage/timeline/aggregator/TimelineAggregatorSqlDao.sql.stg 60(+60 -0)
usage/src/main/resources/com/ning/billing/usage/timeline/persistent/TimelineSqlDao.sql.stg 181(+181 -0)
usage/src/test/java/com/ning/billing/usage/timeline/categories/TestCategoryAndMetrics.java 67(+67 -0)
usage/src/test/java/com/ning/billing/usage/timeline/chunks/TestTimeBytesAndSampleBytes.java 50(+50 -0)
usage/src/test/java/com/ning/billing/usage/timeline/codec/TestEncodedBytesAndSampleCount.java 50(+50 -0)
usage/src/test/java/com/ning/billing/usage/timeline/codec/TestTimelineChunkAccumulator.java 162(+162 -0)
usage/src/test/java/com/ning/billing/usage/timeline/metrics/TestSamplesForMetricAndSource.java 39(+39 -0)
Details
diff --git a/analytics/src/main/java/com/ning/billing/analytics/BusinessSubscriptionTransitionRecorder.java b/analytics/src/main/java/com/ning/billing/analytics/BusinessSubscriptionTransitionRecorder.java
index 83f701d..4e1b199 100644
--- a/analytics/src/main/java/com/ning/billing/analytics/BusinessSubscriptionTransitionRecorder.java
+++ b/analytics/src/main/java/com/ning/billing/analytics/BusinessSubscriptionTransitionRecorder.java
@@ -156,6 +156,8 @@ public class BusinessSubscriptionTransitionRecorder {
return subscriptionCreated(event);
case RE_CREATE:
return subscriptionRecreated(event);
+ case TRANSFER:
+ return subscriptionTransfered(event);
case CANCEL:
return subscriptionCancelled(event);
case CHANGE:
@@ -180,6 +182,11 @@ public class BusinessSubscriptionTransitionRecorder {
return BusinessSubscriptionEvent.subscriptionRecreated(recreated.getNextPlan(), catalogService.getFullCatalog(), recreated.getEffectiveTransitionTime(), recreated.getSubscriptionStartDate());
}
+ private BusinessSubscriptionEvent subscriptionTransfered(final SubscriptionEvent transfered) throws AccountApiException, EntitlementUserApiException {
+ return BusinessSubscriptionEvent.subscriptionTransfered(transfered.getNextPlan(), catalogService.getFullCatalog(), transfered.getEffectiveTransitionTime(), transfered.getSubscriptionStartDate());
+ }
+
+
private BusinessSubscriptionEvent subscriptionCancelled(final SubscriptionEvent cancelled) throws AccountApiException, EntitlementUserApiException {
// cancelled.getNextPlan() is null here - need to look at the previous one to create the correct event name
return BusinessSubscriptionEvent.subscriptionCancelled(cancelled.getPreviousPlan(), catalogService.getFullCatalog(), cancelled.getEffectiveTransitionTime(), cancelled.getSubscriptionStartDate());
@@ -212,7 +219,8 @@ public class BusinessSubscriptionTransitionRecorder {
final ArrayList<BusinessSubscriptionTransition> transitions,
final Currency currency) {
if (BusinessSubscriptionEvent.EventType.ADD.equals(businessEvent.getEventType()) ||
- BusinessSubscriptionEvent.EventType.RE_ADD.equals(businessEvent.getEventType())) {
+ BusinessSubscriptionEvent.EventType.RE_ADD.equals(businessEvent.getEventType()) ||
+ BusinessSubscriptionEvent.EventType.TRANSFER.equals(businessEvent.getEventType())) {
return null;
}
diff --git a/analytics/src/main/java/com/ning/billing/analytics/model/BusinessSubscriptionEvent.java b/analytics/src/main/java/com/ning/billing/analytics/model/BusinessSubscriptionEvent.java
index e21f648..b529559 100644
--- a/analytics/src/main/java/com/ning/billing/analytics/model/BusinessSubscriptionEvent.java
+++ b/analytics/src/main/java/com/ning/billing/analytics/model/BusinessSubscriptionEvent.java
@@ -40,6 +40,7 @@ public class BusinessSubscriptionEvent {
ADD,
CANCEL,
RE_ADD,
+ TRANSFER,
CHANGE,
SYSTEM_CANCEL,
SYSTEM_CHANGE
@@ -96,6 +97,10 @@ public class BusinessSubscriptionEvent {
return eventFromType(EventType.RE_ADD, plan, catalog, eventTime, subscriptionCreationDate);
}
+ public static BusinessSubscriptionEvent subscriptionTransfered(final String plan, final Catalog catalog, final DateTime eventTime, final DateTime subscriptionCreationDate) {
+ return eventFromType(EventType.TRANSFER, plan, catalog, eventTime, subscriptionCreationDate);
+ }
+
public static BusinessSubscriptionEvent subscriptionPhaseChanged(final String plan, final SubscriptionState state, final Catalog catalog, final DateTime eventTime, final DateTime subscriptionCreationDate) {
if (state != null && state.equals(SubscriptionState.CANCELLED)) {
return eventFromType(EventType.SYSTEM_CANCEL, plan, catalog, eventTime, subscriptionCreationDate);
diff --git a/api/src/main/java/com/ning/billing/config/UsageConfig.java b/api/src/main/java/com/ning/billing/config/UsageConfig.java
new file mode 100644
index 0000000..d4cf6bd
--- /dev/null
+++ b/api/src/main/java/com/ning/billing/config/UsageConfig.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2010-2012 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.config;
+
+import org.skife.config.Config;
+import org.skife.config.Default;
+import org.skife.config.Description;
+import org.skife.config.TimeSpan;
+
+public interface UsageConfig extends KillbillConfig {
+
+ @Config("killbill.usage.timelines.length")
+ @Description("How long to buffer data in memory before flushing it to the database")
+ @Default("60m")
+ TimeSpan getTimelineLength();
+
+ // This is used to predict the number of samples between two times. It might be
+ // better to store this information on a per event category basis.
+ @Config("killbill.usage.timelines.pollingInterval")
+ @Description("How long to between attribute polling. This constant should be replaced by a flexible mechanism")
+ @Default("30s")
+ TimeSpan getPollingInterval();
+
+ @Config("killbill.usage.timelines.performForegroundWrites")
+ @Description("If true, perform database writes in the foreground; if false, in the background")
+ @Default("false")
+ boolean getPerformForegroundWrites();
+
+ @Config("killbill.usage.timelines.backgroundWriteBatchSize")
+ @Description("The number of TimelineChunks that must accumulate before we perform background writes, unless the max delay has been exceeded")
+ @Default("1000")
+ int getBackgroundWriteBatchSize();
+
+ @Config("killbill.usage.timelines.backgroundWriteCheckInterval")
+ @Description("The time interval between checks to see if we should perform background writes")
+ @Default("1s")
+ TimeSpan getBackgroundWriteCheckInterval();
+
+ @Config("killbill.usage.timelines.backgroundWriteMaxDelay")
+ @Description("The maximum timespan after pending chunks are added before we perform background writes")
+ @Default("1m")
+ TimeSpan getBackgroundWriteMaxDelay();
+
+ @Config("killbill.usage.timelines.timelineAggregationEnabled")
+ @Description("If true, periodically perform timeline aggregation; if false, don't aggregate")
+ @Default("true")
+ boolean getTimelineAggregationEnabled();
+
+ @Config("killbill.usage.timelines.maxAggregationLevel")
+ @Description("Max aggregation level")
+ @Default("5")
+ int getMaxAggregationLevel();
+
+ @Config("killbill.usage.timelines.chunksToAggregate")
+ @Description("A string with a comma-separated set of integers, one for each aggregation level, giving the number of sequential TimelineChunks with that aggregation level we must find to perform aggregation")
+ // These values translate to 4 hours, 16 hours, 2.7 days, 10.7 days, 42.7 days,
+ @Default("4,4,4,4,4")
+ String getChunksToAggregate();
+
+ @Config("killbill.usage.timelines.aggregationInterval")
+ @Description("How often to check to see if there are timelines ready to be aggregated")
+ @Default("2h")
+ TimeSpan getAggregationInterval();
+
+ @Config("killbill.usage.timelines.aggregationBatchSize")
+ @Description("The number of chunks to fetch in each batch processed")
+ @Default("4000")
+ int getAggregationBatchSize();
+
+ @Config("killbill.usage.timelines.aggregationSleepBetweenBatches")
+ @Description("How long to sleep between aggregation batches")
+ @Default("50ms")
+ TimeSpan getAggregationSleepBetweenBatches();
+
+ @Config("killbill.usage.timelines.maxChunkIdsToInvalidateOrDelete")
+ @Description("If the number of queued chunkIds to invalidate or delete is greater than or equal to this count, perform aggregated timeline writes and delete or invalidate the chunks aggregated")
+ @Default("1000")
+ int getMaxChunkIdsToInvalidateOrDelete();
+
+ @Config("killbill.usage.timelines.deleteAggregatedChunks")
+ @Description("If true, blast the old TimelineChunk rows; if false, leave them in peace, since they won't be accessed")
+ @Default("true")
+ boolean getDeleteAggregatedChunks();
+
+ @Config("killbill.usage.timelines.shutdownSaveMode")
+ @Description("What to save on shut down; either all timelines (save_all_timelines) or just the accumulator start times (save_start_times)")
+ @Default("save_all_timelines")
+ String getShutdownSaveMode();
+
+ @Config("killbill.usage.timelines.segmentsSize")
+ @Description("Direct memory segments size in bytes to allocate when buffering incoming events")
+ @Default("1048576")
+ int getSegmentsSize();
+
+ @Config("killbill.usage.timelines.maxNbSegments")
+ @Description("Max number of direct memory segments to allocate. This times the number of segments indicates the max amount of data buffered before storing a copy to disk")
+ @Default("10")
+ int getMaxNbSegments();
+
+ @Config("killbill.usage.timelines.spoolDir")
+ @Description("Spool directory for in-memory data")
+ @Default("/var/tmp/killbill")
+ String getSpoolDir();
+}
diff --git a/api/src/main/java/com/ning/billing/entitlement/api/transfer/EntitlementTransferApi.java b/api/src/main/java/com/ning/billing/entitlement/api/transfer/EntitlementTransferApi.java
index ce09e8c..796717a 100644
--- a/api/src/main/java/com/ning/billing/entitlement/api/transfer/EntitlementTransferApi.java
+++ b/api/src/main/java/com/ning/billing/entitlement/api/transfer/EntitlementTransferApi.java
@@ -19,11 +19,12 @@ import java.util.UUID;
import org.joda.time.DateTime;
+import com.ning.billing.entitlement.api.user.SubscriptionBundle;
import com.ning.billing.util.callcontext.CallContext;
public interface EntitlementTransferApi {
- public void transferBundle(final UUID sourceAccountId, final UUID destAccountId, final String bundleKey, final DateTime requestedDate, final boolean transferAddOn, final CallContext context)
+ public SubscriptionBundle transferBundle(final UUID sourceAccountId, final UUID destAccountId, final String bundleKey, final DateTime requestedDate, final boolean transferAddOn, final CallContext context)
throws EntitlementTransferApiException;
}
diff --git a/api/src/main/java/com/ning/billing/usage/api/UsageUserApi.java b/api/src/main/java/com/ning/billing/usage/api/UsageUserApi.java
new file mode 100644
index 0000000..eb6935c
--- /dev/null
+++ b/api/src/main/java/com/ning/billing/usage/api/UsageUserApi.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2010-2012 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.usage.api;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+public interface UsageUserApi {
+
+ /**
+ * Shortcut API to record a usage value of "1" for a given metric.
+ *
+ * @param bundleId bundle id source
+ * @param metricName metric name for this usage
+ */
+ public void incrementUsage(final UUID bundleId, final String metricName);
+
+ /**
+ * Fine grained usage API if the external system doesn't roll its usage data. This is used to record e.g. "X has used
+ * 2 credits from his plan at 2012/02/04 4:12pm".
+ *
+ * @param bundleId bundle id source
+ * @param metricName metric name for this usage
+ * @param timestamp timestamp of this usage
+ * @param value value to record
+ */
+ public void recordUsage(final UUID bundleId, final String metricName, final DateTime timestamp, final long value);
+
+ /**
+ * Bulk usage API if the external system rolls-up usage data. This is used to record e.g. "X has used 12 minutes
+ * of his data plan between 2012/02/04 and 2012/02/06".
+ *
+ * @param bundleId bundle id source
+ * @param metricName metric name for this usage
+ * @param startDate start date of the usage period
+ * @param endDate end date of the usage period
+ * @param value value to record
+ */
+ public void recordRolledUpUsage(final UUID bundleId, final String metricName, final DateTime startDate, final DateTime endDate, final long value);
+}
diff --git a/api/src/main/java/com/ning/billing/util/api/AuditUserApi.java b/api/src/main/java/com/ning/billing/util/api/AuditUserApi.java
new file mode 100644
index 0000000..1770335
--- /dev/null
+++ b/api/src/main/java/com/ning/billing/util/api/AuditUserApi.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2010-2012 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.util.api;
+
+import java.util.List;
+import java.util.UUID;
+
+import com.ning.billing.util.audit.AuditLog;
+import com.ning.billing.util.dao.ObjectType;
+
+public interface AuditUserApi {
+
+ /**
+ * Get all the audit entries for a given object
+ *
+ * @param objectId the object id
+ * @param objectType the type of object
+ * @return all audit entries for that object
+ */
+ public List<AuditLog> getAuditLogs(final UUID objectId, final ObjectType objectType);
+}
diff --git a/api/src/main/java/com/ning/billing/util/audit/AuditLog.java b/api/src/main/java/com/ning/billing/util/audit/AuditLog.java
new file mode 100644
index 0000000..be89e7f
--- /dev/null
+++ b/api/src/main/java/com/ning/billing/util/audit/AuditLog.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2010-2012 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.util.audit;
+
+import org.joda.time.DateTime;
+
+import com.ning.billing.util.ChangeType;
+
+public interface AuditLog {
+
+ /**
+ * Get the type of change for this log entry
+ *
+ * @return the ChangeType
+ */
+ public ChangeType getChangeType();
+
+ /**
+ * Get the name of the requestor
+ *
+ * @return the requestor user name
+ */
+ public String getUserName();
+
+ /**
+ * Get the time when this change was effective
+ *
+ * @return the created date of this log entry
+ */
+ public DateTime getCreatedDate();
+
+ /**
+ * Get the reason code for this change
+ *
+ * @return the reason code
+ */
+ public String getReasonCode();
+
+ /**
+ * Get the user token of this change requestor
+ *
+ * @return the user token
+ */
+ public String getUserToken();
+
+ /**
+ * Get the comment for this change
+ *
+ * @return the comment
+ */
+ public String getComment();
+}
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/api/transfer/DefaultEntitlementTransferApi.java b/entitlement/src/main/java/com/ning/billing/entitlement/api/transfer/DefaultEntitlementTransferApi.java
index 6e3a0a9..99155bd 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/api/transfer/DefaultEntitlementTransferApi.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/api/transfer/DefaultEntitlementTransferApi.java
@@ -199,12 +199,14 @@ public class DefaultEntitlementTransferApi implements EntitlementTransferApi {
@Override
- public void transferBundle(final UUID sourceAccountId, final UUID destAccountId,
+ public SubscriptionBundle transferBundle(final UUID sourceAccountId, final UUID destAccountId,
final String bundleKey, final DateTime transferDate, final boolean transferAddOn,
final CallContext context) throws EntitlementTransferApiException {
try {
+ final DateTime effectiveTransferDate = transferDate == null ? clock.getUTCNow() : transferDate;
+
final SubscriptionBundle bundle = dao.getSubscriptionBundleFromAccountAndKey(sourceAccountId, bundleKey);
if (bundle == null) {
throw new EntitlementTransferApiException(ErrorCode.ENT_CREATE_NO_BUNDLE, bundleKey);
@@ -213,7 +215,7 @@ public class DefaultEntitlementTransferApi implements EntitlementTransferApi {
// Get the bundle timeline for the old account
final BundleTimeline bundleTimeline = timelineApi.getBundleTimeline(bundle);
- final SubscriptionBundleData subscriptionBundleData = new SubscriptionBundleData(bundleKey, destAccountId, transferDate);
+ final SubscriptionBundleData subscriptionBundleData = new SubscriptionBundleData(bundleKey, destAccountId, effectiveTransferDate);
final List<SubscriptionMigrationData> subscriptionMigrationDataList = new LinkedList<SubscriptionMigrationData>();
final List<TransferCancelData> transferCancelDataList = new LinkedList<TransferCancelData>();
@@ -232,18 +234,16 @@ public class DefaultEntitlementTransferApi implements EntitlementTransferApi {
}
} else {
-
-
// If BP or STANDALONE subscription, create the cancel event on effectiveCancelDate
- final DateTime effectiveCancelDate = oldSubscription.getChargedThroughDate() != null && transferDate.isBefore(oldSubscription.getChargedThroughDate()) ?
- oldSubscription.getChargedThroughDate() : transferDate;
+ final DateTime effectiveCancelDate = oldSubscription.getChargedThroughDate() != null && effectiveTransferDate.isBefore(oldSubscription.getChargedThroughDate()) ?
+ oldSubscription.getChargedThroughDate() : effectiveTransferDate;
final EntitlementEvent cancelEvent = new ApiEventCancel(new ApiEventBuilder()
.setSubscriptionId(cur.getId())
.setActiveVersion(cur.getActiveVersion())
.setProcessedDate(clock.getUTCNow())
.setEffectiveDate(effectiveCancelDate)
- .setRequestedDate(transferDate)
+ .setRequestedDate(effectiveTransferDate)
.setUserToken(context.getUserToken())
.setFromDisk(true));
@@ -262,11 +262,11 @@ public class DefaultEntitlementTransferApi implements EntitlementTransferApi {
.setId(UUID.randomUUID())
.setBundleId(subscriptionBundleData.getId())
.setCategory(productCategory)
- .setBundleStartDate(transferDate)
+ .setBundleStartDate(effectiveTransferDate)
.setAlignStartDate(subscriptionAlignStartDate),
ImmutableList.<EntitlementEvent>of());
- final List<EntitlementEvent> events = toEvents(existingEvents, subscriptionData, transferDate, context);
+ final List<EntitlementEvent> events = toEvents(existingEvents, subscriptionData, effectiveTransferDate, context);
final SubscriptionMigrationData curData = new SubscriptionMigrationData(subscriptionData, events);
subscriptionMigrationDataList.add(curData);
}
@@ -275,6 +275,7 @@ public class DefaultEntitlementTransferApi implements EntitlementTransferApi {
// Atomically cancel all subscription on old account and create new bundle, subscriptions, events for new account
dao.transfer(sourceAccountId, destAccountId, bundleMigrationData, transferCancelDataList, context);
+ return bundle;
} catch (EntitlementRepairException e) {
throw new EntitlementTransferApiException(e);
}
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/AuditedEntitlementDao.java b/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/AuditedEntitlementDao.java
index b2816ce..455fe64 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/AuditedEntitlementDao.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/AuditedEntitlementDao.java
@@ -567,7 +567,7 @@ public class AuditedEntitlementDao implements EntitlementDao {
final String baseProductName = (futureBaseEvent instanceof ApiEventChange) ?
((ApiEventChange) futureBaseEvent).getEventPlan() : null;
- final boolean createCancelEvent = (futureBaseEvent != null) &&
+ final boolean createCancelEvent = (futureBaseEvent != null && targetAddOnPlan != null) &&
((futureBaseEvent instanceof ApiEventCancel) ||
((!addonUtils.isAddonAvailableFromPlanName(baseProductName, futureBaseEvent.getEffectiveDate(), targetAddOnPlan)) ||
(addonUtils.isAddonIncludedFromPlanName(baseProductName, futureBaseEvent.getEffectiveDate(), targetAddOnPlan))));
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/AccountTimelineJson.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/AccountTimelineJson.java
index 225bcd8..29b1d29 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/AccountTimelineJson.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/AccountTimelineJson.java
@@ -20,6 +20,7 @@ import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
+import java.util.Map;
import java.util.Set;
import java.util.UUID;
@@ -31,6 +32,7 @@ import com.ning.billing.invoice.api.InvoiceItemType;
import com.ning.billing.invoice.api.InvoicePayment;
import com.ning.billing.payment.api.Payment;
import com.ning.billing.payment.api.Refund;
+import com.ning.billing.util.audit.AuditLog;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
@@ -86,11 +88,16 @@ public class AccountTimelineJson {
}
public AccountTimelineJson(final Account account, final List<Invoice> invoices, final List<Payment> payments, final List<BundleTimeline> bundles,
- final Multimap<UUID, Refund> refundsByPayment, final Multimap<UUID, InvoicePayment> chargebacksByPayment) {
+ final Multimap<UUID, Refund> refundsByPayment, final Multimap<UUID, InvoicePayment> chargebacksByPayment,
+ final Map<UUID, List<AuditLog>> invoiceAuditLogs, final Map<UUID, List<AuditLog>> invoiceItemsAuditLogs,
+ final Map<UUID, List<AuditLog>> paymentsAuditLogs, final Map<UUID, List<AuditLog>> refundsAuditLogs,
+ final Map<UUID, List<AuditLog>> chargebacksAuditLogs, final Map<UUID, List<AuditLog>> bundlesAuditLogs) {
this.account = new AccountJsonSimple(account.getId().toString(), account.getExternalKey());
this.bundles = new LinkedList<BundleJsonWithSubscriptions>();
- for (final BundleTimeline cur : bundles) {
- this.bundles.add(new BundleJsonWithSubscriptions(account.getId(), cur));
+ for (final BundleTimeline bundle : bundles) {
+ final List<AuditLog> auditLogs = bundlesAuditLogs.get(bundle.getBundleId());
+ final BundleJsonWithSubscriptions jsonWithSubscriptions = new BundleJsonWithSubscriptions(account.getId(), bundle, auditLogs);
+ this.bundles.add(jsonWithSubscriptions);
}
this.invoices = new LinkedList<InvoiceJsonWithBundleKeys>();
@@ -99,50 +106,45 @@ public class AccountTimelineJson {
for (final Invoice invoice : invoices) {
for (final InvoiceItem invoiceItem : invoice.getInvoiceItems()) {
if (InvoiceItemType.CREDIT_ADJ.equals(invoiceItem.getInvoiceItemType())) {
- credits.add(new CreditJson(invoiceItem, account.getTimeZone()));
+ final List<AuditLog> auditLogs = invoiceItemsAuditLogs.get(invoiceItem.getId());
+ credits.add(new CreditJson(invoiceItem, account.getTimeZone(), auditLogs));
}
}
}
// Create now the invoice json objects
for (final Invoice invoice : invoices) {
- this.invoices.add(new InvoiceJsonWithBundleKeys(invoice.getPaidAmount(),
- invoice.getCBAAmount(),
- invoice.getCreditAdjAmount(),
- invoice.getRefundAdjAmount(),
- invoice.getId().toString(),
- invoice.getInvoiceDate(),
- invoice.getTargetDate(),
- Integer.toString(invoice.getInvoiceNumber()),
- invoice.getBalance(),
- invoice.getAccountId().toString(),
+ final List<AuditLog> auditLogs = invoiceAuditLogs.get(invoice.getId());
+ this.invoices.add(new InvoiceJsonWithBundleKeys(invoice,
getBundleExternalKey(invoice, bundles),
- credits));
+ credits,
+ auditLogs));
}
this.payments = new LinkedList<PaymentJsonWithBundleKeys>();
for (final Payment payment : payments) {
final List<RefundJson> refunds = new ArrayList<RefundJson>();
for (final Refund refund : refundsByPayment.get(payment.getId())) {
- refunds.add(new RefundJson(refund));
+ final List<AuditLog> auditLogs = refundsAuditLogs.get(refund.getId());
+ refunds.add(new RefundJson(refund, auditLogs));
}
final List<ChargebackJson> chargebacks = new ArrayList<ChargebackJson>();
for (final InvoicePayment chargeback : chargebacksByPayment.get(payment.getId())) {
- chargebacks.add(new ChargebackJson(chargeback));
+ final List<AuditLog> auditLogs = chargebacksAuditLogs.get(chargeback.getId());
+ chargebacks.add(new ChargebackJson(chargeback, auditLogs));
}
- final int paymentAttemptSize = payment.getAttempts().size();
+ final int nbOfPaymentAttempts = payment.getAttempts().size();
final String status = payment.getPaymentStatus().toString();
- this.payments.add(new PaymentJsonWithBundleKeys(payment.getAmount(), payment.getPaidAmount(), account.getId().toString(),
- payment.getInvoiceId().toString(), payment.getId().toString(),
- payment.getPaymentMethodId().toString(),
- payment.getEffectiveDate(), payment.getEffectiveDate(),
- paymentAttemptSize, payment.getCurrency().toString(), status,
- payment.getAttempts().get(paymentAttemptSize - 1).getGatewayErrorCode(),
- payment.getAttempts().get(paymentAttemptSize - 1).getGatewayErrorMsg(),
- payment.getExtFirstPaymentIdRef(), payment.getExtSecondPaymentIdRef(),
+ final List<AuditLog> auditLogs = paymentsAuditLogs.get(payment.getId());
+ this.payments.add(new PaymentJsonWithBundleKeys(payment,
+ status,
+ nbOfPaymentAttempts,
getBundleExternalKey(payment.getInvoiceId(), invoices, bundles),
- refunds, chargebacks));
+ account.getId(),
+ refunds,
+ chargebacks,
+ auditLogs));
}
}
@@ -176,15 +178,27 @@ public class AccountTimelineJson {
@Override
public boolean equals(final Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
final AccountTimelineJson that = (AccountTimelineJson) o;
- if (account != null ? !account.equals(that.account) : that.account != null) return false;
- if (bundles != null ? !bundles.equals(that.bundles) : that.bundles != null) return false;
- if (invoices != null ? !invoices.equals(that.invoices) : that.invoices != null) return false;
- if (payments != null ? !payments.equals(that.payments) : that.payments != null) return false;
+ if (account != null ? !account.equals(that.account) : that.account != null) {
+ return false;
+ }
+ if (bundles != null ? !bundles.equals(that.bundles) : that.bundles != null) {
+ return false;
+ }
+ if (invoices != null ? !invoices.equals(that.invoices) : that.invoices != null) {
+ return false;
+ }
+ if (payments != null ? !payments.equals(that.payments) : that.payments != null) {
+ return false;
+ }
return true;
}
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/AuditLogJson.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/AuditLogJson.java
new file mode 100644
index 0000000..333a7d4
--- /dev/null
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/AuditLogJson.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2010-2012 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.jaxrs.json;
+
+import org.joda.time.DateTime;
+
+import com.ning.billing.util.audit.AuditLog;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class AuditLogJson {
+
+ private final String changeType;
+ private final DateTime changeDate;
+ private final String changedBy;
+ private final String reasonCode;
+ private final String comments;
+ private final String userToken;
+
+ @JsonCreator
+ public AuditLogJson(@JsonProperty("changeType") final String changeType,
+ @JsonProperty("changeDate") final DateTime changeDate,
+ @JsonProperty("changedBy") final String changedBy,
+ @JsonProperty("reasonCode") final String reasonCode,
+ @JsonProperty("comments") final String comments,
+ @JsonProperty("userToken") final String userToken) {
+ this.changeType = changeType;
+ this.changeDate = changeDate;
+ this.changedBy = changedBy;
+ this.reasonCode = reasonCode;
+ this.comments = comments;
+ this.userToken = userToken;
+ }
+
+ public AuditLogJson(final AuditLog auditLog) {
+ this(auditLog.getChangeType().toString(), auditLog.getCreatedDate(), auditLog.getUserName(), auditLog.getReasonCode(),
+ auditLog.getComment(), auditLog.getUserToken());
+ }
+
+ public String getChangeType() {
+ return changeType;
+ }
+
+ public DateTime getChangeDate() {
+ return changeDate;
+ }
+
+ public String getChangedBy() {
+ return changedBy;
+ }
+
+ public String getReasonCode() {
+ return reasonCode;
+ }
+
+ public String getComments() {
+ return comments;
+ }
+
+ public String getUserToken() {
+ return userToken;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("AuditLogJson");
+ sb.append("{changeType='").append(changeType).append('\'');
+ sb.append(", changeDate=").append(changeDate);
+ sb.append(", changedBy=").append(changedBy);
+ sb.append(", reasonCode='").append(reasonCode).append('\'');
+ sb.append(", comments='").append(comments).append('\'');
+ sb.append(", userToken='").append(userToken).append('\'');
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final AuditLogJson that = (AuditLogJson) o;
+
+ if (changeDate != null ? changeDate.compareTo(that.changeDate) != 0 : that.changeDate != null) {
+ return false;
+ }
+ if (changeType != null ? !changeType.equals(that.changeType) : that.changeType != null) {
+ return false;
+ }
+ if (changedBy != null ? !changedBy.equals(that.changedBy) : that.changedBy != null) {
+ return false;
+ }
+ if (comments != null ? !comments.equals(that.comments) : that.comments != null) {
+ return false;
+ }
+ if (reasonCode != null ? !reasonCode.equals(that.reasonCode) : that.reasonCode != null) {
+ return false;
+ }
+ if (userToken != null ? !userToken.equals(that.userToken) : that.userToken != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = changeType != null ? changeType.hashCode() : 0;
+ result = 31 * result + (changeDate != null ? changeDate.hashCode() : 0);
+ result = 31 * result + (changedBy != null ? changedBy.hashCode() : 0);
+ result = 31 * result + (reasonCode != null ? reasonCode.hashCode() : 0);
+ result = 31 * result + (comments != null ? comments.hashCode() : 0);
+ result = 31 * result + (userToken != null ? userToken.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/BundleJsonNoSubscriptions.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/BundleJsonNoSubscriptions.java
index cead496..ded56fe 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/BundleJsonNoSubscriptions.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/BundleJsonNoSubscriptions.java
@@ -16,12 +16,14 @@
package com.ning.billing.jaxrs.json;
-import javax.annotation.Nullable;
import java.util.List;
+import javax.annotation.Nullable;
+
+import com.ning.billing.entitlement.api.user.SubscriptionBundle;
+
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
-import com.ning.billing.entitlement.api.user.SubscriptionBundle;
public class BundleJsonNoSubscriptions extends BundleJsonSimple {
@@ -31,8 +33,9 @@ public class BundleJsonNoSubscriptions extends BundleJsonSimple {
public BundleJsonNoSubscriptions(@JsonProperty("bundleId") final String bundleId,
@JsonProperty("accountId") final String accountId,
@JsonProperty("externalKey") final String externalKey,
- @JsonProperty("subscriptions") @Nullable final List<SubscriptionJsonWithEvents> subscriptions) {
- super(bundleId, externalKey);
+ @JsonProperty("subscriptions") @Nullable final List<SubscriptionJsonWithEvents> subscriptions,
+ @JsonProperty("auditLogs") @Nullable final List<AuditLogJson> auditLogs) {
+ super(bundleId, externalKey, auditLogs);
this.accountId = accountId;
}
@@ -41,25 +44,20 @@ public class BundleJsonNoSubscriptions extends BundleJsonSimple {
}
public BundleJsonNoSubscriptions(final SubscriptionBundle bundle) {
- super(bundle.getId().toString(), bundle.getKey());
+ super(bundle.getId().toString(), bundle.getKey(), null);
this.accountId = bundle.getAccountId().toString();
}
- public BundleJsonNoSubscriptions() {
- super(null, null);
- this.accountId = null;
- }
-
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result
- + ((accountId == null) ? 0 : accountId.hashCode());
+ + ((accountId == null) ? 0 : accountId.hashCode());
result = prime * result
- + ((bundleId == null) ? 0 : bundleId.hashCode());
+ + ((bundleId == null) ? 0 : bundleId.hashCode());
result = prime * result
- + ((externalKey == null) ? 0 : externalKey.hashCode());
+ + ((externalKey == null) ? 0 : externalKey.hashCode());
return result;
}
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/BundleJsonSimple.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/BundleJsonSimple.java
index 8c88959..08fcc40 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/BundleJsonSimple.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/BundleJsonSimple.java
@@ -13,14 +13,20 @@
* License for the specific language governing permissions and limitations
* under the License.
*/
+
package com.ning.billing.jaxrs.json;
+import java.util.List;
+import java.util.UUID;
+
import javax.annotation.Nullable;
+import com.ning.billing.util.audit.AuditLog;
+
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
-public class BundleJsonSimple {
+public class BundleJsonSimple extends JsonBase {
protected final String bundleId;
@@ -28,13 +34,15 @@ public class BundleJsonSimple {
@JsonCreator
public BundleJsonSimple(@JsonProperty("bundleId") @Nullable final String bundleId,
- @JsonProperty("externalKey") @Nullable final String externalKey) {
+ @JsonProperty("externalKey") @Nullable final String externalKey,
+ @JsonProperty("auditLogs") @Nullable final List<AuditLogJson> auditLogs) {
+ super(auditLogs);
this.bundleId = bundleId;
this.externalKey = externalKey;
}
- public BundleJsonSimple() {
- this(null, null);
+ public BundleJsonSimple(final UUID bundleId, final String externalKey, final List<AuditLog> auditLogs) {
+ this(bundleId.toString(), externalKey, toAuditLogJson(auditLogs));
}
@JsonProperty("bundleId")
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/BundleJsonWithSubscriptions.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/BundleJsonWithSubscriptions.java
index c2ca338..be03ceb 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/BundleJsonWithSubscriptions.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/BundleJsonWithSubscriptions.java
@@ -13,18 +13,21 @@
* License for the specific language governing permissions and limitations
* under the License.
*/
+
package com.ning.billing.jaxrs.json;
-import javax.annotation.Nullable;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
-import com.fasterxml.jackson.annotation.JsonCreator;
-import com.fasterxml.jackson.annotation.JsonProperty;
+import javax.annotation.Nullable;
+
import com.ning.billing.entitlement.api.timeline.BundleTimeline;
import com.ning.billing.entitlement.api.timeline.SubscriptionTimeline;
-import com.ning.billing.entitlement.api.user.SubscriptionBundle;
+import com.ning.billing.util.audit.AuditLog;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
public class BundleJsonWithSubscriptions extends BundleJsonSimple {
@@ -33,8 +36,9 @@ public class BundleJsonWithSubscriptions extends BundleJsonSimple {
@JsonCreator
public BundleJsonWithSubscriptions(@JsonProperty("bundleId") @Nullable final String bundleId,
@JsonProperty("externalKey") @Nullable final String externalKey,
- @JsonProperty("subscriptions") @Nullable final List<SubscriptionJsonWithEvents> subscriptions) {
- super(bundleId, externalKey);
+ @JsonProperty("subscriptions") @Nullable final List<SubscriptionJsonWithEvents> subscriptions,
+ @JsonProperty("auditLogs") @Nullable final List<AuditLogJson> auditLogs) {
+ super(bundleId, externalKey, auditLogs);
this.subscriptions = subscriptions;
}
@@ -43,23 +47,14 @@ public class BundleJsonWithSubscriptions extends BundleJsonSimple {
return subscriptions;
}
- public BundleJsonWithSubscriptions(@Nullable final UUID accountId, final BundleTimeline bundle) {
- super(bundle.getBundleId().toString(), bundle.getExternalKey());
+ public BundleJsonWithSubscriptions(@Nullable final UUID accountId, final BundleTimeline bundle, final List<AuditLog> auditLogs) {
+ super(bundle.getBundleId(), bundle.getExternalKey(), auditLogs);
this.subscriptions = new LinkedList<SubscriptionJsonWithEvents>();
for (final SubscriptionTimeline cur : bundle.getSubscriptions()) {
this.subscriptions.add(new SubscriptionJsonWithEvents(bundle.getBundleId(), cur));
}
}
- public BundleJsonWithSubscriptions(final SubscriptionBundle bundle) {
- super(bundle.getId().toString(), bundle.getKey());
- this.subscriptions = null;
- }
-
- public BundleJsonWithSubscriptions() {
- this(null, null, null);
- }
-
@Override
public boolean equals(final Object o) {
if (this == o) {
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/ChargebackJson.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/ChargebackJson.java
index 9eba9a9..dec2fd6 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/ChargebackJson.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/ChargebackJson.java
@@ -17,15 +17,20 @@
package com.ning.billing.jaxrs.json;
import java.math.BigDecimal;
+import java.util.List;
+
+import javax.annotation.Nullable;
import org.joda.time.DateTime;
+import com.ning.billing.invoice.api.InvoicePayment;
+import com.ning.billing.util.audit.AuditLog;
+
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
-import com.ning.billing.invoice.api.InvoicePayment;
-// TODO: populate reason code, requested date from audit log
-public class ChargebackJson {
+public class ChargebackJson extends JsonBase {
+
private final DateTime requestedDate;
private final DateTime effectiveDate;
private final BigDecimal chargebackAmount;
@@ -37,7 +42,9 @@ public class ChargebackJson {
@JsonProperty("effectiveDate") final DateTime effectiveDate,
@JsonProperty("chargebackAmount") final BigDecimal chargebackAmount,
@JsonProperty("paymentId") final String paymentId,
- @JsonProperty("reason") final String reason) {
+ @JsonProperty("reason") final String reason,
+ @JsonProperty("auditLogs") @Nullable final List<AuditLogJson> auditLogs) {
+ super(auditLogs);
this.requestedDate = requestedDate;
this.effectiveDate = effectiveDate;
this.chargebackAmount = chargebackAmount;
@@ -46,11 +53,12 @@ public class ChargebackJson {
}
public ChargebackJson(final InvoicePayment chargeback) {
- this.requestedDate = null;
- this.effectiveDate = chargeback.getPaymentDate();
- this.chargebackAmount = chargeback.getAmount().negate();
- this.paymentId = chargeback.getPaymentId().toString();
- this.reason = null;
+ this(chargeback, null);
+ }
+
+ public ChargebackJson(final InvoicePayment chargeback, @Nullable final List<AuditLog> auditLogs) {
+ this(chargeback.getPaymentDate(), chargeback.getPaymentDate(), chargeback.getAmount().negate(),
+ chargeback.getPaymentId().toString(), reasonCodeFromAuditLogs(auditLogs), toAuditLogJson(auditLogs));
}
public DateTime getRequestedDate() {
@@ -85,11 +93,11 @@ public class ChargebackJson {
final ChargebackJson that = (ChargebackJson) o;
if (!((chargebackAmount == null && that.chargebackAmount == null) ||
- (chargebackAmount != null && that.chargebackAmount != null && chargebackAmount.compareTo(that.chargebackAmount) == 0))) {
+ (chargebackAmount != null && that.chargebackAmount != null && chargebackAmount.compareTo(that.chargebackAmount) == 0))) {
return false;
}
if (!((effectiveDate == null && that.effectiveDate == null) ||
- (effectiveDate != null && that.effectiveDate != null && effectiveDate.compareTo(that.effectiveDate) == 0))) {
+ (effectiveDate != null && that.effectiveDate != null && effectiveDate.compareTo(that.effectiveDate) == 0))) {
return false;
}
if (paymentId != null ? !paymentId.equals(that.paymentId) : that.paymentId != null) {
@@ -99,7 +107,7 @@ public class ChargebackJson {
return false;
}
if (!((requestedDate == null && that.requestedDate == null) ||
- (requestedDate != null && that.requestedDate != null && requestedDate.compareTo(that.requestedDate) == 0))) {
+ (requestedDate != null && that.requestedDate != null && requestedDate.compareTo(that.requestedDate) == 0))) {
return false;
}
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/CreditJson.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/CreditJson.java
index ab5221e..458e277 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/CreditJson.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/CreditJson.java
@@ -17,17 +17,21 @@
package com.ning.billing.jaxrs.json;
import java.math.BigDecimal;
+import java.util.List;
import java.util.UUID;
+import javax.annotation.Nullable;
+
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import com.ning.billing.invoice.api.InvoiceItem;
+import com.ning.billing.util.audit.AuditLog;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
-public class CreditJson {
+public class CreditJson extends JsonBase {
private final BigDecimal creditAmount;
private final UUID invoiceId;
@@ -44,7 +48,9 @@ public class CreditJson {
@JsonProperty("requestedDate") final DateTime requestedDate,
@JsonProperty("effectiveDate") final DateTime effectiveDate,
@JsonProperty("reason") final String reason,
- @JsonProperty("accountId") final UUID accountId) {
+ @JsonProperty("accountId") final UUID accountId,
+ @JsonProperty("auditLogs") @Nullable final List<AuditLogJson> auditLogs) {
+ super(auditLogs);
this.creditAmount = creditAmount;
this.invoiceId = invoiceId;
this.invoiceNumber = invoiceNumber;
@@ -54,7 +60,8 @@ public class CreditJson {
this.accountId = accountId;
}
- public CreditJson(final InvoiceItem credit, final DateTimeZone accountTimeZone) {
+ public CreditJson(final InvoiceItem credit, final DateTimeZone accountTimeZone, final List<AuditLog> auditLogs) {
+ super(toAuditLogJson(auditLogs));
this.creditAmount = credit.getAmount();
this.invoiceId = credit.getInvoiceId();
this.invoiceNumber = null;
@@ -64,6 +71,10 @@ public class CreditJson {
this.accountId = credit.getAccountId();
}
+ public CreditJson(final InvoiceItem credit, final DateTimeZone timeZone) {
+ this(credit, timeZone, null);
+ }
+
public BigDecimal getCreditAmount() {
return creditAmount;
}
@@ -93,6 +104,21 @@ public class CreditJson {
}
@Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("CreditJson");
+ sb.append("{creditAmount=").append(creditAmount);
+ sb.append(", invoiceId=").append(invoiceId);
+ sb.append(", invoiceNumber='").append(invoiceNumber).append('\'');
+ sb.append(", requestedDate=").append(requestedDate);
+ sb.append(", effectiveDate=").append(effectiveDate);
+ sb.append(", reason='").append(reason).append('\'');
+ sb.append(", accountId=").append(accountId);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
public boolean equals(final Object o) {
if (this == o) {
return true;
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/InvoiceJsonSimple.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/InvoiceJsonSimple.java
index 195c7d4..7775008 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/InvoiceJsonSimple.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/InvoiceJsonSimple.java
@@ -17,17 +17,19 @@
package com.ning.billing.jaxrs.json;
import java.math.BigDecimal;
+import java.util.List;
import javax.annotation.Nullable;
import org.joda.time.LocalDate;
import com.ning.billing.invoice.api.Invoice;
+import com.ning.billing.util.audit.AuditLog;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
-public class InvoiceJsonSimple {
+public class InvoiceJsonSimple extends JsonBase {
private final BigDecimal amount;
private final String invoiceId;
@@ -40,10 +42,6 @@ public class InvoiceJsonSimple {
private final BigDecimal cba;
private final String accountId;
- public InvoiceJsonSimple() {
- this(BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, null, null, null, null, BigDecimal.ZERO, null);
- }
-
@JsonCreator
public InvoiceJsonSimple(@JsonProperty("amount") final BigDecimal amount,
@JsonProperty("cba") final BigDecimal cba,
@@ -54,7 +52,9 @@ public class InvoiceJsonSimple {
@JsonProperty("targetDate") @Nullable final LocalDate targetDate,
@JsonProperty("invoiceNumber") @Nullable final String invoiceNumber,
@JsonProperty("balance") final BigDecimal balance,
- @JsonProperty("accountId") @Nullable final String accountId) {
+ @JsonProperty("accountId") @Nullable final String accountId,
+ @JsonProperty("auditLogs") @Nullable final List<AuditLogJson> auditLogs) {
+ super(auditLogs);
this.amount = amount;
this.cba = cba;
this.creditAdj = creditAdj;
@@ -67,9 +67,14 @@ public class InvoiceJsonSimple {
this.accountId = accountId;
}
+ public InvoiceJsonSimple(final Invoice input, final List<AuditLog> auditLogs) {
+ this(input.getChargedAmount(), input.getCBAAmount(), input.getCreditAdjAmount(), input.getRefundAdjAmount(),
+ input.getId().toString(), input.getInvoiceDate(), input.getTargetDate(), String.valueOf(input.getInvoiceNumber()),
+ input.getBalance(), input.getAccountId().toString(), toAuditLogJson(auditLogs));
+ }
+
public InvoiceJsonSimple(final Invoice input) {
- this(input.getChargedAmount(), input.getCBAAmount(), input.getCreditAdjAmount(), input.getRefundAdjAmount(), input.getId().toString(), input.getInvoiceDate(),
- input.getTargetDate(), String.valueOf(input.getInvoiceNumber()), input.getBalance(), input.getAccountId().toString());
+ this(input, null);
}
public BigDecimal getAmount() {
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/InvoiceJsonWithBundleKeys.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/InvoiceJsonWithBundleKeys.java
index 5279507..e07729a 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/InvoiceJsonWithBundleKeys.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/InvoiceJsonWithBundleKeys.java
@@ -19,25 +19,21 @@ package com.ning.billing.jaxrs.json;
import java.math.BigDecimal;
import java.util.List;
+import javax.annotation.Nullable;
+
import org.joda.time.LocalDate;
import com.ning.billing.invoice.api.Invoice;
+import com.ning.billing.util.audit.AuditLog;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
-import com.google.common.collect.ImmutableList;
public class InvoiceJsonWithBundleKeys extends InvoiceJsonSimple {
private final String bundleKeys;
private final List<CreditJson> credits;
- public InvoiceJsonWithBundleKeys() {
- super();
- this.bundleKeys = null;
- this.credits = ImmutableList.<CreditJson>of();
- }
-
@JsonCreator
public InvoiceJsonWithBundleKeys(@JsonProperty("amount") final BigDecimal amount,
@JsonProperty("cba") final BigDecimal cba,
@@ -50,14 +46,15 @@ public class InvoiceJsonWithBundleKeys extends InvoiceJsonSimple {
@JsonProperty("balance") final BigDecimal balance,
@JsonProperty("accountId") final String accountId,
@JsonProperty("externalBundleKeys") final String bundleKeys,
- @JsonProperty("credits") final List<CreditJson> credits) {
- super(amount, cba, creditAdj, refundAdj, invoiceId, invoiceDate, targetDate, invoiceNumber, balance, accountId);
+ @JsonProperty("credits") final List<CreditJson> credits,
+ @JsonProperty("auditLogs") @Nullable final List<AuditLogJson> auditLogs) {
+ super(amount, cba, creditAdj, refundAdj, invoiceId, invoiceDate, targetDate, invoiceNumber, balance, accountId, auditLogs);
this.bundleKeys = bundleKeys;
this.credits = credits;
}
- public InvoiceJsonWithBundleKeys(final Invoice input, final String bundleKeys, final List<CreditJson> credits) {
- super(input);
+ public InvoiceJsonWithBundleKeys(final Invoice input, final String bundleKeys, final List<CreditJson> credits, final List<AuditLog> auditLogs) {
+ super(input, auditLogs);
this.bundleKeys = bundleKeys;
this.credits = credits;
}
@@ -82,14 +79,24 @@ public class InvoiceJsonWithBundleKeys extends InvoiceJsonSimple {
@Override
public boolean equals(final Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- if (!super.equals(o)) return false;
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ if (!super.equals(o)) {
+ return false;
+ }
final InvoiceJsonWithBundleKeys that = (InvoiceJsonWithBundleKeys) o;
- if (bundleKeys != null ? !bundleKeys.equals(that.bundleKeys) : that.bundleKeys != null) return false;
- if (credits != null ? !credits.equals(that.credits) : that.credits != null) return false;
+ if (bundleKeys != null ? !bundleKeys.equals(that.bundleKeys) : that.bundleKeys != null) {
+ return false;
+ }
+ if (credits != null ? !credits.equals(that.credits) : that.credits != null) {
+ return false;
+ }
return true;
}
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/InvoiceJsonWithItems.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/InvoiceJsonWithItems.java
index 0f73274..b866f3c 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/InvoiceJsonWithItems.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/InvoiceJsonWithItems.java
@@ -21,6 +21,8 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import javax.annotation.Nullable;
+
import org.joda.time.LocalDate;
import com.ning.billing.invoice.api.Invoice;
@@ -44,8 +46,9 @@ public class InvoiceJsonWithItems extends InvoiceJsonSimple {
@JsonProperty("invoiceNumber") final String invoiceNumber,
@JsonProperty("balance") final BigDecimal balance,
@JsonProperty("accountId") final String accountId,
- @JsonProperty("items") final List<InvoiceItemJsonSimple> items) {
- super(amount, cba, creditAdj, refundAdj, invoiceId, invoiceDate, targetDate, invoiceNumber, balance, accountId);
+ @JsonProperty("items") final List<InvoiceItemJsonSimple> items,
+ @JsonProperty("auditLogs") @Nullable final List<AuditLogJson> auditLogs) {
+ super(amount, cba, creditAdj, refundAdj, invoiceId, invoiceDate, targetDate, invoiceNumber, balance, accountId, auditLogs);
this.items = new ArrayList<InvoiceItemJsonSimple>(items);
}
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/JsonBase.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/JsonBase.java
new file mode 100644
index 0000000..b3e078c
--- /dev/null
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/JsonBase.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2010-2012 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.jaxrs.json;
+
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+import com.ning.billing.util.audit.AuditLog;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableList;
+
+public abstract class JsonBase {
+
+ protected List<AuditLogJson> auditLogs;
+
+ public JsonBase(@Nullable final List<AuditLogJson> auditLogs) {
+ this.auditLogs = auditLogs;
+ }
+
+ protected static ImmutableList<AuditLogJson> toAuditLogJson(@Nullable final List<AuditLog> auditLogs) {
+ if (auditLogs == null) {
+ return null;
+ }
+
+ return ImmutableList.<AuditLogJson>copyOf(Collections2.transform(auditLogs, new Function<AuditLog, AuditLogJson>() {
+ @Override
+ public AuditLogJson apply(@Nullable final AuditLog input) {
+ return new AuditLogJson(input);
+ }
+ }));
+ }
+
+ protected static String reasonCodeFromAuditLogs(@Nullable final List<AuditLog> auditLogs) {
+ if (auditLogs == null || auditLogs.size() == 0) {
+ return null;
+ }
+
+ return auditLogs.get(0).getReasonCode();
+ }
+
+ public List<AuditLogJson> getAuditLogs() {
+ return auditLogs;
+ }
+}
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/PaymentJsonSimple.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/PaymentJsonSimple.java
index 85fb420..360202b 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/PaymentJsonSimple.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/PaymentJsonSimple.java
@@ -19,15 +19,19 @@ package com.ning.billing.jaxrs.json;
import java.math.BigDecimal;
import java.util.List;
+import javax.annotation.Nullable;
+
import org.joda.time.DateTime;
-import com.fasterxml.jackson.annotation.JsonCreator;
-import com.fasterxml.jackson.annotation.JsonProperty;
import com.ning.billing.payment.api.Payment;
-import com.ning.billing.payment.api.Payment.PaymentAttempt;
+import com.ning.billing.util.audit.AuditLog;
import com.ning.billing.util.clock.DefaultClock;
-public class PaymentJsonSimple {
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class PaymentJsonSimple extends JsonBase {
+
private final BigDecimal paidAmount;
private final BigDecimal amount;
@@ -58,25 +62,6 @@ public class PaymentJsonSimple {
private final String extSecondPaymentIdRef;
- public PaymentJsonSimple() {
- this.amount = null;
- this.paidAmount = null;
- this.invoiceId = null;
- this.accountId = null;
- this.paymentId = null;
- this.paymentMethodId = null;
- this.requestedDate = null;
- this.effectiveDate = null;
- this.currency = null;
- this.retryCount = null;
- this.status = null;
- this.gatewayErrorCode = null;
- this.gatewayErrorMsg = null;
- this.extFirstPaymentIdRef = null;
- this.extSecondPaymentIdRef = null;
-
- }
-
@JsonCreator
public PaymentJsonSimple(@JsonProperty("amount") final BigDecimal amount,
@JsonProperty("paidAmount") final BigDecimal paidAmount,
@@ -92,8 +77,9 @@ public class PaymentJsonSimple {
@JsonProperty("gatewayErrorCode") final String gatewayErrorCode,
@JsonProperty("gatewayErrorMsg") final String gatewayErrorMsg,
@JsonProperty("extFirstPaymentIdRef") final String extFirstPaymentIdRef,
- @JsonProperty("extSecondPaymentIdRef") final String extSecondPaymentIdRef) {
- super();
+ @JsonProperty("extSecondPaymentIdRef") final String extSecondPaymentIdRef,
+ @JsonProperty("auditLogs") @Nullable final List<AuditLogJson> auditLogs) {
+ super(auditLogs);
this.amount = amount;
this.paidAmount = paidAmount;
this.invoiceId = invoiceId;
@@ -111,22 +97,16 @@ public class PaymentJsonSimple {
this.extSecondPaymentIdRef = extSecondPaymentIdRef;
}
- public PaymentJsonSimple(final Payment src) {
- this.amount = src.getAmount();
- this.paidAmount = src.getPaidAmount();
- this.invoiceId = src.getInvoiceId().toString();
- this.accountId = src.getAccountId().toString();
- this.paymentId = src.getId().toString();
- this.paymentMethodId =src.getPaymentMethodId().toString();
- this.requestedDate = src.getEffectiveDate();
- this.effectiveDate = src.getEffectiveDate();
- this.currency = src.getCurrency().toString();
- this.retryCount = src.getAttempts().size();
- this.gatewayErrorCode = src.getAttempts().get(retryCount - 1).getGatewayErrorCode();
- this.gatewayErrorMsg = src.getAttempts().get(retryCount - 1).getGatewayErrorMsg();;
- this.status = src.getPaymentStatus().toString();
- this.extFirstPaymentIdRef = src.getExtFirstPaymentIdRef();
- this.extSecondPaymentIdRef = src.getExtSecondPaymentIdRef();
+ public PaymentJsonSimple(final Payment src, final List<AuditLog> auditLogs) {
+ this(src.getAmount(), src.getPaidAmount(), src.getAccountId().toString(), src.getInvoiceId().toString(),
+ src.getId().toString(), src.getPaymentMethodId().toString(), src.getEffectiveDate(), src.getEffectiveDate(),
+ src.getAttempts().size(), src.getCurrency().toString(), src.getPaymentStatus().toString(),
+ src.getAttempts().get(src.getAttempts().size() - 1).getGatewayErrorCode(), src.getAttempts().get(src.getAttempts().size() - 1).getGatewayErrorMsg(),
+ src.getExtFirstPaymentIdRef(), src.getExtSecondPaymentIdRef(), toAuditLogJson(auditLogs));
+ }
+
+ public PaymentJsonSimple(final Payment payment) {
+ this(payment, null);
}
public BigDecimal getPaidAmount() {
@@ -137,7 +117,6 @@ public class PaymentJsonSimple {
return invoiceId;
}
-
public String getPaymentId() {
return paymentId;
}
@@ -205,28 +184,28 @@ public class PaymentJsonSimple {
return false;
}
if (!((amount == null && that.amount == null) ||
- (amount != null && that.amount != null && amount.compareTo(that.amount) == 0))) {
+ (amount != null && that.amount != null && amount.compareTo(that.amount) == 0))) {
return false;
}
if (currency != null ? !currency.equals(that.currency) : that.currency != null) {
return false;
}
if (!((effectiveDate == null && that.effectiveDate == null) ||
- (effectiveDate != null && that.effectiveDate != null && effectiveDate.compareTo(that.effectiveDate) == 0))) {
+ (effectiveDate != null && that.effectiveDate != null && effectiveDate.compareTo(that.effectiveDate) == 0))) {
return false;
}
if (invoiceId != null ? !invoiceId.equals(that.invoiceId) : that.invoiceId != null) {
return false;
}
if (!((paidAmount == null && that.paidAmount == null) ||
- (paidAmount != null && that.paidAmount != null && paidAmount.compareTo(that.paidAmount) == 0))) {
+ (paidAmount != null && that.paidAmount != null && paidAmount.compareTo(that.paidAmount) == 0))) {
return false;
}
if (paymentId != null ? !paymentId.equals(that.paymentId) : that.paymentId != null) {
return false;
}
if (!((requestedDate == null && that.requestedDate == null) ||
- (requestedDate != null && that.requestedDate != null && requestedDate.compareTo(that.requestedDate) == 0))) {
+ (requestedDate != null && that.requestedDate != null && requestedDate.compareTo(that.requestedDate) == 0))) {
return false;
}
if (retryCount != null ? !retryCount.equals(that.retryCount) : that.retryCount != null) {
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/PaymentJsonWithBundleKeys.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/PaymentJsonWithBundleKeys.java
index ec87e78..d7029dc 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/PaymentJsonWithBundleKeys.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/PaymentJsonWithBundleKeys.java
@@ -18,12 +18,17 @@ package com.ning.billing.jaxrs.json;
import java.math.BigDecimal;
import java.util.List;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
import org.joda.time.DateTime;
+import com.ning.billing.payment.api.Payment;
+import com.ning.billing.util.audit.AuditLog;
+
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
-import com.google.common.collect.ImmutableList;
public class PaymentJsonWithBundleKeys extends PaymentJsonSimple {
@@ -31,13 +36,6 @@ public class PaymentJsonWithBundleKeys extends PaymentJsonSimple {
private final List<RefundJson> refunds;
private final List<ChargebackJson> chargebacks;
- public PaymentJsonWithBundleKeys() {
- super();
- this.bundleKeys = null;
- this.refunds = ImmutableList.<RefundJson>of();
- this.chargebacks = ImmutableList.<ChargebackJson>of();
- }
-
@JsonCreator
public PaymentJsonWithBundleKeys(@JsonProperty("amount") final BigDecimal amount,
@JsonProperty("paidAmount") final BigDecimal paidAmount,
@@ -56,14 +54,30 @@ public class PaymentJsonWithBundleKeys extends PaymentJsonSimple {
@JsonProperty("extSecondPaymentIdRef") final String extSecondPaymentIdRef,
@JsonProperty("externalBundleKeys") final String bundleKeys,
@JsonProperty("refunds") final List<RefundJson> refunds,
- @JsonProperty("chargebacks") final List<ChargebackJson> chargebacks) {
- super(amount, paidAmount, accountId, invoiceId, paymentId, paymentMethodId, requestedDate, effectiveDate, retryCount, currency, status, gatewayErrorCode, gatewayErrorMsg,
- extFirstPaymentIdRef, extSecondPaymentIdRef);
+ @JsonProperty("chargebacks") final List<ChargebackJson> chargebacks,
+ @JsonProperty("auditLogs") @Nullable final List<AuditLogJson> auditLogs) {
+ super(amount, paidAmount, accountId, invoiceId, paymentId, paymentMethodId, requestedDate, effectiveDate,
+ retryCount, currency, status, gatewayErrorCode, gatewayErrorMsg, extFirstPaymentIdRef,
+ extSecondPaymentIdRef, auditLogs);
this.bundleKeys = bundleKeys;
this.refunds = refunds;
this.chargebacks = chargebacks;
}
+ public PaymentJsonWithBundleKeys(final Payment payment, final String status, final int nbOfPaymentAttempts, final String bundleExternalKey,
+ final UUID accountId, final List<RefundJson> refunds, final List<ChargebackJson> chargebacks,
+ final List<AuditLog> auditLogs) {
+ this(payment.getAmount(), payment.getPaidAmount(), accountId.toString(),
+ payment.getInvoiceId().toString(), payment.getId().toString(),
+ payment.getPaymentMethodId().toString(),
+ payment.getEffectiveDate(), payment.getEffectiveDate(),
+ nbOfPaymentAttempts, payment.getCurrency().toString(), status,
+ payment.getAttempts().get(nbOfPaymentAttempts - 1).getGatewayErrorCode(),
+ payment.getAttempts().get(nbOfPaymentAttempts - 1).getGatewayErrorMsg(),
+ payment.getExtFirstPaymentIdRef(), payment.getExtSecondPaymentIdRef(),
+ bundleExternalKey, refunds, chargebacks, toAuditLogJson(auditLogs));
+ }
+
public String getBundleKeys() {
return bundleKeys;
}
@@ -89,15 +103,27 @@ public class PaymentJsonWithBundleKeys extends PaymentJsonSimple {
@Override
public boolean equals(final Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- if (!super.equals(o)) return false;
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ if (!super.equals(o)) {
+ return false;
+ }
final PaymentJsonWithBundleKeys that = (PaymentJsonWithBundleKeys) o;
- if (bundleKeys != null ? !bundleKeys.equals(that.bundleKeys) : that.bundleKeys != null) return false;
- if (chargebacks != null ? !chargebacks.equals(that.chargebacks) : that.chargebacks != null) return false;
- if (refunds != null ? !refunds.equals(that.refunds) : that.refunds != null) return false;
+ if (bundleKeys != null ? !bundleKeys.equals(that.bundleKeys) : that.bundleKeys != null) {
+ return false;
+ }
+ if (chargebacks != null ? !chargebacks.equals(that.chargebacks) : that.chargebacks != null) {
+ return false;
+ }
+ if (refunds != null ? !refunds.equals(that.refunds) : that.refunds != null) {
+ return false;
+ }
return true;
}
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/RefundJson.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/RefundJson.java
index ff1547a..390837b 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/RefundJson.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/RefundJson.java
@@ -17,15 +17,19 @@
package com.ning.billing.jaxrs.json;
import java.math.BigDecimal;
+import java.util.List;
+
+import javax.annotation.Nullable;
import org.joda.time.DateTime;
import com.ning.billing.payment.api.Refund;
+import com.ning.billing.util.audit.AuditLog;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
-public class RefundJson {
+public class RefundJson extends JsonBase {
private final String refundId;
private final String paymentId;
@@ -34,18 +38,15 @@ public class RefundJson {
private final DateTime requestedDate;
private final DateTime effectiveDate;
- public RefundJson(final Refund input) {
- this(input.getId().toString(), input.getPaymentId().toString(), input.getRefundAmount(), input.isAdjusted(),
- input.getEffectiveDate(), input.getEffectiveDate());
- }
-
@JsonCreator
public RefundJson(@JsonProperty("refund_id") final String refundId,
@JsonProperty("paymentId") final String paymentId,
@JsonProperty("refundAmount") final BigDecimal refundAmount,
@JsonProperty("adjusted") final Boolean isAdjusted,
@JsonProperty("requestedDate") final DateTime requestedDate,
- @JsonProperty("effectiveDate") final DateTime effectiveDate) {
+ @JsonProperty("effectiveDate") final DateTime effectiveDate,
+ @JsonProperty("auditLogs") @Nullable final List<AuditLogJson> auditLogs) {
+ super(auditLogs);
this.refundId = refundId;
this.paymentId = paymentId;
this.refundAmount = refundAmount;
@@ -54,6 +55,15 @@ public class RefundJson {
this.effectiveDate = effectiveDate;
}
+ public RefundJson(final Refund refund) {
+ this(refund, null);
+ }
+
+ public RefundJson(final Refund refund, final List<AuditLog> auditLogs) {
+ this(refund.getId().toString(), refund.getPaymentId().toString(), refund.getRefundAmount(), refund.isAdjusted(),
+ refund.getEffectiveDate(), refund.getEffectiveDate(), toAuditLogJson(auditLogs));
+ }
+
public String getRefundId() {
return refundId;
}
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/mappers/ExceptionMapperBase.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/mappers/ExceptionMapperBase.java
index d803d10..cf1e2d0 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/mappers/ExceptionMapperBase.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/mappers/ExceptionMapperBase.java
@@ -29,6 +29,8 @@ public abstract class ExceptionMapperBase {
private static final Logger log = LoggerFactory.getLogger(ExceptionMapperBase.class);
protected Response buildConflictingRequestResponse(final Exception e, final UriInfo uriInfo) {
+ // Log the full stacktrace
+ log.warn("Conflicting request", e);
return buildConflictingRequestResponse(e.toString(), uriInfo);
}
@@ -41,6 +43,8 @@ public abstract class ExceptionMapperBase {
}
protected Response buildNotFoundResponse(final Exception e, final UriInfo uriInfo) {
+ // Log the full stacktrace
+ log.warn("Not found", e);
return buildNotFoundResponse(e.toString(), uriInfo);
}
@@ -53,6 +57,8 @@ public abstract class ExceptionMapperBase {
}
protected Response buildBadRequestResponse(final Exception e, final UriInfo uriInfo) {
+ // Log the full stacktrace
+ log.warn("Bad request", e);
return buildBadRequestResponse(e.toString(), uriInfo);
}
@@ -65,6 +71,8 @@ public abstract class ExceptionMapperBase {
}
protected Response buildInternalErrorResponse(final Exception e, final UriInfo uriInfo) {
+ // Log the full stacktrace
+ log.warn("Internal error", e);
return buildInternalErrorResponse(e.toString(), uriInfo);
}
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/mappers/RuntimeExceptionMapper.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/mappers/RuntimeExceptionMapper.java
index e8e3c61..ebbdcab 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/mappers/RuntimeExceptionMapper.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/mappers/RuntimeExceptionMapper.java
@@ -24,12 +24,17 @@ import javax.ws.rs.core.UriInfo;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
@Singleton
@Provider
public class RuntimeExceptionMapper extends ExceptionMapperBase implements ExceptionMapper<RuntimeException> {
private final UriInfo uriInfo;
+ private static final Logger log = LoggerFactory.getLogger(RuntimeExceptionMapper.class);
+
public RuntimeExceptionMapper(@Context final UriInfo uriInfo) {
this.uriInfo = uriInfo;
}
@@ -38,6 +43,8 @@ public class RuntimeExceptionMapper extends ExceptionMapperBase implements Excep
public Response toResponse(final RuntimeException exception) {
if (exception instanceof NullPointerException) {
// Assume bad payload
+ exception.printStackTrace();
+ log.warn("Exception : " + exception.getMessage());
return buildBadRequestResponse(exception, uriInfo);
} else if (exception instanceof WebApplicationException) {
// e.g. com.sun.jersey.api.NotFoundException
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/AccountResource.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/AccountResource.java
index 20d0320..ff6a5fe 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/AccountResource.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/AccountResource.java
@@ -18,8 +18,10 @@ package com.ning.billing.jaxrs.resources;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
+import java.util.Map;
import java.util.UUID;
import javax.ws.rs.Consumes;
@@ -50,6 +52,7 @@ import com.ning.billing.entitlement.api.user.EntitlementUserApi;
import com.ning.billing.entitlement.api.user.EntitlementUserApiException;
import com.ning.billing.entitlement.api.user.SubscriptionBundle;
import com.ning.billing.invoice.api.Invoice;
+import com.ning.billing.invoice.api.InvoiceItem;
import com.ning.billing.invoice.api.InvoicePayment;
import com.ning.billing.invoice.api.InvoicePaymentApi;
import com.ning.billing.invoice.api.InvoiceUserApi;
@@ -69,10 +72,12 @@ import com.ning.billing.payment.api.PaymentApi;
import com.ning.billing.payment.api.PaymentApiException;
import com.ning.billing.payment.api.PaymentMethod;
import com.ning.billing.payment.api.Refund;
+import com.ning.billing.util.api.AuditUserApi;
import com.ning.billing.util.api.CustomFieldUserApi;
import com.ning.billing.util.api.TagApiException;
import com.ning.billing.util.api.TagDefinitionApiException;
import com.ning.billing.util.api.TagUserApi;
+import com.ning.billing.util.audit.AuditLog;
import com.ning.billing.util.dao.ObjectType;
import com.google.common.base.Function;
@@ -107,9 +112,10 @@ public class AccountResource extends JaxRsResourceBase {
final PaymentApi paymentApi,
final EntitlementTimelineApi timelineApi,
final TagUserApi tagUserApi,
+ final AuditUserApi auditUserApi,
final CustomFieldUserApi customFieldUserApi,
final Context context) {
- super(uriBuilder, tagUserApi, customFieldUserApi);
+ super(uriBuilder, tagUserApi, customFieldUserApi, auditUserApi);
this.accountApi = accountApi;
this.entitlementApi = entitlementApi;
this.invoiceApi = invoiceApi;
@@ -133,7 +139,7 @@ public class AccountResource extends JaxRsResourceBase {
@Path("/{accountId:" + UUID_PATTERN + "}/" + BUNDLES)
@Produces(APPLICATION_JSON)
public Response getAccountBundles(@PathParam("accountId") final String accountId, @QueryParam(QUERY_EXTERNAL_KEY) final String externalKey)
- throws AccountApiException, EntitlementUserApiException {
+ throws AccountApiException, EntitlementUserApiException {
final UUID uuid = UUID.fromString(accountId);
accountApi.getAccountById(uuid);
@@ -210,50 +216,77 @@ public class AccountResource extends JaxRsResourceBase {
@GET
@Path("/{accountId:" + UUID_PATTERN + "}/" + TIMELINE)
@Produces(APPLICATION_JSON)
- public Response getAccountTimeline(@PathParam("accountId") final String accountIdString) throws AccountApiException, PaymentApiException, EntitlementRepairException {
+ public Response getAccountTimeline(@PathParam("accountId") final String accountIdString,
+ @QueryParam("audit") @DefaultValue("false") final Boolean withAudit) throws AccountApiException, PaymentApiException, EntitlementRepairException {
final UUID accountId = UUID.fromString(accountIdString);
final Account account = accountApi.getAccountById(accountId);
// Get the invoices
final List<Invoice> invoices = invoiceApi.getInvoicesByAccount(account.getId());
+ final Map<UUID, List<AuditLog>> invoiceAuditLogs = new HashMap<UUID, List<AuditLog>>();
+ final Map<UUID, List<AuditLog>> invoiceItemsAuditLogs = new HashMap<UUID, List<AuditLog>>();
+ if (withAudit) {
+ for (final Invoice invoice : invoices) {
+ invoiceAuditLogs.put(invoice.getId(), auditUserApi.getAuditLogs(invoice.getId(), ObjectType.INVOICE));
+ for (final InvoiceItem invoiceItem : invoice.getInvoiceItems()) {
+ invoiceItemsAuditLogs.put(invoiceItem.getId(), auditUserApi.getAuditLogs(invoiceItem.getId(), ObjectType.INVOICE_ITEM));
+ }
+ }
+ }
// Get the payments
final List<Payment> payments = paymentApi.getAccountPayments(accountId);
+ final Map<UUID, List<AuditLog>> paymentsAuditLogs = new HashMap<UUID, List<AuditLog>>();
+ if (withAudit) {
+ for (final Payment payment : payments) {
+ paymentsAuditLogs.put(payment.getId(), auditUserApi.getAuditLogs(payment.getId(), ObjectType.PAYMENT));
+ }
+ }
// Get the refunds
final List<Refund> refunds = paymentApi.getAccountRefunds(account);
+ final Map<UUID, List<AuditLog>> refundsAuditLogs = new HashMap<UUID, List<AuditLog>>();
final Multimap<UUID, Refund> refundsByPayment = ArrayListMultimap.<UUID, Refund>create();
for (final Refund refund : refunds) {
+ if (withAudit) {
+ refundsAuditLogs.put(refund.getId(), auditUserApi.getAuditLogs(refund.getId(), ObjectType.REFUND));
+ }
refundsByPayment.put(refund.getPaymentId(), refund);
}
// Get the chargebacks
final List<InvoicePayment> chargebacks = invoicePaymentApi.getChargebacksByAccountId(accountId);
+ final Map<UUID, List<AuditLog>> chargebacksAuditLogs = new HashMap<UUID, List<AuditLog>>();
final Multimap<UUID, InvoicePayment> chargebacksByPayment = ArrayListMultimap.<UUID, InvoicePayment>create();
for (final InvoicePayment chargeback : chargebacks) {
+ if (withAudit) {
+ chargebacksAuditLogs.put(chargeback.getId(), auditUserApi.getAuditLogs(chargeback.getId(), ObjectType.INVOICE_PAYMENT));
+ }
chargebacksByPayment.put(chargeback.getPaymentId(), chargeback);
}
// Get the bundles
final List<SubscriptionBundle> bundles = entitlementApi.getBundlesForAccount(account.getId());
+ final Map<UUID, List<AuditLog>> bundlesAuditLogs = new HashMap<UUID, List<AuditLog>>();
final List<BundleTimeline> bundlesTimeline = new LinkedList<BundleTimeline>();
- for (final SubscriptionBundle cur : bundles) {
- bundlesTimeline.add(timelineApi.getBundleTimeline(cur.getId()));
+ for (final SubscriptionBundle bundle : bundles) {
+ if (withAudit) {
+ bundlesAuditLogs.put(bundle.getId(), auditUserApi.getAuditLogs(bundle.getId(), ObjectType.BUNDLE));
+ }
+ bundlesTimeline.add(timelineApi.getBundleTimeline(bundle.getId()));
}
final AccountTimelineJson json = new AccountTimelineJson(account, invoices, payments, bundlesTimeline,
- refundsByPayment, chargebacksByPayment);
+ refundsByPayment, chargebacksByPayment, invoiceAuditLogs,
+ invoiceItemsAuditLogs, paymentsAuditLogs, refundsAuditLogs,
+ chargebacksAuditLogs, bundlesAuditLogs);
return Response.status(Status.OK).entity(json).build();
}
-
-
-
-
/*
- * ************************** EMAIL NOTIFICATIONS FOR INVOICES ********************************
- */
+ * ************************** EMAIL NOTIFICATIONS FOR INVOICES ********************************
+ */
@GET
@Path("/{accountId:" + UUID_PATTERN + "}/" + EMAIL_NOTIFICATIONS)
@@ -296,8 +329,8 @@ public class AccountResource extends JaxRsResourceBase {
@QueryParam(QUERY_PAYMENT_NAME_ON_CC) final String nameOnCC) throws PaymentApiException {
final List<Payment> payments = paymentApi.getAccountPayments(UUID.fromString(accountId));
final List<PaymentJsonSimple> result = new ArrayList<PaymentJsonSimple>(payments.size());
- for (final Payment cur : payments) {
- result.add(new PaymentJsonSimple(cur));
+ for (final Payment payment : payments) {
+ result.add(new PaymentJsonSimple(payment));
}
return Response.status(Status.OK).entity(result).build();
}
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/BundleResource.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/BundleResource.java
index b1c3586..083a9a8 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/BundleResource.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/BundleResource.java
@@ -22,9 +22,11 @@ import java.util.UUID;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
+import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
@@ -33,6 +35,10 @@ import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriInfo;
+import org.joda.time.DateTime;
+
+import com.ning.billing.entitlement.api.transfer.EntitlementTransferApi;
+import com.ning.billing.entitlement.api.transfer.EntitlementTransferApiException;
import com.ning.billing.entitlement.api.user.EntitlementUserApi;
import com.ning.billing.entitlement.api.user.EntitlementUserApiException;
import com.ning.billing.entitlement.api.user.Subscription;
@@ -62,18 +68,21 @@ public class BundleResource extends JaxRsResourceBase {
private static final String TAG_URI = JaxrsResource.TAGS;
private final EntitlementUserApi entitlementApi;
+ private final EntitlementTransferApi transferApi;
private final Context context;
private final JaxrsUriBuilder uriBuilder;
@Inject
public BundleResource(final JaxrsUriBuilder uriBuilder,
final EntitlementUserApi entitlementApi,
+ final EntitlementTransferApi transferApi,
final TagUserApi tagUserApi,
final CustomFieldUserApi customFieldUserApi,
final Context context) {
super(uriBuilder, tagUserApi, customFieldUserApi);
this.uriBuilder = uriBuilder;
this.entitlementApi = entitlementApi;
+ this.transferApi = transferApi;
this.context = context;
}
@@ -159,6 +168,27 @@ public class BundleResource extends JaxRsResourceBase {
return super.getTags(UUID.fromString(id));
}
+ @PUT
+ @Path("/{bundleId:" + UUID_PATTERN + "}")
+ @Consumes(APPLICATION_JSON)
+ @Produces(APPLICATION_JSON)
+ public Response transferBundle(@PathParam(ID_PARAM_NAME) final String id,
+ @QueryParam(QUERY_REQUESTED_DT) final String requestedDate,
+ @QueryParam(QUERY_BUNDLE_TRANSFER_ADDON) @DefaultValue("true") final Boolean transferAddOn,
+ final BundleJsonNoSubscriptions json,
+ @HeaderParam(HDR_CREATED_BY) final String createdBy,
+ @HeaderParam(HDR_REASON) final String reason,
+ @HeaderParam(HDR_COMMENT) final String comment,
+ @javax.ws.rs.core.Context final UriInfo uriInfo) throws EntitlementUserApiException, EntitlementTransferApiException {
+
+ final SubscriptionBundle bundle = entitlementApi.getBundleFromId(UUID.fromString(id));
+ final DateTime inputDate = (requestedDate != null) ? DATE_TIME_FORMATTER.parseDateTime(requestedDate) : null;
+ final SubscriptionBundle newBundle = transferApi.transferBundle(bundle.getAccountId(), UUID.fromString(json.getAccountId()), bundle.getKey(), inputDate, transferAddOn,
+ context.createContext(createdBy, reason, comment));
+
+ return uriBuilder.buildResponse(BundleResource.class, "getBundle", newBundle.getId(), uriInfo.getBaseUri().toString());
+ }
+
@POST
@Path("/{bundleId:" + UUID_PATTERN + "}/" + TAG_URI)
@Consumes(APPLICATION_JSON)
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/InvoiceResource.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/InvoiceResource.java
index f4adea8..90be987 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/InvoiceResource.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/InvoiceResource.java
@@ -114,8 +114,8 @@ public class InvoiceResource extends JaxRsResourceBase {
final List<Invoice> invoices = invoiceApi.getInvoicesByAccount(UUID.fromString(accountId));
final List<InvoiceJsonSimple> result = new LinkedList<InvoiceJsonSimple>();
- for (final Invoice cur : invoices) {
- result.add(new InvoiceJsonSimple(cur));
+ for (final Invoice invoice : invoices) {
+ result.add(new InvoiceJsonSimple(invoice));
}
return Response.status(Status.OK).entity(result).build();
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/JaxrsResource.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/JaxrsResource.java
index 932d1e2..4c9ceb0 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/JaxrsResource.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/JaxrsResource.java
@@ -61,6 +61,7 @@ public interface JaxrsResource {
public static final String QUERY_PAYMENT_METHOD_PLUGIN_INFO = "withPluginInfo";
public static final String QUERY_PAYMENT_METHOD_IS_DEFAULT = "isDefault";
+ public static final String QUERY_BUNDLE_TRANSFER_ADDON = "transferAddOn";
public static final String ACCOUNTS = "accounts";
public static final String ACCOUNTS_PATH = PREFIX + "/" + ACCOUNTS;
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/JaxRsResourceBase.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/JaxRsResourceBase.java
index e092c39..6f2d2f1 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/JaxRsResourceBase.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/JaxRsResourceBase.java
@@ -26,11 +26,14 @@ import java.util.UUID;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
+import org.joda.time.format.DateTimeFormatter;
+import org.joda.time.format.ISODateTimeFormat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.ning.billing.jaxrs.json.CustomFieldJson;
import com.ning.billing.jaxrs.util.JaxrsUriBuilder;
+import com.ning.billing.util.api.AuditUserApi;
import com.ning.billing.util.api.CustomFieldUserApi;
import com.ning.billing.util.api.TagApiException;
import com.ning.billing.util.api.TagDefinitionApiException;
@@ -53,15 +56,26 @@ public abstract class JaxRsResourceBase implements JaxrsResource {
protected final JaxrsUriBuilder uriBuilder;
protected final TagUserApi tagUserApi;
protected final CustomFieldUserApi customFieldUserApi;
+ protected final AuditUserApi auditUserApi;
- protected abstract ObjectType getObjectType();
+ protected final DateTimeFormatter DATE_TIME_FORMATTER = ISODateTimeFormat.dateTime();
public JaxRsResourceBase(final JaxrsUriBuilder uriBuilder,
final TagUserApi tagUserApi,
- final CustomFieldUserApi customFieldUserApi) {
+ final CustomFieldUserApi customFieldUserApi,
+ final AuditUserApi auditUserApi) {
this.uriBuilder = uriBuilder;
this.tagUserApi = tagUserApi;
this.customFieldUserApi = customFieldUserApi;
+ this.auditUserApi = auditUserApi;
+ }
+
+ protected abstract ObjectType getObjectType();
+
+ public JaxRsResourceBase(final JaxrsUriBuilder uriBuilder,
+ final TagUserApi tagUserApi,
+ final CustomFieldUserApi customFieldUserApi) {
+ this(uriBuilder, tagUserApi, customFieldUserApi, null);
}
protected Response getTags(final UUID id) throws TagDefinitionApiException {
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/SubscriptionResource.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/SubscriptionResource.java
index 38027ff..03c30d2 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/SubscriptionResource.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/SubscriptionResource.java
@@ -37,8 +37,6 @@ import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriInfo;
import org.joda.time.DateTime;
-import org.joda.time.format.DateTimeFormatter;
-import org.joda.time.format.ISODateTimeFormat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -79,8 +77,6 @@ public class SubscriptionResource extends JaxRsResourceBase {
private static final String CUSTOM_FIELD_URI = JaxrsResource.CUSTOM_FIELDS + "/{" + ID_PARAM_NAME + ":" + UUID_PATTERN + "}";
private static final String TAG_URI = JaxrsResource.TAGS + "/{" + ID_PARAM_NAME + ":" + UUID_PATTERN + "}";
- private final DateTimeFormatter DATE_TIME_FORMATTER = ISODateTimeFormat.dateTime();
-
private final EntitlementUserApi entitlementApi;
private final Context context;
private final JaxrsUriBuilder uriBuilder;
diff --git a/jaxrs/src/test/java/com/ning/billing/jaxrs/JaxrsTestSuite.java b/jaxrs/src/test/java/com/ning/billing/jaxrs/JaxrsTestSuite.java
index 51e2ac7..9018ab0 100644
--- a/jaxrs/src/test/java/com/ning/billing/jaxrs/JaxrsTestSuite.java
+++ b/jaxrs/src/test/java/com/ning/billing/jaxrs/JaxrsTestSuite.java
@@ -16,7 +16,37 @@
package com.ning.billing.jaxrs;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
import com.ning.billing.KillbillTestSuite;
+import com.ning.billing.jaxrs.json.AuditLogJson;
+import com.ning.billing.util.clock.Clock;
+import com.ning.billing.util.clock.ClockMock;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.datatype.joda.JodaModule;
public abstract class JaxrsTestSuite extends KillbillTestSuite {
+
+ private final Clock clock = new ClockMock();
+
+ protected static final ObjectMapper mapper = new ObjectMapper();
+
+ static {
+ mapper.registerModule(new JodaModule());
+ mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
+ }
+
+ protected List<AuditLogJson> createAuditLogsJson() {
+ final List<AuditLogJson> auditLogs = new ArrayList<AuditLogJson>();
+ for (int i = 0; i < 20; i++) {
+ auditLogs.add(new AuditLogJson(UUID.randomUUID().toString(), clock.getUTCNow(), UUID.randomUUID().toString(),
+ UUID.randomUUID().toString(), UUID.randomUUID().toString(), UUID.randomUUID().toString()));
+ }
+
+ return auditLogs;
+ }
}
diff --git a/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestAuditLogJson.java b/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestAuditLogJson.java
new file mode 100644
index 0000000..3c7e31c
--- /dev/null
+++ b/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestAuditLogJson.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2010-2012 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.jaxrs.json;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.ning.billing.jaxrs.JaxrsTestSuite;
+import com.ning.billing.util.ChangeType;
+import com.ning.billing.util.audit.AuditLog;
+import com.ning.billing.util.audit.DefaultAuditLog;
+import com.ning.billing.util.callcontext.CallContext;
+import com.ning.billing.util.callcontext.CallOrigin;
+import com.ning.billing.util.callcontext.DefaultCallContext;
+import com.ning.billing.util.callcontext.UserType;
+import com.ning.billing.util.clock.Clock;
+import com.ning.billing.util.clock.ClockMock;
+import com.ning.billing.util.clock.DefaultClock;
+import com.ning.billing.util.dao.EntityAudit;
+import com.ning.billing.util.dao.TableName;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.datatype.joda.JodaModule;
+
+public class TestAuditLogJson extends JaxrsTestSuite {
+
+ private static final ObjectMapper mapper = new ObjectMapper();
+
+ private final Clock clock = new DefaultClock();
+
+ static {
+ mapper.registerModule(new JodaModule());
+ mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
+ }
+
+ @Test(groups = "fast")
+ public void testJson() throws Exception {
+ final String changeType = UUID.randomUUID().toString();
+ final DateTime changeDate = clock.getUTCNow();
+ final String changedBy = UUID.randomUUID().toString();
+ final String reasonCode = UUID.randomUUID().toString();
+ final String comments = UUID.randomUUID().toString();
+ final String userToken = UUID.randomUUID().toString();
+
+ final AuditLogJson auditLogJson = new AuditLogJson(changeType, changeDate, changedBy, reasonCode, comments, userToken);
+ Assert.assertEquals(auditLogJson.getChangeType(), changeType);
+ Assert.assertEquals(auditLogJson.getChangeDate(), changeDate);
+ Assert.assertEquals(auditLogJson.getChangedBy(), changedBy);
+ Assert.assertEquals(auditLogJson.getReasonCode(), reasonCode);
+ Assert.assertEquals(auditLogJson.getComments(), comments);
+ Assert.assertEquals(auditLogJson.getUserToken(), userToken);
+
+ final String asJson = mapper.writeValueAsString(auditLogJson);
+ Assert.assertEquals(asJson, "{\"changeType\":\"" + auditLogJson.getChangeType() + "\"," +
+ "\"changeDate\":\"" + auditLogJson.getChangeDate().toDateTimeISO().toString() + "\"," +
+ "\"changedBy\":\"" + auditLogJson.getChangedBy() + "\"," +
+ "\"reasonCode\":\"" + auditLogJson.getReasonCode() + "\"," +
+ "\"comments\":\"" + auditLogJson.getComments() + "\"," +
+ "\"userToken\":\"" + auditLogJson.getUserToken() + "\"}");
+
+ final AuditLogJson fromJson = mapper.readValue(asJson, AuditLogJson.class);
+ Assert.assertEquals(fromJson, auditLogJson);
+ }
+
+ @Test(groups = "fast")
+ public void testConstructor() throws Exception {
+ final TableName tableName = TableName.ACCOUNT_EMAIL_HISTORY;
+ final long recordId = Long.MAX_VALUE;
+ final ChangeType changeType = ChangeType.DELETE;
+ final EntityAudit entityAudit = new EntityAudit(tableName, recordId, changeType);
+
+ final String userName = UUID.randomUUID().toString();
+ final CallOrigin callOrigin = CallOrigin.EXTERNAL;
+ final UserType userType = UserType.CUSTOMER;
+ final UUID userToken = UUID.randomUUID();
+ final ClockMock clock = new ClockMock();
+ final CallContext callContext = new DefaultCallContext(userName, callOrigin, userType, userToken, clock);
+
+ final AuditLog auditLog = new DefaultAuditLog(entityAudit, callContext);
+
+ final AuditLogJson auditLogJson = new AuditLogJson(auditLog);
+ Assert.assertEquals(auditLogJson.getChangeType(), changeType.toString());
+ Assert.assertNotNull(auditLogJson.getChangeDate());
+ Assert.assertEquals(auditLogJson.getChangedBy(), userName);
+ Assert.assertNull(auditLogJson.getReasonCode());
+ Assert.assertNull(auditLogJson.getComments());
+ Assert.assertEquals(auditLogJson.getUserToken(), userToken.toString());
+ }
+}
diff --git a/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestBundleJsonNoSubscriptions.java b/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestBundleJsonNoSubscriptions.java
index 97caed0..d87274a 100644
--- a/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestBundleJsonNoSubscriptions.java
+++ b/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestBundleJsonNoSubscriptions.java
@@ -16,34 +16,31 @@
package com.ning.billing.jaxrs.json;
+import java.util.List;
import java.util.UUID;
import org.mockito.Mockito;
import org.testng.Assert;
import org.testng.annotations.Test;
-import com.fasterxml.jackson.databind.ObjectMapper;
import com.ning.billing.entitlement.api.user.SubscriptionBundle;
import com.ning.billing.jaxrs.JaxrsTestSuite;
public class TestBundleJsonNoSubscriptions extends JaxrsTestSuite {
- private static final ObjectMapper mapper = new ObjectMapper();
@Test(groups = "fast")
public void testJson() throws Exception {
final String bundleId = UUID.randomUUID().toString();
final String accountId = UUID.randomUUID().toString();
final String externalKey = UUID.randomUUID().toString();
- final BundleJsonNoSubscriptions bundleJsonNoSubscriptions = new BundleJsonNoSubscriptions(bundleId, accountId, externalKey, null);
+ final List<AuditLogJson> auditLogs = createAuditLogsJson();
+ final BundleJsonNoSubscriptions bundleJsonNoSubscriptions = new BundleJsonNoSubscriptions(bundleId, accountId, externalKey, null, auditLogs);
Assert.assertEquals(bundleJsonNoSubscriptions.getBundleId(), bundleId);
Assert.assertEquals(bundleJsonNoSubscriptions.getAccountId(), accountId);
Assert.assertEquals(bundleJsonNoSubscriptions.getExternalKey(), externalKey);
+ Assert.assertEquals(bundleJsonNoSubscriptions.getAuditLogs(), auditLogs);
final String asJson = mapper.writeValueAsString(bundleJsonNoSubscriptions);
- Assert.assertEquals(asJson, "{\"bundleId\":\"" + bundleJsonNoSubscriptions.getBundleId() + "\"," +
- "\"accountId\":\"" + bundleJsonNoSubscriptions.getAccountId() + "\"," +
- "\"externalKey\":\"" + bundleJsonNoSubscriptions.getExternalKey() + "\"}");
-
final BundleJsonNoSubscriptions fromJson = mapper.readValue(asJson, BundleJsonNoSubscriptions.class);
Assert.assertEquals(fromJson, bundleJsonNoSubscriptions);
}
@@ -62,5 +59,6 @@ public class TestBundleJsonNoSubscriptions extends JaxrsTestSuite {
Assert.assertEquals(bundleJsonNoSubscriptions.getBundleId(), bundleId.toString());
Assert.assertEquals(bundleJsonNoSubscriptions.getExternalKey(), externalKey);
Assert.assertEquals(bundleJsonNoSubscriptions.getAccountId(), accountId.toString());
+ Assert.assertNull(bundleJsonNoSubscriptions.getAuditLogs());
}
}
diff --git a/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestBundleJsonSimple.java b/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestBundleJsonSimple.java
index e00ed56..62e56d3 100644
--- a/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestBundleJsonSimple.java
+++ b/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestBundleJsonSimple.java
@@ -16,29 +16,27 @@
package com.ning.billing.jaxrs.json;
+import java.util.List;
import java.util.UUID;
import org.testng.Assert;
import org.testng.annotations.Test;
-import com.fasterxml.jackson.databind.ObjectMapper;
import com.ning.billing.jaxrs.JaxrsTestSuite;
public class TestBundleJsonSimple extends JaxrsTestSuite {
- private static final ObjectMapper mapper = new ObjectMapper();
@Test(groups = "fast")
public void testJson() throws Exception {
final String bundleId = UUID.randomUUID().toString();
final String externalKey = UUID.randomUUID().toString();
- final BundleJsonSimple bundleJsonSimple = new BundleJsonSimple(bundleId, externalKey);
+ final List<AuditLogJson> auditLogs = createAuditLogsJson();
+ final BundleJsonSimple bundleJsonSimple = new BundleJsonSimple(bundleId, externalKey, auditLogs);
Assert.assertEquals(bundleJsonSimple.getBundleId(), bundleId);
Assert.assertEquals(bundleJsonSimple.getExternalKey(), externalKey);
+ Assert.assertEquals(bundleJsonSimple.getAuditLogs(), auditLogs);
final String asJson = mapper.writeValueAsString(bundleJsonSimple);
- Assert.assertEquals(asJson, "{\"bundleId\":\"" + bundleJsonSimple.getBundleId() + "\"," +
- "\"externalKey\":\"" + bundleJsonSimple.getExternalKey() + "\"}");
-
final BundleJsonSimple fromJson = mapper.readValue(asJson, BundleJsonSimple.class);
Assert.assertEquals(fromJson, bundleJsonSimple);
}
diff --git a/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestBundleJsonWithSubscriptions.java b/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestBundleJsonWithSubscriptions.java
index 84cac3c..36e328b 100644
--- a/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestBundleJsonWithSubscriptions.java
+++ b/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestBundleJsonWithSubscriptions.java
@@ -16,6 +16,7 @@
package com.ning.billing.jaxrs.json;
+import java.util.List;
import java.util.UUID;
import org.joda.time.DateTime;
@@ -24,10 +25,6 @@ import org.mockito.Mockito;
import org.testng.Assert;
import org.testng.annotations.Test;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.SerializationFeature;
-import com.fasterxml.jackson.datatype.joda.JodaModule;
-import com.google.common.collect.ImmutableList;
import com.ning.billing.catalog.api.BillingPeriod;
import com.ning.billing.catalog.api.PhaseType;
import com.ning.billing.catalog.api.PlanPhaseSpecifier;
@@ -35,17 +32,12 @@ import com.ning.billing.catalog.api.ProductCategory;
import com.ning.billing.entitlement.api.SubscriptionTransitionType;
import com.ning.billing.entitlement.api.timeline.BundleTimeline;
import com.ning.billing.entitlement.api.timeline.SubscriptionTimeline;
-import com.ning.billing.entitlement.api.user.SubscriptionBundle;
import com.ning.billing.jaxrs.JaxrsTestSuite;
import com.ning.billing.util.clock.DefaultClock;
-public class TestBundleJsonWithSubscriptions extends JaxrsTestSuite {
- private static final ObjectMapper mapper = new ObjectMapper();
+import com.google.common.collect.ImmutableList;
- static {
- mapper.registerModule(new JodaModule());
- mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
- }
+public class TestBundleJsonWithSubscriptions extends JaxrsTestSuite {
@Test(groups = "fast")
public void testJson() throws Exception {
@@ -67,24 +59,14 @@ public class TestBundleJsonWithSubscriptions extends JaxrsTestSuite {
final UUID bundleId = UUID.randomUUID();
final String externalKey = UUID.randomUUID().toString();
final SubscriptionJsonWithEvents subscription = new SubscriptionJsonWithEvents(bundleId, subscriptionTimeline);
- final BundleJsonWithSubscriptions bundleJsonWithSubscriptions = new BundleJsonWithSubscriptions(bundleId.toString(), externalKey, ImmutableList.<SubscriptionJsonWithEvents>of(subscription));
+ final List<AuditLogJson> auditLogs = createAuditLogsJson();
+ final BundleJsonWithSubscriptions bundleJsonWithSubscriptions = new BundleJsonWithSubscriptions(bundleId.toString(), externalKey, ImmutableList.<SubscriptionJsonWithEvents>of(subscription), auditLogs);
Assert.assertEquals(bundleJsonWithSubscriptions.getBundleId(), bundleId.toString());
Assert.assertEquals(bundleJsonWithSubscriptions.getExternalKey(), externalKey);
Assert.assertEquals(bundleJsonWithSubscriptions.getSubscriptions().size(), 1);
+ Assert.assertEquals(bundleJsonWithSubscriptions.getAuditLogs(), auditLogs);
final String asJson = mapper.writeValueAsString(bundleJsonWithSubscriptions);
- Assert.assertEquals(asJson, "{\"bundleId\":\"" + bundleJsonWithSubscriptions.getBundleId() + "\"," +
- "\"externalKey\":\"" + bundleJsonWithSubscriptions.getExternalKey() + "\"," +
- "\"subscriptions\":[{\"events\":[{\"eventId\":\"" + event.getEventId().toString() + "\"," +
- "\"billingPeriod\":\"" + event.getPlanPhaseSpecifier().getBillingPeriod().toString() + "\"," +
- "\"product\":\"" + event.getPlanPhaseSpecifier().getProductName() + "\"," +
- "\"priceList\":\"" + event.getPlanPhaseSpecifier().getPriceListName() + "\"," +
- "\"eventType\":\"" + event.getSubscriptionTransitionType().toString() + "\"," +
- "\"phase\":\"" + event.getPlanPhaseSpecifier().getPhaseType() + "\"," +
- "\"requestedDate\":null," +
- "\"effectiveDate\":\"" + event.getEffectiveDate().toDateTimeISO().toString() + "\"}]," +
- "\"subscriptionId\":\"" + subscriptionTimeline.getId().toString() + "\",\"deletedEvents\":null,\"newEvents\":null}]}");
-
final BundleJsonWithSubscriptions fromJson = mapper.readValue(asJson, BundleJsonWithSubscriptions.class);
Assert.assertEquals(fromJson, bundleJsonWithSubscriptions);
}
@@ -113,7 +95,7 @@ public class TestBundleJsonWithSubscriptions extends JaxrsTestSuite {
Mockito.when(bundleTimeline.getExternalKey()).thenReturn(externalKey);
Mockito.when(bundleTimeline.getSubscriptions()).thenReturn(ImmutableList.<SubscriptionTimeline>of(subscriptionTimeline));
- final BundleJsonWithSubscriptions bundleJsonWithSubscriptions = new BundleJsonWithSubscriptions(null, bundleTimeline);
+ final BundleJsonWithSubscriptions bundleJsonWithSubscriptions = new BundleJsonWithSubscriptions(null, bundleTimeline, null);
Assert.assertEquals(bundleJsonWithSubscriptions.getBundleId(), bundleId.toString());
Assert.assertEquals(bundleJsonWithSubscriptions.getExternalKey(), externalKey);
Assert.assertEquals(bundleJsonWithSubscriptions.getSubscriptions().size(), 1);
@@ -124,19 +106,21 @@ public class TestBundleJsonWithSubscriptions extends JaxrsTestSuite {
// Note - ms are truncated
Assert.assertEquals(events.getEvents().get(0).getEffectiveDate(), DefaultClock.toUTCDateTime(effectiveDate));
Assert.assertEquals(events.getEvents().get(0).getEventId(), eventId.toString());
+ Assert.assertNull(bundleJsonWithSubscriptions.getAuditLogs());
}
@Test(groups = "fast")
public void testFromSubscriptionBundle() throws Exception {
- final SubscriptionBundle bundle = Mockito.mock(SubscriptionBundle.class);
+ final BundleTimeline bundle = Mockito.mock(BundleTimeline.class);
final UUID bundleId = UUID.randomUUID();
final String externalKey = UUID.randomUUID().toString();
- Mockito.when(bundle.getId()).thenReturn(bundleId);
- Mockito.when(bundle.getKey()).thenReturn(externalKey);
+ Mockito.when(bundle.getBundleId()).thenReturn(bundleId);
+ Mockito.when(bundle.getExternalKey()).thenReturn(externalKey);
- final BundleJsonWithSubscriptions bundleJsonWithSubscriptions = new BundleJsonWithSubscriptions(bundle);
+ final BundleJsonWithSubscriptions bundleJsonWithSubscriptions = new BundleJsonWithSubscriptions(null, bundle, null);
Assert.assertEquals(bundleJsonWithSubscriptions.getBundleId(), bundleId.toString());
Assert.assertEquals(bundleJsonWithSubscriptions.getExternalKey(), externalKey);
- Assert.assertNull(bundleJsonWithSubscriptions.getSubscriptions());
+ Assert.assertEquals(bundleJsonWithSubscriptions.getSubscriptions().size(), 0);
+ Assert.assertNull(bundleJsonWithSubscriptions.getAuditLogs());
}
}
diff --git a/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestBundleTimelineJson.java b/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestBundleTimelineJson.java
index c798b24..17df65d 100644
--- a/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestBundleTimelineJson.java
+++ b/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestBundleTimelineJson.java
@@ -35,22 +35,12 @@ import com.ning.billing.jaxrs.JaxrsTestSuite;
import com.ning.billing.util.clock.Clock;
import com.ning.billing.util.clock.DefaultClock;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.SerializationFeature;
-import com.fasterxml.jackson.datatype.joda.JodaModule;
import com.google.common.collect.ImmutableList;
public class TestBundleTimelineJson extends JaxrsTestSuite {
- private static final ObjectMapper mapper = new ObjectMapper();
-
private final Clock clock = new DefaultClock();
- static {
- mapper.registerModule(new JodaModule());
- mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
- }
-
@Test(groups = "fast")
public void testJson() throws Exception {
final String viewId = UUID.randomUUID().toString();
@@ -59,63 +49,15 @@ public class TestBundleTimelineJson extends JaxrsTestSuite {
final BundleJsonWithSubscriptions bundleJsonWithSubscriptions = createBundleWithSubscriptions();
final InvoiceJsonSimple invoiceJsonSimple = createInvoice();
final PaymentJsonSimple paymentJsonSimple = createPayment(UUID.fromString(invoiceJsonSimple.getAccountId()),
- UUID.fromString(invoiceJsonSimple.getInvoiceId()));
+ UUID.fromString(invoiceJsonSimple.getInvoiceId()));
final BundleTimelineJson bundleTimelineJson = new BundleTimelineJson(viewId,
- bundleJsonWithSubscriptions,
- ImmutableList.<PaymentJsonSimple>of(paymentJsonSimple),
- ImmutableList.<InvoiceJsonSimple>of(invoiceJsonSimple),
- reason);
+ bundleJsonWithSubscriptions,
+ ImmutableList.<PaymentJsonSimple>of(paymentJsonSimple),
+ ImmutableList.<InvoiceJsonSimple>of(invoiceJsonSimple),
+ reason);
final String asJson = mapper.writeValueAsString(bundleTimelineJson);
-
- final SubscriptionJsonWithEvents subscription = bundleTimelineJson.getBundle().getSubscriptions().get(0);
- final SubscriptionJsonWithEvents.SubscriptionReadEventJson event = subscription.getEvents().get(0);
- final PaymentJsonSimple payment = bundleTimelineJson.getPayments().get(0);
- final InvoiceJsonSimple invoice = bundleTimelineJson.getInvoices().get(0);
-
- Assert.assertEquals(asJson, "{\"viewId\":\"" + bundleTimelineJson.getViewId() + "\"," +
- "\"bundle\":{\"bundleId\":\"" + bundleTimelineJson.getBundle().getBundleId() + "\"," +
- "\"externalKey\":\"" + bundleTimelineJson.getBundle().getExternalKey() + "\"," +
- "\"subscriptions\":" +
- "[{\"events\":[{\"eventId\":\"" + event.getEventId() + "\"," +
- "\"billingPeriod\":\"" + event.getBillingPeriod() + "\"," +
- "\"product\":\"" + event.getProduct() + "\"," +
- "\"priceList\":\"" + event.getPriceList() + "\"," +
- "\"eventType\":\"" + event.getEventType() + "\"," +
- "\"phase\":\"" + event.getPhase() + "\"," +
- "\"requestedDate\":null," +
- "\"effectiveDate\":\"" + event.getEffectiveDate().toDateTimeISO().toString() + "\"}]," +
- "\"subscriptionId\":\"" + subscription.getSubscriptionId() + "\"," +
- "\"deletedEvents\":null," +
- "\"newEvents\":null}]}," +
- "\"payments\":[{\"amount\":" + payment.getAmount() + "," +
- "\"paidAmount\":" + payment.getPaidAmount() + "," +
- "\"accountId\":\"" + payment.getAccountId() + "\"," +
- "\"invoiceId\":\"" + payment.getInvoiceId() + "\"," +
- "\"paymentId\":\"" + payment.getPaymentId() + "\"," +
- "\"paymentMethodId\":\"" + payment.getPaymentMethodId() + "\"," +
- "\"requestedDate\":\"" + payment.getRequestedDate().toDateTimeISO().toString() + "\"," +
- "\"effectiveDate\":\"" + payment.getEffectiveDate().toDateTimeISO().toString() + "\"," +
- "\"retryCount\":" + payment.getRetryCount() + "," +
- "\"currency\":\"" + payment.getCurrency() + "\"," +
- "\"status\":\"" + payment.getStatus() + "\"," +
- "\"gatewayErrorCode\":\"" + payment.getGatewayErrorCode() + "\"," +
- "\"gatewayErrorMsg\":\"" + payment.getGatewayErrorMsg() + "\"," +
- "\"extFirstPaymentIdRef\":\"" + payment.getExtFirstPaymentIdRef() + "\"," +
- "\"extSecondPaymentIdRef\":\"" + payment.getExtSecondPaymentIdRef() + "\"}]," +
- "\"invoices\":[{\"amount\":" + invoice.getAmount() + "," +
- "\"cba\":" + invoice.getCBA() + "," +
- "\"creditAdj\":" + invoice.getCreditAdj() + "," +
- "\"refundAdj\":" + invoice.getRefundAdj() + "," +
- "\"invoiceId\":\"" + invoice.getInvoiceId() + "\"," +
- "\"invoiceDate\":\"" + invoice.getInvoiceDate().toString() + "\"," +
- "\"targetDate\":\"" + invoice.getTargetDate() + "\"," +
- "\"invoiceNumber\":\"" + invoice.getInvoiceNumber() + "\"," +
- "\"balance\":" + invoice.getBalance() + "," +
- "\"accountId\":\"" + invoice.getAccountId() + "\"}]," +
- "\"reasonForChange\":\"" + reason + "\"}");
-
final BundleTimelineJson fromJson = mapper.readValue(asJson, BundleTimelineJson.class);
Assert.assertEquals(fromJson, bundleTimelineJson);
}
@@ -125,8 +67,8 @@ public class TestBundleTimelineJson extends JaxrsTestSuite {
final DateTime effectiveDate = clock.getUTCNow();
final UUID eventId = UUID.randomUUID();
final PlanPhaseSpecifier planPhaseSpecifier = new PlanPhaseSpecifier(UUID.randomUUID().toString(), ProductCategory.BASE,
- BillingPeriod.NO_BILLING_PERIOD, UUID.randomUUID().toString(),
- PhaseType.EVERGREEN);
+ BillingPeriod.NO_BILLING_PERIOD, UUID.randomUUID().toString(),
+ PhaseType.EVERGREEN);
Mockito.when(event.getEffectiveDate()).thenReturn(effectiveDate);
Mockito.when(event.getEventId()).thenReturn(eventId);
Mockito.when(event.getSubscriptionTransitionType()).thenReturn(SubscriptionTransitionType.CREATE);
@@ -140,7 +82,7 @@ public class TestBundleTimelineJson extends JaxrsTestSuite {
final String externalKey = UUID.randomUUID().toString();
final SubscriptionJsonWithEvents subscription = new SubscriptionJsonWithEvents(bundleId, subscriptionTimeline);
- return new BundleJsonWithSubscriptions(bundleId.toString(), externalKey, ImmutableList.<SubscriptionJsonWithEvents>of(subscription));
+ return new BundleJsonWithSubscriptions(bundleId.toString(), externalKey, ImmutableList.<SubscriptionJsonWithEvents>of(subscription), null);
}
private InvoiceJsonSimple createInvoice() {
@@ -156,7 +98,7 @@ public class TestBundleTimelineJson extends JaxrsTestSuite {
final BigDecimal balance = BigDecimal.ZERO;
return new InvoiceJsonSimple(invoiceAmount, cba, creditAdj, refundAdj, invoiceId.toString(), invoiceDate,
- targetDate, invoiceNumber, balance, accountId.toString());
+ targetDate, invoiceNumber, balance, accountId.toString(), null);
}
private PaymentJsonSimple createPayment(final UUID accountId, final UUID invoiceId) {
@@ -175,6 +117,7 @@ public class TestBundleTimelineJson extends JaxrsTestSuite {
final String extSecondPaymentIdRef = UUID.randomUUID().toString();
return new PaymentJsonSimple(amount, paidAmount, accountId.toString(), invoiceId.toString(), paymentId.toString(),
- paymentMethodId.toString(), paymentRequestedDate, paymentEffectiveDate, retryCount, currency, status, gatewayErrorCode, gatewayErrorMsg, extFirstPaymentIdRef, extSecondPaymentIdRef);
+ paymentMethodId.toString(), paymentRequestedDate, paymentEffectiveDate, retryCount, currency, status,
+ gatewayErrorCode, gatewayErrorMsg, extFirstPaymentIdRef, extSecondPaymentIdRef, null);
}
}
diff --git a/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestChargebackCollectionJson.java b/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestChargebackCollectionJson.java
index af4c8c6..d58c565 100644
--- a/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestChargebackCollectionJson.java
+++ b/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestChargebackCollectionJson.java
@@ -17,6 +17,7 @@
package com.ning.billing.jaxrs.json;
import java.math.BigDecimal;
+import java.util.List;
import java.util.UUID;
import org.joda.time.DateTime;
@@ -24,13 +25,15 @@ import org.joda.time.DateTimeZone;
import org.testng.Assert;
import org.testng.annotations.Test;
+import com.ning.billing.jaxrs.JaxrsTestSuite;
+
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.joda.JodaModule;
import com.google.common.collect.ImmutableList;
-import com.ning.billing.jaxrs.JaxrsTestSuite;
public class TestChargebackCollectionJson extends JaxrsTestSuite {
+
private static final ObjectMapper mapper = new ObjectMapper();
static {
@@ -45,22 +48,18 @@ public class TestChargebackCollectionJson extends JaxrsTestSuite {
final BigDecimal chargebackAmount = BigDecimal.TEN;
final String paymentId = UUID.randomUUID().toString();
final String reason = UUID.randomUUID().toString();
- final ChargebackJson chargebackJson = new ChargebackJson(requestedDate, effectiveDate, chargebackAmount, paymentId, reason);
+ final List<AuditLogJson> auditLogs = createAuditLogsJson();
+ final ChargebackJson chargebackJson = new ChargebackJson(requestedDate, effectiveDate, chargebackAmount, paymentId,
+ reason, auditLogs);
final String accountId = UUID.randomUUID().toString();
final ChargebackCollectionJson chargebackCollectionJson = new ChargebackCollectionJson(accountId, ImmutableList.<ChargebackJson>of(chargebackJson));
Assert.assertEquals(chargebackCollectionJson.getAccountId(), accountId);
Assert.assertEquals(chargebackCollectionJson.getChargebacks().size(), 1);
Assert.assertEquals(chargebackCollectionJson.getChargebacks().get(0), chargebackJson);
+ Assert.assertEquals(chargebackCollectionJson.getChargebacks().get(0).getAuditLogs(), auditLogs);
final String asJson = mapper.writeValueAsString(chargebackCollectionJson);
- Assert.assertEquals(asJson, "{\"accountId\":\"" + accountId + "\",\"chargebacks\":[" +
- "{\"requestedDate\":\"" + chargebackJson.getRequestedDate() + "\"," +
- "\"effectiveDate\":\"" + chargebackJson.getEffectiveDate() + "\"," +
- "\"chargebackAmount\":" + chargebackJson.getChargebackAmount() + "," +
- "\"paymentId\":\"" + chargebackJson.getPaymentId() + "\"," +
- "\"reason\":\"" + chargebackJson.getReason() + "\"}]}");
-
final ChargebackCollectionJson fromJson = mapper.readValue(asJson, ChargebackCollectionJson.class);
Assert.assertEquals(fromJson, chargebackCollectionJson);
}
diff --git a/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestChargebackJson.java b/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestChargebackJson.java
index 006582d..b6d0b5e 100644
--- a/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestChargebackJson.java
+++ b/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestChargebackJson.java
@@ -17,6 +17,7 @@
package com.ning.billing.jaxrs.json;
import java.math.BigDecimal;
+import java.util.List;
import java.util.UUID;
import org.joda.time.DateTime;
@@ -44,20 +45,17 @@ public class TestChargebackJson extends JaxrsTestSuite {
final BigDecimal chargebackAmount = BigDecimal.TEN;
final String paymentId = UUID.randomUUID().toString();
final String reason = UUID.randomUUID().toString();
- final ChargebackJson chargebackJson = new ChargebackJson(requestedDate, effectiveDate, chargebackAmount, paymentId, reason);
+ final List<AuditLogJson> auditLogs = createAuditLogsJson();
+ final ChargebackJson chargebackJson = new ChargebackJson(requestedDate, effectiveDate, chargebackAmount, paymentId,
+ reason, auditLogs);
Assert.assertEquals(chargebackJson.getRequestedDate(), requestedDate);
Assert.assertEquals(chargebackJson.getEffectiveDate(), effectiveDate);
Assert.assertEquals(chargebackJson.getChargebackAmount(), chargebackAmount);
Assert.assertEquals(chargebackJson.getPaymentId(), paymentId);
Assert.assertEquals(chargebackJson.getReason(), reason);
+ Assert.assertEquals(chargebackJson.getAuditLogs(), auditLogs);
final String asJson = mapper.writeValueAsString(chargebackJson);
- Assert.assertEquals(asJson, "{\"requestedDate\":\"" + chargebackJson.getRequestedDate() + "\"," +
- "\"effectiveDate\":\"" + chargebackJson.getEffectiveDate() + "\"," +
- "\"chargebackAmount\":" + chargebackJson.getChargebackAmount() + "," +
- "\"paymentId\":\"" + chargebackJson.getPaymentId() + "\"," +
- "\"reason\":\"" + chargebackJson.getReason() + "\"}");
-
final ChargebackJson fromJson = mapper.readValue(asJson, ChargebackJson.class);
Assert.assertEquals(fromJson, chargebackJson);
}
diff --git a/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestCreditCollectionJson.java b/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestCreditCollectionJson.java
index 0579cac..04e8177 100644
--- a/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestCreditCollectionJson.java
+++ b/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestCreditCollectionJson.java
@@ -17,6 +17,7 @@
package com.ning.billing.jaxrs.json;
import java.math.BigDecimal;
+import java.util.List;
import java.util.UUID;
import org.joda.time.DateTime;
@@ -27,22 +28,12 @@ import com.ning.billing.jaxrs.JaxrsTestSuite;
import com.ning.billing.util.clock.Clock;
import com.ning.billing.util.clock.DefaultClock;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.SerializationFeature;
-import com.fasterxml.jackson.datatype.joda.JodaModule;
import com.google.common.collect.ImmutableList;
public class TestCreditCollectionJson extends JaxrsTestSuite {
- private static final ObjectMapper mapper = new ObjectMapper();
-
private final Clock clock = new DefaultClock();
- static {
- mapper.registerModule(new JodaModule());
- mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
- }
-
@Test(groups = "fast")
public void testJson() throws Exception {
final UUID accountId = UUID.randomUUID();
@@ -53,23 +44,17 @@ public class TestCreditCollectionJson extends JaxrsTestSuite {
final DateTime requestedDate = clock.getUTCNow();
final DateTime effectiveDate = clock.getUTCNow();
final String reason = UUID.randomUUID().toString();
- final CreditJson creditJson = new CreditJson(creditAmount, invoiceId, invoiceNumber, requestedDate, effectiveDate, reason, accountId);
+ final List<AuditLogJson> auditLogs = createAuditLogsJson();
+ final CreditJson creditJson = new CreditJson(creditAmount, invoiceId, invoiceNumber, requestedDate,
+ effectiveDate, reason, accountId, auditLogs);
final CreditCollectionJson creditCollectionJson = new CreditCollectionJson(accountId, ImmutableList.<CreditJson>of(creditJson));
Assert.assertEquals(creditCollectionJson.getAccountId(), accountId);
Assert.assertEquals(creditCollectionJson.getCredits().size(), 1);
Assert.assertEquals(creditCollectionJson.getCredits().get(0), creditJson);
+ Assert.assertEquals(creditCollectionJson.getCredits().get(0).getAuditLogs(), auditLogs);
final String asJson = mapper.writeValueAsString(creditCollectionJson);
- Assert.assertEquals(asJson, "{\"accountId\":\"" + accountId.toString() + "\"," +
- "\"credits\":[{\"creditAmount\":" + creditJson.getCreditAmount() + "," +
- "\"invoiceId\":\"" + creditJson.getInvoiceId().toString() + "\"," +
- "\"invoiceNumber\":\"" + creditJson.getInvoiceNumber() + "\"," +
- "\"requestedDate\":\"" + creditJson.getRequestedDate().toDateTimeISO().toString() + "\"," +
- "\"effectiveDate\":\"" + creditJson.getEffectiveDate().toDateTimeISO().toString() + "\"," +
- "\"reason\":\"" + creditJson.getReason() + "\"," +
- "\"accountId\":\"" + creditJson.getAccountId().toString() + "\"}]}");
-
final CreditCollectionJson fromJson = mapper.readValue(asJson, CreditCollectionJson.class);
Assert.assertEquals(fromJson, creditCollectionJson);
}
diff --git a/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestCreditJson.java b/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestCreditJson.java
index b18156c..913c5ec 100644
--- a/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestCreditJson.java
+++ b/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestCreditJson.java
@@ -17,6 +17,7 @@
package com.ning.billing.jaxrs.json;
import java.math.BigDecimal;
+import java.util.List;
import java.util.UUID;
import org.joda.time.DateTime;
@@ -27,21 +28,10 @@ import com.ning.billing.jaxrs.JaxrsTestSuite;
import com.ning.billing.util.clock.Clock;
import com.ning.billing.util.clock.DefaultClock;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.SerializationFeature;
-import com.fasterxml.jackson.datatype.joda.JodaModule;
-
public class TestCreditJson extends JaxrsTestSuite {
- private static final ObjectMapper mapper = new ObjectMapper();
-
private final Clock clock = new DefaultClock();
- static {
- mapper.registerModule(new JodaModule());
- mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
- }
-
@Test(groups = "fast")
public void testJson() throws Exception {
final BigDecimal creditAmount = BigDecimal.TEN;
@@ -51,8 +41,9 @@ public class TestCreditJson extends JaxrsTestSuite {
final DateTime effectiveDate = clock.getUTCNow();
final String reason = UUID.randomUUID().toString();
final UUID accountId = UUID.randomUUID();
-
- final CreditJson creditJson = new CreditJson(creditAmount, invoiceId, invoiceNumber, requestedDate, effectiveDate, reason, accountId);
+ final List<AuditLogJson> auditLogs = createAuditLogsJson();
+ final CreditJson creditJson = new CreditJson(creditAmount, invoiceId, invoiceNumber, requestedDate, effectiveDate,
+ reason, accountId, auditLogs);
Assert.assertEquals(creditJson.getRequestedDate(), requestedDate);
Assert.assertEquals(creditJson.getEffectiveDate(), effectiveDate);
Assert.assertEquals(creditJson.getCreditAmount(), creditAmount);
@@ -62,14 +53,6 @@ public class TestCreditJson extends JaxrsTestSuite {
Assert.assertEquals(creditJson.getAccountId(), accountId);
final String asJson = mapper.writeValueAsString(creditJson);
- Assert.assertEquals(asJson, "{\"creditAmount\":" + creditJson.getCreditAmount() + "," +
- "\"invoiceId\":\"" + creditJson.getInvoiceId().toString() + "\"," +
- "\"invoiceNumber\":\"" + creditJson.getInvoiceNumber() + "\"," +
- "\"requestedDate\":\"" + creditJson.getRequestedDate().toDateTimeISO().toString() + "\"," +
- "\"effectiveDate\":\"" + creditJson.getEffectiveDate().toDateTimeISO().toString() + "\"," +
- "\"reason\":\"" + creditJson.getReason() + "\"," +
- "\"accountId\":\"" + creditJson.getAccountId().toString() + "\"}");
-
final CreditJson fromJson = mapper.readValue(asJson, CreditJson.class);
Assert.assertEquals(fromJson, creditJson);
}
diff --git a/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestInvoiceJsonSimple.java b/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestInvoiceJsonSimple.java
index b517039..0e2ade2 100644
--- a/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestInvoiceJsonSimple.java
+++ b/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestInvoiceJsonSimple.java
@@ -17,6 +17,7 @@
package com.ning.billing.jaxrs.json;
import java.math.BigDecimal;
+import java.util.List;
import java.util.UUID;
import org.joda.time.LocalDate;
@@ -29,21 +30,10 @@ import com.ning.billing.jaxrs.JaxrsTestSuite;
import com.ning.billing.util.clock.Clock;
import com.ning.billing.util.clock.DefaultClock;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.SerializationFeature;
-import com.fasterxml.jackson.datatype.joda.JodaModule;
-
public class TestInvoiceJsonSimple extends JaxrsTestSuite {
- private static final ObjectMapper mapper = new ObjectMapper();
-
private final Clock clock = new DefaultClock();
- static {
- mapper.registerModule(new JodaModule());
- mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
- }
-
@Test(groups = "fast")
public void testJson() throws Exception {
final BigDecimal amount = BigDecimal.TEN;
@@ -56,8 +46,9 @@ public class TestInvoiceJsonSimple extends JaxrsTestSuite {
final String invoiceNumber = UUID.randomUUID().toString();
final BigDecimal balance = BigDecimal.ZERO;
final String accountId = UUID.randomUUID().toString();
+ final List<AuditLogJson> auditLogs = createAuditLogsJson();
final InvoiceJsonSimple invoiceJsonSimple = new InvoiceJsonSimple(amount, cba, creditAdj, refundAdj, invoiceId, invoiceDate,
- targetDate, invoiceNumber, balance, accountId);
+ targetDate, invoiceNumber, balance, accountId, auditLogs);
Assert.assertEquals(invoiceJsonSimple.getAmount(), amount);
Assert.assertEquals(invoiceJsonSimple.getCBA(), cba);
Assert.assertEquals(invoiceJsonSimple.getCreditAdj(), creditAdj);
@@ -68,19 +59,9 @@ public class TestInvoiceJsonSimple extends JaxrsTestSuite {
Assert.assertEquals(invoiceJsonSimple.getInvoiceNumber(), invoiceNumber);
Assert.assertEquals(invoiceJsonSimple.getBalance(), balance);
Assert.assertEquals(invoiceJsonSimple.getAccountId(), accountId);
+ Assert.assertEquals(invoiceJsonSimple.getAuditLogs(), auditLogs);
final String asJson = mapper.writeValueAsString(invoiceJsonSimple);
- Assert.assertEquals(asJson, "{\"amount\":" + invoiceJsonSimple.getAmount().toString() + "," +
- "\"cba\":" + invoiceJsonSimple.getCBA().toString() + "," +
- "\"creditAdj\":" + invoiceJsonSimple.getCreditAdj().toString() + "," +
- "\"refundAdj\":" + invoiceJsonSimple.getRefundAdj().toString() + "," +
- "\"invoiceId\":\"" + invoiceJsonSimple.getInvoiceId() + "\"," +
- "\"invoiceDate\":\"" + invoiceJsonSimple.getInvoiceDate().toString() + "\"," +
- "\"targetDate\":\"" + invoiceJsonSimple.getTargetDate().toString() + "\"," +
- "\"invoiceNumber\":\"" + invoiceJsonSimple.getInvoiceNumber() + "\"," +
- "\"balance\":" + invoiceJsonSimple.getBalance().toString() + "," +
- "\"accountId\":\"" + invoiceJsonSimple.getAccountId() + "\"}");
-
final InvoiceJsonSimple fromJson = mapper.readValue(asJson, InvoiceJsonSimple.class);
Assert.assertEquals(fromJson, invoiceJsonSimple);
}
@@ -110,5 +91,6 @@ public class TestInvoiceJsonSimple extends JaxrsTestSuite {
Assert.assertEquals(invoiceJsonSimple.getInvoiceNumber(), String.valueOf(invoice.getInvoiceNumber()));
Assert.assertEquals(invoiceJsonSimple.getBalance(), invoice.getBalance());
Assert.assertEquals(invoiceJsonSimple.getAccountId(), invoice.getAccountId().toString());
+ Assert.assertNull(invoiceJsonSimple.getAuditLogs());
}
}
diff --git a/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestInvoiceJsonWithBundleKeys.java b/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestInvoiceJsonWithBundleKeys.java
index 5c2ca0b..d312294 100644
--- a/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestInvoiceJsonWithBundleKeys.java
+++ b/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestInvoiceJsonWithBundleKeys.java
@@ -31,22 +31,12 @@ import com.ning.billing.jaxrs.JaxrsTestSuite;
import com.ning.billing.util.clock.Clock;
import com.ning.billing.util.clock.DefaultClock;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.SerializationFeature;
-import com.fasterxml.jackson.datatype.joda.JodaModule;
import com.google.common.collect.ImmutableList;
public class TestInvoiceJsonWithBundleKeys extends JaxrsTestSuite {
- private static final ObjectMapper mapper = new ObjectMapper();
-
private final Clock clock = new DefaultClock();
- static {
- mapper.registerModule(new JodaModule());
- mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
- }
-
@Test(groups = "fast")
public void testJson() throws Exception {
final BigDecimal amount = BigDecimal.TEN;
@@ -60,11 +50,12 @@ public class TestInvoiceJsonWithBundleKeys extends JaxrsTestSuite {
final BigDecimal balance = BigDecimal.ZERO;
final String accountId = UUID.randomUUID().toString();
final String bundleKeys = UUID.randomUUID().toString();
- CreditJson creditJson = createCreditJson();
+ final CreditJson creditJson = createCreditJson();
final List<CreditJson> credits = ImmutableList.<CreditJson>of(creditJson);
+ final List<AuditLogJson> auditLogs = createAuditLogsJson();
final InvoiceJsonWithBundleKeys invoiceJsonSimple = new InvoiceJsonWithBundleKeys(amount, cba, creditAdj, refundAdj, invoiceId, invoiceDate,
targetDate, invoiceNumber, balance, accountId, bundleKeys,
- credits);
+ credits, auditLogs);
Assert.assertEquals(invoiceJsonSimple.getAmount(), amount);
Assert.assertEquals(invoiceJsonSimple.getCBA(), cba);
Assert.assertEquals(invoiceJsonSimple.getCreditAdj(), creditAdj);
@@ -77,28 +68,9 @@ public class TestInvoiceJsonWithBundleKeys extends JaxrsTestSuite {
Assert.assertEquals(invoiceJsonSimple.getAccountId(), accountId);
Assert.assertEquals(invoiceJsonSimple.getBundleKeys(), bundleKeys);
Assert.assertEquals(invoiceJsonSimple.getCredits(), credits);
+ Assert.assertEquals(invoiceJsonSimple.getAuditLogs(), auditLogs);
final String asJson = mapper.writeValueAsString(invoiceJsonSimple);
- Assert.assertEquals(asJson, "{\"amount\":" + invoiceJsonSimple.getAmount().toString() + "," +
- "\"cba\":" + invoiceJsonSimple.getCBA().toString() + "," +
- "\"creditAdj\":" + invoiceJsonSimple.getCreditAdj().toString() + "," +
- "\"refundAdj\":" + invoiceJsonSimple.getRefundAdj().toString() + "," +
- "\"invoiceId\":\"" + invoiceJsonSimple.getInvoiceId() + "\"," +
- "\"invoiceDate\":\"" + invoiceJsonSimple.getInvoiceDate().toString() + "\"," +
- "\"targetDate\":\"" + invoiceJsonSimple.getTargetDate().toString() + "\"," +
- "\"invoiceNumber\":\"" + invoiceJsonSimple.getInvoiceNumber() + "\"," +
- "\"balance\":" + invoiceJsonSimple.getBalance().toString() + "," +
- "\"accountId\":\"" + invoiceJsonSimple.getAccountId() + "\"," +
- "\"credits\":[" +
- "{\"creditAmount\":" + creditJson.getCreditAmount() + "," +
- "\"invoiceId\":\"" + creditJson.getInvoiceId().toString() + "\"," +
- "\"invoiceNumber\":\"" + creditJson.getInvoiceNumber() + "\"," +
- "\"requestedDate\":\"" + creditJson.getRequestedDate().toDateTimeISO().toString() + "\"," +
- "\"effectiveDate\":\"" + creditJson.getEffectiveDate().toDateTimeISO().toString() + "\"," +
- "\"reason\":\"" + creditJson.getReason() + "\"," +
- "\"accountId\":\"" + creditJson.getAccountId().toString() + "\"}]," +
- "\"bundleKeys\":\"" + invoiceJsonSimple.getBundleKeys() + "\"}");
-
final InvoiceJsonWithBundleKeys fromJson = mapper.readValue(asJson, InvoiceJsonWithBundleKeys.class);
Assert.assertEquals(fromJson, invoiceJsonSimple);
}
@@ -120,7 +92,7 @@ public class TestInvoiceJsonWithBundleKeys extends JaxrsTestSuite {
final String bundleKeys = UUID.randomUUID().toString();
final List<CreditJson> credits = ImmutableList.<CreditJson>of(createCreditJson());
- final InvoiceJsonWithBundleKeys invoiceJsonWithBundleKeys = new InvoiceJsonWithBundleKeys(invoice, bundleKeys, credits);
+ final InvoiceJsonWithBundleKeys invoiceJsonWithBundleKeys = new InvoiceJsonWithBundleKeys(invoice, bundleKeys, credits, null);
Assert.assertEquals(invoiceJsonWithBundleKeys.getAmount(), invoice.getChargedAmount());
Assert.assertEquals(invoiceJsonWithBundleKeys.getCBA(), invoice.getCBAAmount());
Assert.assertEquals(invoiceJsonWithBundleKeys.getCreditAdj(), invoice.getCreditAdjAmount());
@@ -133,6 +105,7 @@ public class TestInvoiceJsonWithBundleKeys extends JaxrsTestSuite {
Assert.assertEquals(invoiceJsonWithBundleKeys.getAccountId(), invoice.getAccountId().toString());
Assert.assertEquals(invoiceJsonWithBundleKeys.getBundleKeys(), bundleKeys);
Assert.assertEquals(invoiceJsonWithBundleKeys.getCredits(), credits);
+ Assert.assertNull(invoiceJsonWithBundleKeys.getAuditLogs());
}
private CreditJson createCreditJson() {
@@ -143,6 +116,6 @@ public class TestInvoiceJsonWithBundleKeys extends JaxrsTestSuite {
final DateTime effectiveDate = clock.getUTCNow();
final String reason = UUID.randomUUID().toString();
final UUID accountId = UUID.randomUUID();
- return new CreditJson(creditAmount, invoiceId, invoiceNumber, requestedDate, effectiveDate, reason, accountId);
+ return new CreditJson(creditAmount, invoiceId, invoiceNumber, requestedDate, effectiveDate, reason, accountId, null);
}
}
diff --git a/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestInvoiceJsonWithItems.java b/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestInvoiceJsonWithItems.java
index e849f0e..c66bc4f 100644
--- a/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestInvoiceJsonWithItems.java
+++ b/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestInvoiceJsonWithItems.java
@@ -17,6 +17,7 @@
package com.ning.billing.jaxrs.json;
import java.math.BigDecimal;
+import java.util.List;
import java.util.UUID;
import org.joda.time.LocalDate;
@@ -31,22 +32,12 @@ import com.ning.billing.jaxrs.JaxrsTestSuite;
import com.ning.billing.util.clock.Clock;
import com.ning.billing.util.clock.DefaultClock;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.SerializationFeature;
-import com.fasterxml.jackson.datatype.joda.JodaModule;
import com.google.common.collect.ImmutableList;
public class TestInvoiceJsonWithItems extends JaxrsTestSuite {
- private static final ObjectMapper mapper = new ObjectMapper();
-
private final Clock clock = new DefaultClock();
- static {
- mapper.registerModule(new JodaModule());
- mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
- }
-
@Test(groups = "fast")
public void testJson() throws Exception {
final BigDecimal amount = BigDecimal.TEN;
@@ -60,9 +51,10 @@ public class TestInvoiceJsonWithItems extends JaxrsTestSuite {
final BigDecimal balance = BigDecimal.ZERO;
final String accountId = UUID.randomUUID().toString();
final InvoiceItemJsonSimple invoiceItemJsonSimple = createInvoiceItemJson();
+ final List<AuditLogJson> auditLogs = createAuditLogsJson();
final InvoiceJsonWithItems invoiceJsonWithItems = new InvoiceJsonWithItems(amount, cba, creditAdj, refundAdj, invoiceId, invoiceDate,
targetDate, invoiceNumber, balance, accountId,
- ImmutableList.<InvoiceItemJsonSimple>of(invoiceItemJsonSimple));
+ ImmutableList.<InvoiceItemJsonSimple>of(invoiceItemJsonSimple), auditLogs);
Assert.assertEquals(invoiceJsonWithItems.getAmount(), amount);
Assert.assertEquals(invoiceJsonWithItems.getCBA(), cba);
Assert.assertEquals(invoiceJsonWithItems.getCreditAdj(), creditAdj);
@@ -75,30 +67,9 @@ public class TestInvoiceJsonWithItems extends JaxrsTestSuite {
Assert.assertEquals(invoiceJsonWithItems.getAccountId(), accountId);
Assert.assertEquals(invoiceJsonWithItems.getItems().size(), 1);
Assert.assertEquals(invoiceJsonWithItems.getItems().get(0), invoiceItemJsonSimple);
+ Assert.assertEquals(invoiceJsonWithItems.getAuditLogs(), auditLogs);
final String asJson = mapper.writeValueAsString(invoiceJsonWithItems);
- Assert.assertEquals(asJson, "{\"amount\":" + invoiceJsonWithItems.getAmount().toString() + "," +
- "\"cba\":" + invoiceJsonWithItems.getCBA().toString() + "," +
- "\"creditAdj\":" + invoiceJsonWithItems.getCreditAdj().toString() + "," +
- "\"refundAdj\":" + invoiceJsonWithItems.getRefundAdj().toString() + "," +
- "\"invoiceId\":\"" + invoiceJsonWithItems.getInvoiceId() + "\"," +
- "\"invoiceDate\":\"" + invoiceJsonWithItems.getInvoiceDate().toString() + "\"," +
- "\"targetDate\":\"" + invoiceJsonWithItems.getTargetDate().toString() + "\"," +
- "\"invoiceNumber\":\"" + invoiceJsonWithItems.getInvoiceNumber() + "\"," +
- "\"balance\":" + invoiceJsonWithItems.getBalance().toString() + "," +
- "\"accountId\":\"" + invoiceJsonWithItems.getAccountId() + "\"," +
- "\"items\":[{\"invoiceId\":\"" + invoiceItemJsonSimple.getInvoiceId().toString() + "\"," +
- "\"accountId\":\"" + invoiceItemJsonSimple.getAccountId().toString() + "\"," +
- "\"bundleId\":\"" + invoiceItemJsonSimple.getBundleId().toString() + "\"," +
- "\"subscriptionId\":\"" + invoiceItemJsonSimple.getSubscriptionId().toString() + "\"," +
- "\"planName\":\"" + invoiceItemJsonSimple.getPlanName() + "\"," +
- "\"phaseName\":\"" + invoiceItemJsonSimple.getPhaseName() + "\"," +
- "\"description\":\"" + invoiceItemJsonSimple.getDescription() + "\"," +
- "\"startDate\":\"" + invoiceItemJsonSimple.getStartDate().toString() + "\"," +
- "\"endDate\":\"" + invoiceItemJsonSimple.getEndDate().toString() + "\"," +
- "\"amount\":" + invoiceItemJsonSimple.getAmount().toString() + "," +
- "\"currency\":\"" + invoiceItemJsonSimple.getCurrency().toString() + "\"}]}");
-
final InvoiceJsonWithItems fromJson = mapper.readValue(asJson, InvoiceJsonWithItems.class);
Assert.assertEquals(fromJson, invoiceJsonWithItems);
}
@@ -125,6 +96,8 @@ public class TestInvoiceJsonWithItems extends JaxrsTestSuite {
Assert.assertEquals(invoiceJsonWithItems.getBalance(), invoice.getBalance());
Assert.assertEquals(invoiceJsonWithItems.getAccountId(), invoice.getAccountId().toString());
Assert.assertEquals(invoiceJsonWithItems.getItems().size(), 1);
+ Assert.assertNull(invoiceJsonWithItems.getAuditLogs());
+
final InvoiceItemJsonSimple invoiceItemJsonSimple = invoiceJsonWithItems.getItems().get(0);
Assert.assertEquals(invoiceItemJsonSimple.getInvoiceId(), invoiceItem.getInvoiceId());
Assert.assertEquals(invoiceItemJsonSimple.getAccountId(), invoiceItem.getAccountId());
pom.xml 16(+16 -0)
diff --git a/pom.xml b/pom.xml
index 7d00037..03f99e9 100644
--- a/pom.xml
+++ b/pom.xml
@@ -48,6 +48,7 @@
<module>junction</module>
<module>overdue</module>
<module>payment</module>
+ <module>usage</module>
<module>util</module>
<module>jaxrs</module>
<module>server</module>
@@ -177,6 +178,11 @@
</dependency>
<dependency>
<groupId>com.ning.billing</groupId>
+ <artifactId>killbill-usage</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.ning.billing</groupId>
<artifactId>killbill-overdue</artifactId>
<version>${project.version}</version>
</dependency>
@@ -216,6 +222,11 @@
<version>2.0.0</version>
</dependency>
<dependency>
+ <groupId>com.fasterxml.jackson.dataformat</groupId>
+ <artifactId>jackson-dataformat-smile</artifactId>
+ <version>2.0.1</version>
+ </dependency>
+ <dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-joda</artifactId>
<version>2.0.1</version>
@@ -226,6 +237,11 @@
<version>2.0.0</version>
</dependency>
<dependency>
+ <groupId>com.fasterxml.util</groupId>
+ <artifactId>low-gc-membuffers</artifactId>
+ <version>0.9.0</version>
+ </dependency>
+ <dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>12.0</version>
diff --git a/server/src/main/java/com/ning/billing/server/modules/KillbillServerModule.java b/server/src/main/java/com/ning/billing/server/modules/KillbillServerModule.java
index fda58c0..32ff962 100644
--- a/server/src/main/java/com/ning/billing/server/modules/KillbillServerModule.java
+++ b/server/src/main/java/com/ning/billing/server/modules/KillbillServerModule.java
@@ -41,6 +41,7 @@ import com.ning.billing.junction.glue.DefaultJunctionModule;
import com.ning.billing.payment.glue.PaymentModule;
import com.ning.billing.util.email.EmailModule;
import com.ning.billing.util.email.templates.TemplateModule;
+import com.ning.billing.util.glue.AuditModule;
import com.ning.billing.util.glue.BusModule;
import com.ning.billing.util.glue.CallContextModule;
import com.ning.billing.util.glue.ClockModule;
@@ -91,6 +92,7 @@ public class KillbillServerModule extends AbstractModule {
install(new GlobalLockerModule());
install(new CustomFieldModule());
install(new TagStoreModule());
+ install(new AuditModule());
install(new CatalogModule());
install(new BusModule());
install(new NotificationQueueModule());
diff --git a/server/src/test/java/com/ning/billing/jaxrs/TestBundle.java b/server/src/test/java/com/ning/billing/jaxrs/TestBundle.java
index 749e108..94ae280 100644
--- a/server/src/test/java/com/ning/billing/jaxrs/TestBundle.java
+++ b/server/src/test/java/com/ning/billing/jaxrs/TestBundle.java
@@ -24,11 +24,15 @@ import java.util.Map;
import javax.ws.rs.core.Response.Status;
+import org.joda.time.DateTime;
import org.testng.Assert;
import org.testng.annotations.Test;
+import com.ning.billing.catalog.api.BillingPeriod;
+import com.ning.billing.catalog.api.ProductCategory;
import com.ning.billing.jaxrs.json.AccountJson;
import com.ning.billing.jaxrs.json.BundleJsonNoSubscriptions;
+import com.ning.billing.jaxrs.json.SubscriptionJsonNoEvents;
import com.ning.billing.jaxrs.resources.JaxrsResource;
import com.ning.http.client.Response;
@@ -107,5 +111,34 @@ public class TestBundle extends TestJaxrsBase {
Assert.assertEquals(response.getStatusCode(), Status.NOT_FOUND.getStatusCode());
}
+ @Test(groups = "slow", enabled = true)
+ public void testBundleTransfer() throws Exception {
+
+ final DateTime initialDate = new DateTime(2012, 4, 25, 0, 3, 42, 0);
+ clock.setDeltaFromReality(initialDate.getMillis() - clock.getUTCNow().getMillis());
+
+ final AccountJson accountJson = createAccountWithDefaultPaymentMethod("src", "src", "src@yahoo.com");
+ final BundleJsonNoSubscriptions bundleJson = createBundle(accountJson.getAccountId(), "93199");
+
+ final String productName = "Shotgun";
+ final BillingPeriod term = BillingPeriod.MONTHLY;
+
+ final SubscriptionJsonNoEvents subscriptionJson = createSubscription(bundleJson.getBundleId(), productName, ProductCategory.BASE.toString(), term.toString(), true);
+ Assert.assertNotNull(subscriptionJson.getChargedThroughDate());
+ Assert.assertEquals(subscriptionJson.getChargedThroughDate().toString(), "2012-04-25T00:00:00.000Z");
+ final AccountJson newAccount = createAccountWithDefaultPaymentMethod("dst", "dst", "dst@yahoo.com");
+
+ final BundleJsonNoSubscriptions newBundleInput = new BundleJsonNoSubscriptions(null, newAccount.getAccountId(), null, null, null);
+ final String newBundleInputJson = mapper.writeValueAsString(newBundleInput);
+ final String uri = JaxrsResource.BUNDLES_PATH + "/" + bundleJson.getBundleId();
+ Response response = doPut(uri, newBundleInputJson, DEFAULT_EMPTY_QUERY, DEFAULT_HTTP_TIMEOUT_SEC);
+ Assert.assertEquals(response.getStatusCode(), Status.CREATED.getStatusCode());
+
+ final String locationCC = response.getHeader("Location");
+ Assert.assertNotNull(locationCC);
+
+ response = doGetWithUrl(locationCC, DEFAULT_EMPTY_QUERY, DEFAULT_HTTP_TIMEOUT_SEC);
+ Assert.assertEquals(response.getStatusCode(), Status.OK.getStatusCode());
+ }
}
diff --git a/server/src/test/java/com/ning/billing/jaxrs/TestChargeback.java b/server/src/test/java/com/ning/billing/jaxrs/TestChargeback.java
index c20e0fe..c472456 100644
--- a/server/src/test/java/com/ning/billing/jaxrs/TestChargeback.java
+++ b/server/src/test/java/com/ning/billing/jaxrs/TestChargeback.java
@@ -52,7 +52,7 @@ public class TestChargeback extends TestJaxrsBase {
@Test(groups = "slow")
public void testAddChargeback() throws Exception {
final PaymentJsonSimple payment = createAccountWithInvoiceAndPayment();
- final ChargebackJson input = new ChargebackJson(null, null, BigDecimal.TEN, payment.getPaymentId(), null);
+ final ChargebackJson input = new ChargebackJson(null, null, BigDecimal.TEN, payment.getPaymentId(), null, null);
final String jsonInput = mapper.writeValueAsString(input);
// Create the chargeback
@@ -81,7 +81,7 @@ public class TestChargeback extends TestJaxrsBase {
final PaymentJsonSimple payment = createAccountWithInvoiceAndPayment();
// We get a 249.95 payment so we do 4 chargeback and then the fifth should fail
- final ChargebackJson input = new ChargebackJson(null, null, new BigDecimal("50.00"), payment.getPaymentId(), null);
+ final ChargebackJson input = new ChargebackJson(null, null, new BigDecimal("50.00"), payment.getPaymentId(), null, null);
final String jsonInput = mapper.writeValueAsString(input);
//
@@ -135,7 +135,8 @@ public class TestChargeback extends TestJaxrsBase {
@Test(groups = "slow")
public void testInvoicePaymentDoesNotExist() throws Exception {
final ChargebackJson input = new ChargebackJson(new DateTime(DateTimeZone.UTC), new DateTime(DateTimeZone.UTC),
- BigDecimal.TEN, UUID.randomUUID().toString(), UUID.randomUUID().toString());
+ BigDecimal.TEN, UUID.randomUUID().toString(), UUID.randomUUID().toString(),
+ null);
final String jsonInput = mapper.writeValueAsString(input);
// Try to create the chargeback
@@ -145,7 +146,7 @@ public class TestChargeback extends TestJaxrsBase {
@Test(groups = "slow")
public void testBadRequest() throws Exception {
- final ChargebackJson input = new ChargebackJson(null, null, null, null, null);
+ final ChargebackJson input = new ChargebackJson(null, null, null, null, null, null);
final String jsonInput = mapper.writeValueAsString(input);
// Try to create the chargeback
diff --git a/server/src/test/java/com/ning/billing/jaxrs/TestCredit.java b/server/src/test/java/com/ning/billing/jaxrs/TestCredit.java
index dc67b41..d7b5dca 100644
--- a/server/src/test/java/com/ning/billing/jaxrs/TestCredit.java
+++ b/server/src/test/java/com/ning/billing/jaxrs/TestCredit.java
@@ -62,7 +62,8 @@ public class TestCredit extends TestJaxrsBase {
final InvoiceJsonSimple invoice = createInvoice();
final CreditJson input = new CreditJson(BigDecimal.TEN, UUID.fromString(invoice.getInvoiceId()), UUID.randomUUID().toString(),
requestedDate, effectiveDate,
- UUID.randomUUID().toString(), UUID.fromString(accountJson.getAccountId()));
+ UUID.randomUUID().toString(), UUID.fromString(accountJson.getAccountId()),
+ null);
final String jsonInput = mapper.writeValueAsString(input);
// Create the credit
@@ -121,7 +122,7 @@ public class TestCredit extends TestJaxrsBase {
final DateTime effectiveDate = clock.getUTCNow();
final CreditJson input = new CreditJson(BigDecimal.TEN, UUID.randomUUID(), UUID.randomUUID().toString(),
requestedDate, effectiveDate,
- UUID.randomUUID().toString(), UUID.randomUUID());
+ UUID.randomUUID().toString(), UUID.randomUUID(), null);
final String jsonInput = mapper.writeValueAsString(input);
// Try to create the credit
@@ -131,7 +132,7 @@ public class TestCredit extends TestJaxrsBase {
@Test(groups = "slow")
public void testBadRequest() throws Exception {
- final CreditJson input = new CreditJson(null, null, null, null, null, null, null);
+ final CreditJson input = new CreditJson(null, null, null, null, null, null, null, null);
final String jsonInput = mapper.writeValueAsString(input);
// Try to create the credit
diff --git a/server/src/test/java/com/ning/billing/jaxrs/TestInvoice.java b/server/src/test/java/com/ning/billing/jaxrs/TestInvoice.java
index 4efe6cb..4dfc5ab 100644
--- a/server/src/test/java/com/ning/billing/jaxrs/TestInvoice.java
+++ b/server/src/test/java/com/ning/billing/jaxrs/TestInvoice.java
@@ -209,7 +209,8 @@ public class TestInvoice extends TestJaxrsBase {
}
// CREATE INSTA PAYMENT
- final PaymentJsonSimple payment = new PaymentJsonSimple(cur.getAmount(), BigDecimal.ZERO, accountJson.getAccountId(), cur.getInvoiceId(), null, null, null, null, 0, null, null, null, null, null, null);
+ final PaymentJsonSimple payment = new PaymentJsonSimple(cur.getAmount(), BigDecimal.ZERO, accountJson.getAccountId(),
+ cur.getInvoiceId(), null, null, null, null, 0, null, null, null, null, null, null, null);
final String postJson = mapper.writeValueAsString(payment);
uri = JaxrsResource.INVOICES_PATH + "/" + cur.getInvoiceId() + "/" + JaxrsResource.PAYMENTS;
@@ -267,7 +268,7 @@ public class TestInvoice extends TestJaxrsBase {
final BigDecimal paidAmount = BigDecimal.TEN;
final PaymentJsonSimple payment = new PaymentJsonSimple(paidAmount, BigDecimal.ZERO, accountJson.getAccountId(),
invoiceId, null, null, null, null, 0,
- null, null, null, null, null, null);
+ null, null, null, null, null, null, null);
final String postJson = mapper.writeValueAsString(payment);
final String paymentURI = JaxrsResource.INVOICES_PATH + "/" + invoiceId + "/" + JaxrsResource.PAYMENTS;
final Response paymentResponse = doPost(paymentURI, postJson, ImmutableMap.<String, String>of("externalPayment", "true"), DEFAULT_HTTP_TIMEOUT_SEC);
diff --git a/server/src/test/java/com/ning/billing/jaxrs/TestJaxrsBase.java b/server/src/test/java/com/ning/billing/jaxrs/TestJaxrsBase.java
index 9153cd5..aba8426 100644
--- a/server/src/test/java/com/ning/billing/jaxrs/TestJaxrsBase.java
+++ b/server/src/test/java/com/ning/billing/jaxrs/TestJaxrsBase.java
@@ -81,6 +81,7 @@ import com.ning.billing.util.clock.Clock;
import com.ning.billing.util.clock.ClockMock;
import com.ning.billing.util.email.EmailModule;
import com.ning.billing.util.email.templates.TemplateModule;
+import com.ning.billing.util.glue.AuditModule;
import com.ning.billing.util.glue.BusModule;
import com.ning.billing.util.glue.CallContextModule;
import com.ning.billing.util.glue.CustomFieldModule;
@@ -209,6 +210,7 @@ public class TestJaxrsBase extends ServerTestSuiteWithEmbeddedDB {
install(new GlobalLockerModule());
install(new CustomFieldModule());
install(new TagStoreModule());
+ install(new AuditModule());
install(new CatalogModule());
install(new BusModule());
install(new NotificationQueueModule());
@@ -370,7 +372,7 @@ public class TestJaxrsBase extends ServerTestSuiteWithEmbeddedDB {
protected BundleJsonNoSubscriptions createBundle(final String accountId, final String key) throws Exception {
- final BundleJsonNoSubscriptions input = new BundleJsonNoSubscriptions(null, accountId, key, null);
+ final BundleJsonNoSubscriptions input = new BundleJsonNoSubscriptions(null, accountId, key, null, null);
String baseJson = mapper.writeValueAsString(input);
Response response = doPost(JaxrsResource.BUNDLES_PATH, baseJson, DEFAULT_EMPTY_QUERY, DEFAULT_HTTP_TIMEOUT_SEC);
Assert.assertEquals(response.getStatusCode(), Status.CREATED.getStatusCode());
diff --git a/server/src/test/java/com/ning/billing/jaxrs/TestPayment.java b/server/src/test/java/com/ning/billing/jaxrs/TestPayment.java
index 0afc6b8..2b16d3f 100644
--- a/server/src/test/java/com/ning/billing/jaxrs/TestPayment.java
+++ b/server/src/test/java/com/ning/billing/jaxrs/TestPayment.java
@@ -92,7 +92,7 @@ public class TestPayment extends TestJaxrsBase {
// Issue the refund
- final RefundJson refundJson = new RefundJson(null, paymentId, paymentAmount, false, null, null);
+ final RefundJson refundJson = new RefundJson(null, paymentId, paymentAmount, false, null, null, null);
baseJson = mapper.writeValueAsString(refundJson);
response = doPost(uri, baseJson, DEFAULT_EMPTY_QUERY, DEFAULT_HTTP_TIMEOUT_SEC);
assertEquals(response.getStatusCode(), Status.CREATED.getStatusCode());
usage/pom.xml 110(+110 -0)
diff --git a/usage/pom.xml b/usage/pom.xml
new file mode 100644
index 0000000..f97c4fa
--- /dev/null
+++ b/usage/pom.xml
@@ -0,0 +1,110 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- ~ Copyright 2010-2011 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. -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>com.ning.billing</groupId>
+ <artifactId>killbill</artifactId>
+ <version>0.1.26-SNAPSHOT</version>
+ <relativePath>../pom.xml</relativePath>
+ </parent>
+ <artifactId>killbill-usage</artifactId>
+ <name>killbill-usage</name>
+ <packaging>jar</packaging>
+ <dependencies>
+ <dependency>
+ <groupId>org.jdbi</groupId>
+ <artifactId>jdbi</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-core</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.fasterxml.jackson.datatype</groupId>
+ <artifactId>jackson-datatype-joda</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.fasterxml.jackson.dataformat</groupId>
+ <artifactId>jackson-dataformat-smile</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.fasterxml.util</groupId>
+ <artifactId>low-gc-membuffers</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.google.inject</groupId>
+ <artifactId>guice</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.ning.billing</groupId>
+ <artifactId>killbill-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.ning.billing</groupId>
+ <artifactId>killbill-util</artifactId>
+ <type>test-jar</type>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.ning.billing</groupId>
+ <artifactId>killbill-util</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>joda-time</groupId>
+ <artifactId>joda-time</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.skife.config</groupId>
+ <artifactId>config-magic</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.antlr</groupId>
+ <artifactId>stringtemplate</artifactId>
+ <scope>runtime</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.google.guava</groupId>
+ <artifactId>guava</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-simple</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>mysql</groupId>
+ <artifactId>mysql-connector-mxj</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>mysql</groupId>
+ <artifactId>mysql-connector-mxj-db-files</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-all</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.testng</groupId>
+ <artifactId>testng</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+</project>
diff --git a/usage/src/main/java/com/ning/billing/usage/api/user/DefaultUsageUserApi.java b/usage/src/main/java/com/ning/billing/usage/api/user/DefaultUsageUserApi.java
new file mode 100644
index 0000000..8f1e6a9
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/api/user/DefaultUsageUserApi.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2010-2012 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.usage.api.user;
+
+import java.util.UUID;
+
+import javax.inject.Inject;
+
+import org.joda.time.DateTime;
+
+import com.ning.billing.usage.api.UsageUserApi;
+import com.ning.billing.usage.dao.RolledUpUsageDao;
+import com.ning.billing.usage.timeline.TimelineEventHandler;
+import com.ning.billing.util.clock.Clock;
+
+import com.google.common.collect.ImmutableMap;
+
+public class DefaultUsageUserApi implements UsageUserApi {
+
+ private static final String DEFAULT_EVENT_TYPE = "__DefaultUsageUserApi__";
+
+ private final RolledUpUsageDao rolledUpUsageDao;
+ private final TimelineEventHandler timelineEventHandler;
+ private final Clock clock;
+
+ @Inject
+ public DefaultUsageUserApi(final RolledUpUsageDao rolledUpUsageDao, final TimelineEventHandler timelineEventHandler, final Clock clock) {
+ this.rolledUpUsageDao = rolledUpUsageDao;
+ this.timelineEventHandler = timelineEventHandler;
+ this.clock = clock;
+ }
+
+ @Override
+ public void incrementUsage(final UUID bundleId, final String metricName) {
+ recordUsage(bundleId, metricName, clock.getUTCNow(), 1);
+ }
+
+ @Override
+ public void recordUsage(final UUID bundleId, final String metricName, final DateTime timestamp, final long value) {
+ final String sourceName = getSourceNameFromBundleId(bundleId);
+ timelineEventHandler.record(sourceName, DEFAULT_EVENT_TYPE, timestamp, ImmutableMap.<String, Object>of(metricName, value));
+ }
+
+ @Override
+ public void recordRolledUpUsage(final UUID bundleId, final String metricName, final DateTime startDate, final DateTime endDate, final long value) {
+ final String sourceName = getSourceNameFromBundleId(bundleId);
+ rolledUpUsageDao.record(sourceName, DEFAULT_EVENT_TYPE, metricName, startDate, endDate, value);
+ }
+
+ private String getSourceNameFromBundleId(final UUID bundleId) {
+ // TODO we should do better
+ return bundleId.toString();
+ }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/dao/DefaultRolledUpUsageDao.java b/usage/src/main/java/com/ning/billing/usage/dao/DefaultRolledUpUsageDao.java
new file mode 100644
index 0000000..58e3dce
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/dao/DefaultRolledUpUsageDao.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2010-2012 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.usage.dao;
+
+import javax.inject.Inject;
+
+import org.joda.time.DateTime;
+import org.skife.jdbi.v2.Transaction;
+import org.skife.jdbi.v2.TransactionStatus;
+
+import com.ning.billing.usage.timeline.persistent.TimelineSqlDao;
+
+public class DefaultRolledUpUsageDao implements RolledUpUsageDao {
+
+ private final RolledUpUsageSqlDao rolledUpUsageSqlDao;
+
+ @Inject
+ public DefaultRolledUpUsageDao(final RolledUpUsageSqlDao rolledUpUsageSqlDao) {
+ this.rolledUpUsageSqlDao = rolledUpUsageSqlDao;
+ }
+
+ @Override
+ public void record(final String source, final String eventType, final String metricName, final DateTime startDate, final DateTime endDate, final long value) {
+ rolledUpUsageSqlDao.inTransaction(new Transaction<Void, RolledUpUsageSqlDao>() {
+ @Override
+ public Void inTransaction(final RolledUpUsageSqlDao transactional, final TransactionStatus status) throws Exception {
+ final TimelineSqlDao timelineSqlDao = transactional.become(TimelineSqlDao.class);
+
+ // Create the source if it doesn't exist
+ Integer sourceId = timelineSqlDao.getSourceId(source);
+ if (sourceId == null) {
+ timelineSqlDao.addSource(source);
+ sourceId = timelineSqlDao.getSourceId(source);
+ }
+
+ // Create the category if it doesn't exist
+ Integer categoryId = timelineSqlDao.getEventCategoryId(eventType);
+ if (categoryId == null) {
+ timelineSqlDao.addEventCategory(eventType);
+ categoryId = timelineSqlDao.getEventCategoryId(eventType);
+ }
+
+ // Create the metric if it doesn't exist
+ Integer metricId = timelineSqlDao.getMetricId(categoryId, metricName);
+ if (metricId == null) {
+ timelineSqlDao.addMetric(categoryId, metricName);
+ metricId = timelineSqlDao.getMetricId(categoryId, metricName);
+ }
+
+ transactional.record(sourceId, metricId, startDate.toDate(), endDate.toDate(), value);
+
+ return null;
+ }
+ });
+ }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/dao/RolledUpUsageDao.java b/usage/src/main/java/com/ning/billing/usage/dao/RolledUpUsageDao.java
new file mode 100644
index 0000000..a75a882
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/dao/RolledUpUsageDao.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2010-2012 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.usage.dao;
+
+import org.joda.time.DateTime;
+
+/**
+ * Dao to record already rolled-up usage data (rolled-up by the user).
+ * For raw tracking of the data, @see TimelineEventHandler.
+ */
+public interface RolledUpUsageDao {
+
+ public void record(final String sourceName, final String eventType, final String metricName, final DateTime startDate, final DateTime endDate, final long value);
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/dao/RolledUpUsageSqlDao.java b/usage/src/main/java/com/ning/billing/usage/dao/RolledUpUsageSqlDao.java
new file mode 100644
index 0000000..b495535
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/dao/RolledUpUsageSqlDao.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2010-2012 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.usage.dao;
+
+import java.util.Date;
+
+import org.skife.jdbi.v2.sqlobject.Bind;
+import org.skife.jdbi.v2.sqlobject.SqlUpdate;
+import org.skife.jdbi.v2.sqlobject.mixins.Transactional;
+import org.skife.jdbi.v2.sqlobject.mixins.Transmogrifier;
+import org.skife.jdbi.v2.sqlobject.stringtemplate.ExternalizedSqlViaStringTemplate3;
+
+@ExternalizedSqlViaStringTemplate3()
+public interface RolledUpUsageSqlDao extends Transactional<RolledUpUsageSqlDao>, Transmogrifier {
+
+ @SqlUpdate
+ public void record(@Bind("sourceId") final int sourceId, @Bind("metricId") final int metricId,
+ @Bind("startTime") final Date startTime, @Bind("endTime") final Date endTime,
+ @Bind("value") final long value);
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/glue/CachingDefaultTimelineDaoProvider.java b/usage/src/main/java/com/ning/billing/usage/glue/CachingDefaultTimelineDaoProvider.java
new file mode 100644
index 0000000..6e53a63
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/glue/CachingDefaultTimelineDaoProvider.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2010-2012 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.usage.glue;
+
+import javax.inject.Provider;
+
+import org.skife.jdbi.v2.DBI;
+
+import com.ning.billing.usage.timeline.persistent.CachingTimelineDao;
+import com.ning.billing.usage.timeline.persistent.DefaultTimelineDao;
+import com.ning.billing.usage.timeline.persistent.TimelineDao;
+
+import com.google.inject.Inject;
+
+public class CachingDefaultTimelineDaoProvider implements Provider<TimelineDao> {
+
+ private final DBI dbi;
+
+ @Inject
+ public CachingDefaultTimelineDaoProvider(final DBI dbi) {
+ this.dbi = dbi;
+ }
+
+ @Override
+ public TimelineDao get() {
+ final TimelineDao delegate = new DefaultTimelineDao(dbi);
+
+ return new CachingTimelineDao(delegate);
+ }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/glue/UsageModule.java b/usage/src/main/java/com/ning/billing/usage/glue/UsageModule.java
new file mode 100644
index 0000000..d6ec3e8
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/glue/UsageModule.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2010-2012 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.usage.glue;
+
+import java.io.IOException;
+
+import org.skife.config.ConfigSource;
+import org.skife.config.ConfigurationObjectFactory;
+import org.skife.config.SimplePropertyConfigSource;
+
+import com.ning.billing.config.UsageConfig;
+import com.ning.billing.usage.timeline.codec.DefaultSampleCoder;
+import com.ning.billing.usage.timeline.codec.SampleCoder;
+import com.ning.billing.usage.timeline.persistent.FileBackedBuffer;
+import com.ning.billing.usage.timeline.persistent.TimelineDao;
+import com.ning.billing.usage.timeline.times.DefaultTimelineCoder;
+import com.ning.billing.usage.timeline.times.TimelineCoder;
+
+import com.google.inject.AbstractModule;
+
+public class UsageModule extends AbstractModule {
+
+ private final ConfigSource configSource;
+
+ public UsageModule() {
+ this(new SimplePropertyConfigSource(System.getProperties()));
+ }
+
+ public UsageModule(final ConfigSource configSource) {
+ this.configSource = configSource;
+ }
+
+ protected UsageConfig installConfig() {
+ final UsageConfig config = new ConfigurationObjectFactory(configSource).build(UsageConfig.class);
+ bind(UsageConfig.class).toInstance(config);
+
+ return config;
+ }
+
+ protected void configureFileBackedBuffer(final UsageConfig config) {
+ // Persistent buffer for in-memory samples
+ try {
+ final boolean deleteFilesOnClose = config.getShutdownSaveMode().equals("save_all_timelines");
+ final FileBackedBuffer fileBackedBuffer = new FileBackedBuffer(config.getSpoolDir(), "TimelineEventHandler", deleteFilesOnClose, config.getSegmentsSize(), config.getMaxNbSegments());
+ bind(FileBackedBuffer.class).toInstance(fileBackedBuffer);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ protected void configureDao() {
+ bind(TimelineDao.class).toProvider(CachingDefaultTimelineDaoProvider.class).asEagerSingleton();
+ }
+
+ protected void configureTimelineObjects() {
+ bind(TimelineCoder.class).to(DefaultTimelineCoder.class).asEagerSingleton();
+ bind(SampleCoder.class).to(DefaultSampleCoder.class).asEagerSingleton();
+ }
+
+ @Override
+ protected void configure() {
+ final UsageConfig config = installConfig();
+
+ configureFileBackedBuffer(config);
+ configureDao();
+ configureTimelineObjects();
+
+ // TODO
+ //configureTimelineAggregator();
+ //configureBackgroundDBChunkWriter();
+ //configureReplayer();
+ }
+}
+
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/aggregator/TimelineAggregator.java b/usage/src/main/java/com/ning/billing/usage/timeline/aggregator/TimelineAggregator.java
new file mode 100644
index 0000000..ad0b3bc
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/aggregator/TimelineAggregator.java
@@ -0,0 +1,419 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.aggregator;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.joda.time.DateTime;
+import org.skife.jdbi.v2.Handle;
+import org.skife.jdbi.v2.IDBI;
+import org.skife.jdbi.v2.Query;
+import org.skife.jdbi.v2.ResultIterator;
+import org.skife.jdbi.v2.sqlobject.stringtemplate.StringTemplate3StatementLocator;
+import org.skife.jdbi.v2.tweak.HandleCallback;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.ning.billing.config.UsageConfig;
+import com.ning.billing.usage.timeline.chunks.TimelineChunk;
+import com.ning.billing.usage.timeline.chunks.TimelineChunkMapper;
+import com.ning.billing.usage.timeline.codec.SampleCoder;
+import com.ning.billing.usage.timeline.consumer.TimelineChunkConsumer;
+import com.ning.billing.usage.timeline.persistent.DefaultTimelineDao;
+import com.ning.billing.usage.timeline.times.TimelineCoder;
+
+import com.google.inject.Inject;
+
+/**
+ * This class runs a thread that periodically looks for unaggregated timelines.
+ * When it finds them, it combines them intelligently as if they were originally
+ * a single sequence of times.
+ */
+public class TimelineAggregator {
+
+ private static final Logger log = LoggerFactory.getLogger(TimelineAggregator.class);
+
+ private final IDBI dbi;
+ private final DefaultTimelineDao timelineDao;
+ private final TimelineCoder timelineCoder;
+ private final SampleCoder sampleCoder;
+ private final UsageConfig config;
+ private final TimelineAggregatorSqlDao aggregatorSqlDao;
+ private final TimelineChunkMapper timelineChunkMapper;
+ private final ScheduledExecutorService aggregatorThread = Executors.newSingleThreadScheduledExecutor();
+
+ private Map<String, AtomicLong> aggregatorCounters = new LinkedHashMap<String, AtomicLong>();
+
+ private final AtomicBoolean isAggregating = new AtomicBoolean(false);
+
+ private final AtomicLong aggregationRuns = new AtomicLong();
+ private final AtomicLong foundNothingRuns = new AtomicLong();
+ private final AtomicLong aggregatesCreated = makeCounter("aggsCreated");
+ private final AtomicLong timelineChunksConsidered = makeCounter("chunksConsidered");
+ private final AtomicLong timelineChunkBatchesProcessed = makeCounter("batchesProcessed");
+ private final AtomicLong timelineChunksCombined = makeCounter("chunksCombined");
+ private final AtomicLong timelineChunksQueuedForCreation = makeCounter("chunksQueued");
+ private final AtomicLong timelineChunksWritten = makeCounter("chunksWritten");
+ private final AtomicLong timelineChunksInvalidatedOrDeleted = makeCounter("chunksInvalidatedOrDeleted");
+ private final AtomicLong timelineChunksBytesCreated = makeCounter("bytesCreated");
+ private final AtomicLong msSpentAggregating = makeCounter("msSpentAggregating");
+ private final AtomicLong msSpentSleeping = makeCounter("msSpentSleeping");
+ private final AtomicLong msWritingDb = makeCounter("msWritingDb");
+
+ // These lists support batching of aggregated chunk writes and updates or deletes of the chunks aggregated
+ private final List<TimelineChunk> chunksToWrite = new ArrayList<TimelineChunk>();
+ private final List<Long> chunkIdsToInvalidateOrDelete = new ArrayList<Long>();
+
+ @Inject
+ public TimelineAggregator(final IDBI dbi, final DefaultTimelineDao timelineDao, final TimelineCoder timelineCoder, final SampleCoder sampleCoder, final UsageConfig config) {
+ this.dbi = dbi;
+ this.timelineDao = timelineDao;
+ this.timelineCoder = timelineCoder;
+ this.sampleCoder = sampleCoder;
+ this.config = config;
+ this.aggregatorSqlDao = dbi.onDemand(TimelineAggregatorSqlDao.class);
+ this.timelineChunkMapper = new TimelineChunkMapper();
+ }
+
+ private int aggregateTimelineCandidates(final List<TimelineChunk> timelineChunkCandidates, final int aggregationLevel, final int chunksToAggregate) {
+ final TimelineChunk firstCandidate = timelineChunkCandidates.get(0);
+ final int sourceId = firstCandidate.getSourceId();
+ final int metricId = firstCandidate.getMetricId();
+ log.debug("For sourceId {}, metricId {}, looking to aggregate {} candidates in {} chunks",
+ new Object[]{sourceId, metricId, timelineChunkCandidates.size(), chunksToAggregate});
+ int aggregatesCreated = 0;
+ int chunkIndex = 0;
+ while (timelineChunkCandidates.size() >= chunkIndex + chunksToAggregate) {
+ final List<TimelineChunk> chunkCandidates = timelineChunkCandidates.subList(chunkIndex, chunkIndex + chunksToAggregate);
+ chunkIndex += chunksToAggregate;
+ timelineChunksCombined.addAndGet(chunksToAggregate);
+ try {
+ aggregateHostSampleChunks(chunkCandidates, aggregationLevel);
+ } catch (IOException e) {
+ log.error(String.format("IOException aggregating {} chunks, sourceId %s, metricId %s, looking to aggregate %s candidates in %s chunks",
+ new Object[]{firstCandidate.getSourceId(), firstCandidate.getMetricId(), timelineChunkCandidates.size(), chunksToAggregate}), e);
+ }
+ aggregatesCreated++;
+ }
+
+ return aggregatesCreated;
+ }
+
+ /**
+ * The sequence of events is:
+ * <ul>
+ * <li>Build the aggregated TimelineChunk object, and save it, setting not_valid to true, and
+ * aggregation_level to 1. This means that it won't be noticed by any of the dashboard
+ * queries. The save operation returns the new timeline_times_id</li>
+ * <li>Then, in a single transaction, update the aggregated TimelineChunk object to have not_valid = 0,
+ * and also delete the TimelineChunk objects that were the basis of the aggregation, and flush
+ * any TimelineChunks that happen to be in the cache.</li>
+ * <p/>
+ *
+ * @param timelineChunks the TimelineChunks to be aggregated
+ */
+ private void aggregateHostSampleChunks(final List<TimelineChunk> timelineChunks, final int aggregationLevel) throws IOException {
+ final TimelineChunk firstTimesChunk = timelineChunks.get(0);
+ final TimelineChunk lastTimesChunk = timelineChunks.get(timelineChunks.size() - 1);
+ final int chunkCount = timelineChunks.size();
+ final int sourceId = firstTimesChunk.getSourceId();
+ final DateTime startTime = firstTimesChunk.getStartTime();
+ final DateTime endTime = lastTimesChunk.getEndTime();
+ final List<byte[]> timeParts = new ArrayList<byte[]>(chunkCount);
+ try {
+ final List<byte[]> sampleParts = new ArrayList<byte[]>(chunkCount);
+ final List<Long> timelineChunkIds = new ArrayList<Long>(chunkCount);
+ int sampleCount = 0;
+ for (final TimelineChunk timelineChunk : timelineChunks) {
+ timeParts.add(timelineChunk.getTimeBytesAndSampleBytes().getTimeBytes());
+ sampleParts.add(timelineChunk.getTimeBytesAndSampleBytes().getSampleBytes());
+ sampleCount += timelineChunk.getSampleCount();
+ timelineChunkIds.add(timelineChunk.getChunkId());
+ }
+ final byte[] combinedTimeBytes = timelineCoder.combineTimelines(timeParts, sampleCount);
+ final byte[] combinedSampleBytes = sampleCoder.combineSampleBytes(sampleParts);
+ final int timeBytesLength = combinedTimeBytes.length;
+ final int totalSize = 4 + timeBytesLength + combinedSampleBytes.length;
+ log.debug("For sourceId {}, aggregationLevel {}, aggregating {} timelines ({} bytes, {} samples): {}",
+ new Object[]{firstTimesChunk.getSourceId(), firstTimesChunk.getAggregationLevel(), timelineChunks.size(), totalSize, sampleCount});
+ timelineChunksBytesCreated.addAndGet(totalSize);
+ final int totalSampleCount = sampleCount;
+ final TimelineChunk chunk = new TimelineChunk(0, sourceId, firstTimesChunk.getMetricId(), startTime, endTime,
+ combinedTimeBytes, combinedSampleBytes, totalSampleCount, aggregationLevel + 1, false, false);
+ chunksToWrite.add(chunk);
+ chunkIdsToInvalidateOrDelete.addAll(timelineChunkIds);
+ timelineChunksQueuedForCreation.incrementAndGet();
+
+ if (chunkIdsToInvalidateOrDelete.size() >= config.getMaxChunkIdsToInvalidateOrDelete()) {
+ performWrites();
+ }
+ } catch (Exception e) {
+ log.error(String.format("Exception aggregating level %d, sourceId %d, metricId %d, startTime %s, endTime %s",
+ aggregationLevel, sourceId, firstTimesChunk.getMetricId(), startTime, endTime), e);
+ }
+ }
+
+ private void performWrites() {
+ // This is the atomic operation: bulk insert the new aggregated TimelineChunk objects, and delete
+ // or invalidate the ones that were aggregated. This should be very fast.
+ final long startWriteTime = System.currentTimeMillis();
+ aggregatorSqlDao.begin();
+ timelineDao.bulkInsertTimelineChunks(chunksToWrite);
+ if (config.getDeleteAggregatedChunks()) {
+ aggregatorSqlDao.deleteTimelineChunks(chunkIdsToInvalidateOrDelete);
+ } else {
+ aggregatorSqlDao.makeTimelineChunksInvalid(chunkIdsToInvalidateOrDelete);
+ }
+ aggregatorSqlDao.commit();
+ msWritingDb.addAndGet(System.currentTimeMillis() - startWriteTime);
+
+ timelineChunksWritten.addAndGet(chunksToWrite.size());
+ timelineChunksInvalidatedOrDeleted.addAndGet(chunkIdsToInvalidateOrDelete.size());
+ chunksToWrite.clear();
+ chunkIdsToInvalidateOrDelete.clear();
+ final long sleepMs = config.getAggregationSleepBetweenBatches().getMillis();
+ if (sleepMs > 0) {
+ final long timeBeforeSleep = System.currentTimeMillis();
+ try {
+ Thread.sleep(sleepMs);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ msSpentSleeping.addAndGet(System.currentTimeMillis() - timeBeforeSleep);
+ }
+ timelineChunkBatchesProcessed.incrementAndGet();
+ }
+
+ /**
+ * This method aggregates candidate timelines
+ */
+ public void getAndProcessTimelineAggregationCandidates() {
+ if (!isAggregating.compareAndSet(false, true)) {
+ log.info("Asked to aggregate, but we're already aggregating!");
+ return;
+ } else {
+ log.debug("Starting aggregating");
+ }
+
+ aggregationRuns.incrementAndGet();
+ final String[] chunkCountsToAggregate = config.getChunksToAggregate().split(",");
+ for (int aggregationLevel = 0; aggregationLevel < config.getMaxAggregationLevel(); aggregationLevel++) {
+ final long startingAggregatesCreated = aggregatesCreated.get();
+ final Map<String, Long> initialCounters = captureAggregatorCounters();
+ final int chunkCountIndex = aggregationLevel >= chunkCountsToAggregate.length ? chunkCountsToAggregate.length - 1 : aggregationLevel;
+ final int chunksToAggregate = Integer.parseInt(chunkCountsToAggregate[chunkCountIndex]);
+ streamingAggregateLevel(aggregationLevel, chunksToAggregate);
+ final Map<String, Long> counterDeltas = subtractFromAggregatorCounters(initialCounters);
+ final long netAggregatesCreated = aggregatesCreated.get() - startingAggregatesCreated;
+ if (netAggregatesCreated == 0) {
+ if (aggregationLevel == 0) {
+ foundNothingRuns.incrementAndGet();
+ }
+ log.debug("Created no new aggregates, so skipping higher-level aggregations");
+ break;
+ } else {
+ final StringBuilder builder = new StringBuilder();
+ builder
+ .append("For aggregation level ")
+ .append(aggregationLevel)
+ .append(", runs ")
+ .append(aggregationRuns.get())
+ .append(", foundNothingRuns ")
+ .append(foundNothingRuns.get());
+ for (final Map.Entry<String, Long> entry : counterDeltas.entrySet()) {
+ builder.append(", ").append(entry.getKey()).append(": ").append(entry.getValue());
+ }
+ log.info(builder.toString());
+ }
+ }
+
+ log.debug("Aggregation done");
+ isAggregating.set(false);
+ }
+
+ private void streamingAggregateLevel(final int aggregationLevel, final int chunksToAggregate) {
+ final List<TimelineChunk> sourceTimelineCandidates = new ArrayList<TimelineChunk>();
+ final TimelineChunkConsumer aggregationConsumer = new TimelineChunkConsumer() {
+
+ int lastSourceId = 0;
+ int lastMetricId = 0;
+
+ @Override
+ public void processTimelineChunk(final TimelineChunk candidate) {
+ timelineChunksConsidered.incrementAndGet();
+ final int sourceId = candidate.getSourceId();
+ final int metricId = candidate.getMetricId();
+ if (lastSourceId == 0) {
+ lastSourceId = sourceId;
+ lastMetricId = metricId;
+ }
+ if (lastSourceId != sourceId || lastMetricId != metricId) {
+ aggregatesCreated.addAndGet(aggregateTimelineCandidates(sourceTimelineCandidates, aggregationLevel, chunksToAggregate));
+ sourceTimelineCandidates.clear();
+ lastSourceId = sourceId;
+ lastMetricId = metricId;
+ }
+ sourceTimelineCandidates.add(candidate);
+ }
+ };
+ final long startTime = System.currentTimeMillis();
+ try {
+ dbi.withHandle(new HandleCallback<Void>() {
+
+ @Override
+ public Void withHandle(final Handle handle) throws Exception {
+ final Query<Map<String, Object>> query = handle.createQuery("getStreamingAggregationCandidates")
+ .setFetchSize(Integer.MIN_VALUE)
+ .bind("aggregationLevel", aggregationLevel);
+ query.setStatementLocator(new StringTemplate3StatementLocator(TimelineAggregatorSqlDao.class));
+ ResultIterator<TimelineChunk> iterator = null;
+ try {
+ iterator = query
+ .map(timelineChunkMapper)
+ .iterator();
+ while (iterator.hasNext()) {
+ aggregationConsumer.processTimelineChunk(iterator.next());
+ }
+ } catch (Exception e) {
+ log.error(String.format("Exception during aggregation of level %d", aggregationLevel), e);
+ } finally {
+ if (iterator != null) {
+ iterator.close();
+ }
+ }
+ return null;
+ }
+
+ });
+ if (sourceTimelineCandidates.size() >= chunksToAggregate) {
+ aggregatesCreated.addAndGet(aggregateTimelineCandidates(sourceTimelineCandidates, aggregationLevel, chunksToAggregate));
+ }
+ if (chunkIdsToInvalidateOrDelete.size() > 0) {
+ performWrites();
+ }
+ } finally {
+ msSpentAggregating.addAndGet(System.currentTimeMillis() - startTime);
+ }
+ }
+
+ private AtomicLong makeCounter(final String counterName) {
+ final AtomicLong counter = new AtomicLong();
+ aggregatorCounters.put(counterName, counter);
+ return counter;
+ }
+
+ private Map<String, Long> captureAggregatorCounters() {
+ final Map<String, Long> counterValues = new LinkedHashMap<String, Long>();
+ for (final Map.Entry<String, AtomicLong> entry : aggregatorCounters.entrySet()) {
+ counterValues.put(entry.getKey(), entry.getValue().get());
+ }
+ return counterValues;
+ }
+
+ private Map<String, Long> subtractFromAggregatorCounters(final Map<String, Long> initialCounters) {
+ final Map<String, Long> counterValues = new LinkedHashMap<String, Long>();
+ for (final Map.Entry<String, AtomicLong> entry : aggregatorCounters.entrySet()) {
+ final String key = entry.getKey();
+ counterValues.put(key, entry.getValue().get() - initialCounters.get(key));
+ }
+ return counterValues;
+ }
+
+ public void runAggregationThread() {
+ aggregatorThread.scheduleWithFixedDelay(new Runnable() {
+ @Override
+ public void run() {
+ getAndProcessTimelineAggregationCandidates();
+ }
+ },
+ config.getAggregationInterval().getMillis(),
+ config.getAggregationInterval().getMillis(),
+ TimeUnit.MILLISECONDS);
+ }
+
+ public void stopAggregationThread() {
+ aggregatorThread.shutdown();
+ }
+
+ public long getAggregationRuns() {
+ return aggregationRuns.get();
+ }
+
+ public long getFoundNothingRuns() {
+ return foundNothingRuns.get();
+ }
+
+ public long getTimelineChunksConsidered() {
+ return timelineChunksConsidered.get();
+ }
+
+ public long getTimelineChunkBatchesProcessed() {
+ return timelineChunkBatchesProcessed.get();
+ }
+
+ public long getTimelineChunksCombined() {
+ return timelineChunksCombined.get();
+ }
+
+ public long getTimelineChunksQueuedForCreation() {
+ return timelineChunksQueuedForCreation.get();
+ }
+
+ public long getTimelineChunksWritten() {
+ return timelineChunksWritten.get();
+ }
+
+ public long getTimelineChunksInvalidatedOrDeleted() {
+ return timelineChunksInvalidatedOrDeleted.get();
+ }
+
+ public long getTimelineChunksBytesCreated() {
+ return timelineChunksBytesCreated.get();
+ }
+
+ public long getMsSpentAggregating() {
+ return msSpentAggregating.get();
+ }
+
+ public long getMsSpentSleeping() {
+ return msSpentSleeping.get();
+ }
+
+ public long getMsWritingDb() {
+ return msWritingDb.get();
+ }
+
+ public void initiateAggregation() {
+ log.info("Starting user-initiated aggregation");
+ Executors.newSingleThreadExecutor().execute(new Runnable() {
+
+ @Override
+ public void run() {
+ getAndProcessTimelineAggregationCandidates();
+ }
+ });
+ }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/aggregator/TimelineAggregatorSqlDao.java b/usage/src/main/java/com/ning/billing/usage/timeline/aggregator/TimelineAggregatorSqlDao.java
new file mode 100644
index 0000000..6ba063d
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/aggregator/TimelineAggregatorSqlDao.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.aggregator;
+
+import java.util.List;
+
+import org.skife.jdbi.v2.sqlobject.Bind;
+import org.skife.jdbi.v2.sqlobject.SqlQuery;
+import org.skife.jdbi.v2.sqlobject.SqlUpdate;
+import org.skife.jdbi.v2.sqlobject.mixins.Transactional;
+import org.skife.jdbi.v2.sqlobject.stringtemplate.ExternalizedSqlViaStringTemplate3;
+import org.skife.jdbi.v2.unstable.BindIn;
+
+@ExternalizedSqlViaStringTemplate3()
+public interface TimelineAggregatorSqlDao extends Transactional<TimelineAggregatorSqlDao> {
+
+ @SqlQuery
+ int getLastInsertedId();
+
+ @SqlUpdate
+ void makeTimelineChunkValid(@Bind("chunkId") final long chunkId);
+
+ @SqlUpdate
+ void makeTimelineChunksInvalid(@BindIn("chunkIds") final List<Long> chunkIds);
+
+ @SqlUpdate
+ void deleteTimelineChunks(@BindIn("chunkIds") final List<Long> chunkIds);
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/BackgroundDBChunkWriter.java b/usage/src/main/java/com/ning/billing/usage/timeline/BackgroundDBChunkWriter.java
new file mode 100644
index 0000000..a7ab352
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/BackgroundDBChunkWriter.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTime;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.ning.billing.config.UsageConfig;
+import com.ning.billing.usage.timeline.chunks.TimelineChunk;
+import com.ning.billing.usage.timeline.persistent.TimelineDao;
+
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+/**
+ * This class runs a thread that batch-writes TimelineChunks to the db.
+ * This class is thread-safe, and only holds up threads that want to queue
+ * TimelineChunks for the time it takes to copy the ArrayList of PendingChunkMaps.
+ * <p/>
+ * The background writing thread is scheduled every few seconds, as controlled by
+ * config.getBackgroundWriteCheckInterval(). It writes the current inventory of
+ * chunks if there are at least config.getBackgroundWriteBatchSize()
+ * TimelineChunks to be written, or if the time since the last write exceeds
+ * config.getBackgroundWriteMaxDelay().
+ */
+@Singleton
+public class BackgroundDBChunkWriter {
+
+ private static final Logger log = LoggerFactory.getLogger(BackgroundDBChunkWriter.class);
+
+ private final TimelineDao timelineDAO;
+ private final UsageConfig config;
+ private final boolean performForegroundWrites;
+
+ private final AtomicInteger pendingChunkCount = new AtomicInteger();
+ private final AtomicBoolean shuttingDown = new AtomicBoolean();
+ private List<PendingChunkMap> pendingChunks = new ArrayList<PendingChunkMap>();
+ private DateTime lastWriteTime = new DateTime();
+ private AtomicBoolean doingWritesNow = new AtomicBoolean();
+ private final ScheduledExecutorService backgroundWriteThread = Executors.newSingleThreadScheduledExecutor();
+
+ private final AtomicLong maybePerformBackgroundWritesCount = new AtomicLong();
+ private final AtomicLong backgroundWritesCount = new AtomicLong();
+ private final AtomicLong pendingChunkMapsAdded = new AtomicLong();
+ private final AtomicLong pendingChunksAdded = new AtomicLong();
+ private final AtomicLong pendingChunkMapsWritten = new AtomicLong();
+ private final AtomicLong pendingChunksWritten = new AtomicLong();
+ private final AtomicLong pendingChunkMapsMarkedConsumed = new AtomicLong();
+ private final AtomicLong foregroundChunkMapsWritten = new AtomicLong();
+ private final AtomicLong foregroundChunksWritten = new AtomicLong();
+
+ @Inject
+ public BackgroundDBChunkWriter(final TimelineDao timelineDAO, final UsageConfig config) {
+ this(timelineDAO, config, config.getPerformForegroundWrites());
+ }
+
+ public BackgroundDBChunkWriter(final TimelineDao timelineDAO, @Nullable final UsageConfig config, final boolean performForegroundWrites) {
+ this.timelineDAO = timelineDAO;
+ this.config = config;
+ this.performForegroundWrites = performForegroundWrites;
+ }
+
+ public synchronized void addPendingChunkMap(final PendingChunkMap chunkMap) {
+ if (shuttingDown.get()) {
+ log.error("In addPendingChunkMap(), but finishBackgroundWritingAndExit is true!");
+ } else {
+ if (performForegroundWrites) {
+ foregroundChunkMapsWritten.incrementAndGet();
+ final List<TimelineChunk> chunksToWrite = new ArrayList<TimelineChunk>(chunkMap.getChunkMap().values());
+ foregroundChunksWritten.addAndGet(chunksToWrite.size());
+ timelineDAO.bulkInsertTimelineChunks(chunksToWrite);
+ chunkMap.getAccumulator().markPendingChunkMapConsumed(chunkMap.getPendingChunkMapId());
+ } else {
+ pendingChunkMapsAdded.incrementAndGet();
+ final int chunkCount = chunkMap.getChunkCount();
+ pendingChunksAdded.addAndGet(chunkCount);
+ pendingChunks.add(chunkMap);
+ pendingChunkCount.addAndGet(chunkCount);
+ }
+ }
+ }
+
+ private void performBackgroundWrites() {
+ backgroundWritesCount.incrementAndGet();
+ List<PendingChunkMap> chunkMapsToWrite = null;
+ synchronized (this) {
+ chunkMapsToWrite = pendingChunks;
+ pendingChunks = new ArrayList<PendingChunkMap>();
+ pendingChunkCount.set(0);
+ }
+ final List<TimelineChunk> chunks = new ArrayList<TimelineChunk>();
+ for (final PendingChunkMap map : chunkMapsToWrite) {
+ pendingChunkMapsWritten.incrementAndGet();
+ pendingChunksWritten.addAndGet(map.getChunkMap().size());
+ chunks.addAll(map.getChunkMap().values());
+ }
+ timelineDAO.bulkInsertTimelineChunks(chunks);
+ for (final PendingChunkMap map : chunkMapsToWrite) {
+ pendingChunkMapsMarkedConsumed.incrementAndGet();
+ map.getAccumulator().markPendingChunkMapConsumed(map.getPendingChunkMapId());
+ }
+ }
+
+ private void maybePerformBackgroundWrites() {
+ // If already running background writes, just return
+ maybePerformBackgroundWritesCount.incrementAndGet();
+ if (!doingWritesNow.compareAndSet(false, true)) {
+ return;
+ } else {
+ try {
+ if (shuttingDown.get()) {
+ performBackgroundWrites();
+ }
+ final int pendingCount = pendingChunkCount.get();
+ if (pendingCount > 0) {
+ if (pendingCount >= config.getBackgroundWriteBatchSize() ||
+ new DateTime().isBefore(lastWriteTime.plusMillis((int) config.getBackgroundWriteMaxDelay().getMillis()))) {
+ performBackgroundWrites();
+ lastWriteTime = new DateTime();
+ }
+ }
+ } finally {
+ doingWritesNow.set(false);
+ }
+ }
+ }
+
+ public synchronized boolean getShutdownFinished() {
+ return !doingWritesNow.get() && pendingChunks.size() == 0;
+ }
+
+ public void initiateShutdown() {
+ shuttingDown.set(true);
+ }
+
+ public void runBackgroundWriteThread() {
+ if (!performForegroundWrites) {
+ backgroundWriteThread.scheduleWithFixedDelay(new Runnable() {
+ @Override
+ public void run() {
+ maybePerformBackgroundWrites();
+ }
+ },
+ config.getBackgroundWriteCheckInterval().getMillis(),
+ config.getBackgroundWriteCheckInterval().getMillis(),
+ TimeUnit.MILLISECONDS);
+ }
+ }
+
+ public void stopBackgroundWriteThread() {
+ if (!performForegroundWrites) {
+ backgroundWriteThread.shutdown();
+ }
+ }
+
+ public long getMaybePerformBackgroundWritesCount() {
+ return maybePerformBackgroundWritesCount.get();
+ }
+
+ public long getBackgroundWritesCount() {
+ return backgroundWritesCount.get();
+ }
+
+ public long getPendingChunkMapsAdded() {
+ return pendingChunkMapsAdded.get();
+ }
+
+ public long getPendingChunksAdded() {
+ return pendingChunksAdded.get();
+ }
+
+ public long getPendingChunkMapsWritten() {
+ return pendingChunkMapsWritten.get();
+ }
+
+ public long getPendingChunksWritten() {
+ return pendingChunksWritten.get();
+ }
+
+ public long getPendingChunkMapsMarkedConsumed() {
+ return pendingChunkMapsMarkedConsumed.get();
+ }
+
+ public long getForegroundChunkMapsWritten() {
+ return foregroundChunkMapsWritten.get();
+ }
+
+ public long getForegroundChunksWritten() {
+ return foregroundChunksWritten.get();
+ }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/categories/CategoryAndMetrics.java b/usage/src/main/java/com/ning/billing/usage/timeline/categories/CategoryAndMetrics.java
new file mode 100644
index 0000000..c1ba8b5
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/categories/CategoryAndMetrics.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.categories;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class CategoryAndMetrics implements Comparable<CategoryAndMetrics> {
+
+ @JsonProperty
+ private final String eventCategory;
+ @JsonProperty
+ private final Set<String> metrics = new HashSet<String>();
+
+ public CategoryAndMetrics(final String eventCategory) {
+ this.eventCategory = eventCategory;
+ }
+
+ @JsonCreator
+ public CategoryAndMetrics(@JsonProperty("eventCategory") final String eventCategory, @JsonProperty("metrics") final List<String> metrics) {
+ this.eventCategory = eventCategory;
+ this.metrics.addAll(metrics);
+ }
+
+ public void addMetric(final String metric) {
+ metrics.add(metric);
+ }
+
+ public String getEventCategory() {
+ return eventCategory;
+ }
+
+ public Set<String> getMetrics() {
+ return metrics;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("CategoryAndMetrics");
+ sb.append("{eventCategory='").append(eventCategory).append('\'');
+ sb.append(", metrics=").append(metrics);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final CategoryAndMetrics that = (CategoryAndMetrics) o;
+
+ if (!eventCategory.equals(that.eventCategory)) {
+ return false;
+ }
+ if (!metrics.equals(that.metrics)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = eventCategory.hashCode();
+ result = 31 * result + metrics.hashCode();
+ return result;
+ }
+
+ @Override
+ public int compareTo(final CategoryAndMetrics o) {
+ final int categoryComparison = eventCategory.compareTo(o.getEventCategory());
+ if (categoryComparison != 0) {
+ return categoryComparison;
+ } else {
+ if (metrics.size() > o.getMetrics().size()) {
+ return 1;
+ } else if (metrics.size() < o.getMetrics().size()) {
+ return -1;
+ } else {
+ return 0;
+ }
+ }
+ }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/categories/CategoryAndMetricsForSources.java b/usage/src/main/java/com/ning/billing/usage/timeline/categories/CategoryAndMetricsForSources.java
new file mode 100644
index 0000000..3126db6
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/categories/CategoryAndMetricsForSources.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.categories;
+
+import java.util.Set;
+import java.util.TreeSet;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class CategoryAndMetricsForSources implements Comparable<CategoryAndMetricsForSources> {
+
+ @JsonProperty
+ private final CategoryAndMetrics categoryAndMetrics;
+ @JsonProperty
+ private final Set<String> sources;
+
+ public CategoryAndMetricsForSources(final String eventCategory) {
+ this(new CategoryAndMetrics(eventCategory), new TreeSet<String>());
+ }
+
+ @JsonCreator
+ public CategoryAndMetricsForSources(@JsonProperty("categoryAndMetrics") final CategoryAndMetrics categoryAndMetrics, @JsonProperty("sources") final Set<String> sources) {
+ this.categoryAndMetrics = categoryAndMetrics;
+ this.sources = sources;
+ }
+
+ public void add(final String metric, final String source) {
+ categoryAndMetrics.addMetric(metric);
+ sources.add(source);
+ }
+
+ public CategoryAndMetrics getCategoryAndMetrics() {
+ return categoryAndMetrics;
+ }
+
+ public Set<String> getSources() {
+ return sources;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("CategoryAndMetricsForSources");
+ sb.append("{categoryAndMetrics=").append(categoryAndMetrics);
+ sb.append(", sources=").append(sources);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final CategoryAndMetricsForSources that = (CategoryAndMetricsForSources) o;
+
+ if (categoryAndMetrics != null ? !categoryAndMetrics.equals(that.categoryAndMetrics) : that.categoryAndMetrics != null) {
+ return false;
+ }
+ if (sources != null ? !sources.equals(that.sources) : that.sources != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = categoryAndMetrics != null ? categoryAndMetrics.hashCode() : 0;
+ result = 31 * result + (sources != null ? sources.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public int compareTo(final CategoryAndMetricsForSources o) {
+ return categoryAndMetrics.compareTo(o.getCategoryAndMetrics());
+ }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/categories/CategoryIdAndMetric.java b/usage/src/main/java/com/ning/billing/usage/timeline/categories/CategoryIdAndMetric.java
new file mode 100644
index 0000000..30d58bb
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/categories/CategoryIdAndMetric.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.categories;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+public class CategoryIdAndMetric {
+
+ private final int eventCategoryId;
+ private final String metric;
+
+ public CategoryIdAndMetric(final int eventCategoryId, final String metric) {
+ this.eventCategoryId = eventCategoryId;
+ this.metric = metric;
+ }
+
+ public int getEventCategoryId() {
+ return eventCategoryId;
+ }
+
+ public String getMetric() {
+ return metric;
+ }
+
+ @Override
+ public boolean equals(final Object other) {
+ if (other == null || !(other instanceof CategoryIdAndMetric)) {
+ return false;
+ } else {
+ final CategoryIdAndMetric typedOther = (CategoryIdAndMetric) other;
+ return eventCategoryId == typedOther.getEventCategoryId() && metric.equals(typedOther.getMetric());
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return eventCategoryId ^ metric.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return String.format("EventCategoryIdAndMetric(eventCategoryId %d, metric %s)", eventCategoryId, metric);
+ }
+
+ public static List<String> extractMetrics(final Collection<CategoryIdAndMetric> categoryIdsAndMetrics) {
+ final List<String> metrics = new ArrayList<String>();
+ for (final CategoryIdAndMetric categoryIdAndMetric : categoryIdsAndMetrics) {
+ metrics.add(categoryIdAndMetric.getMetric());
+ }
+ return metrics;
+ }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/categories/CategoryIdAndMetricBinder.java b/usage/src/main/java/com/ning/billing/usage/timeline/categories/CategoryIdAndMetricBinder.java
new file mode 100644
index 0000000..1c75f83
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/categories/CategoryIdAndMetricBinder.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.categories;
+
+import java.lang.annotation.Annotation;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.skife.jdbi.v2.SQLStatement;
+import org.skife.jdbi.v2.sqlobject.Binder;
+import org.skife.jdbi.v2.sqlobject.BinderFactory;
+import org.skife.jdbi.v2.sqlobject.BindingAnnotation;
+
+import com.ning.billing.usage.timeline.categories.CategoryIdAndMetricBinder.CategoryIdAndMetricBinderFactory;
+
+@BindingAnnotation(CategoryIdAndMetricBinderFactory.class)
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.PARAMETER})
+public @interface CategoryIdAndMetricBinder {
+
+ public static class CategoryIdAndMetricBinderFactory implements BinderFactory {
+
+ public Binder build(final Annotation annotation) {
+ return new Binder<CategoryIdAndMetricBinder, CategoryIdAndMetric>() {
+ public void bind(final SQLStatement query, final CategoryIdAndMetricBinder binder, final CategoryIdAndMetric categoryAndKind) {
+ query.bind("eventCategoryId", categoryAndKind.getEventCategoryId())
+ .bind("metric", categoryAndKind.getMetric());
+ }
+ };
+ }
+ }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/categories/CategoryIdAndMetricMapper.java b/usage/src/main/java/com/ning/billing/usage/timeline/categories/CategoryIdAndMetricMapper.java
new file mode 100644
index 0000000..5610f7d
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/categories/CategoryIdAndMetricMapper.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.categories;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+import org.skife.jdbi.v2.StatementContext;
+import org.skife.jdbi.v2.tweak.ResultSetMapper;
+
+public class CategoryIdAndMetricMapper implements ResultSetMapper<CategoryIdAndMetric> {
+
+ @Override
+ public CategoryIdAndMetric map(final int index, final ResultSet rs, final StatementContext ctx) throws SQLException {
+ final int eventCategoryId = rs.getInt("event_category_id");
+ final String metric = rs.getString("sample_kind");
+ return new CategoryIdAndMetric(eventCategoryId, metric);
+ }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/chunks/TimeBytesAndSampleBytes.java b/usage/src/main/java/com/ning/billing/usage/timeline/chunks/TimeBytesAndSampleBytes.java
new file mode 100644
index 0000000..6d062b9
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/chunks/TimeBytesAndSampleBytes.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.chunks;
+
+import java.util.Arrays;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonView;
+
+/**
+ * POJO containing a series of bytes and associated time points
+ */
+public class TimeBytesAndSampleBytes {
+
+ @JsonProperty
+ @JsonView(TimelineChunksViews.Compact.class)
+ private final byte[] timeBytes;
+ @JsonProperty
+ @JsonView(TimelineChunksViews.Compact.class)
+ private final byte[] sampleBytes;
+
+ public TimeBytesAndSampleBytes(final byte[] timeBytes, final byte[] sampleBytes) {
+ this.timeBytes = timeBytes;
+ this.sampleBytes = sampleBytes;
+ }
+
+ public byte[] getTimeBytes() {
+ return timeBytes;
+ }
+
+ public byte[] getSampleBytes() {
+ return sampleBytes;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("TimeBytesAndSampleBytes");
+ sb.append("{timeBytes=").append(timeBytes == null ? "null" : "");
+ for (int i = 0; timeBytes != null && i < timeBytes.length; ++i) {
+ sb.append(i == 0 ? "" : ", ").append(timeBytes[i]);
+ }
+ sb.append(", sampleBytes=").append(sampleBytes == null ? "null" : "");
+ for (int i = 0; sampleBytes != null && i < sampleBytes.length; ++i) {
+ sb.append(i == 0 ? "" : ", ").append(sampleBytes[i]);
+ }
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final TimeBytesAndSampleBytes that = (TimeBytesAndSampleBytes) o;
+
+ if (!Arrays.equals(sampleBytes, that.sampleBytes)) {
+ return false;
+ }
+ if (!Arrays.equals(timeBytes, that.timeBytes)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = timeBytes != null ? Arrays.hashCode(timeBytes) : 0;
+ result = 31 * result + (sampleBytes != null ? Arrays.hashCode(sampleBytes) : 0);
+ return result;
+ }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/chunks/TimelineChunk.java b/usage/src/main/java/com/ning/billing/usage/timeline/chunks/TimelineChunk.java
new file mode 100644
index 0000000..835022e
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/chunks/TimelineChunk.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.chunks;
+
+import org.joda.time.DateTime;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonView;
+
+/**
+ * Instances of this class represent timeline sequences read from the database
+ * for a single source and single metric. The samples are held in a byte
+ * array.
+ */
+public class TimelineChunk {
+
+ @JsonProperty
+ @JsonView(TimelineChunksViews.Base.class)
+ private final long chunkId;
+ @JsonProperty
+ @JsonView(TimelineChunksViews.Base.class)
+ private final int sourceId;
+ @JsonProperty
+ @JsonView(TimelineChunksViews.Base.class)
+ private final int metricId;
+ @JsonProperty
+ @JsonView(TimelineChunksViews.Compact.class)
+ private final DateTime startTime;
+ @JsonProperty
+ @JsonView(TimelineChunksViews.Compact.class)
+ private final DateTime endTime;
+ @JsonProperty
+ @JsonView(TimelineChunksViews.Compact.class)
+ private final TimeBytesAndSampleBytes timeBytesAndSampleBytes;
+ @JsonProperty
+ @JsonView(TimelineChunksViews.Compact.class)
+ private final int sampleCount;
+ @JsonProperty
+ @JsonView(TimelineChunksViews.Compact.class)
+ private final int aggregationLevel;
+ @JsonProperty
+ @JsonView(TimelineChunksViews.Compact.class)
+ private final boolean notValid;
+ @JsonProperty
+ @JsonView(TimelineChunksViews.Compact.class)
+ private final boolean dontAggregate;
+
+ public TimelineChunk(final long chunkId, final int sourceId, final int metricId, final DateTime startTime, final DateTime endTime,
+ final byte[] times, final byte[] samples, final int sampleCount) {
+ this(chunkId, sourceId, metricId, startTime, endTime, times, samples, sampleCount, 0, false, false);
+ }
+
+ public TimelineChunk(final long chunkId, final int sourceId, final int metricId, final DateTime startTime, final DateTime endTime,
+ final byte[] times, final byte[] samples, final int sampleCount, final int aggregationLevel, final boolean notValid, final boolean dontAggregate) {
+ this(chunkId, sourceId, metricId, startTime, endTime, new TimeBytesAndSampleBytes(times, samples), sampleCount,
+ aggregationLevel, notValid, dontAggregate);
+ }
+
+ public TimelineChunk(final long chunkId, final int sourceId, final int metricId, final DateTime startTime, final DateTime endTime,
+ final TimeBytesAndSampleBytes timeBytesAndSampleBytes, final int sampleCount, final int aggregationLevel, final boolean notValid, final boolean dontAggregate) {
+ this.chunkId = chunkId;
+ this.sourceId = sourceId;
+ this.metricId = metricId;
+ this.startTime = startTime;
+ this.endTime = endTime;
+ this.timeBytesAndSampleBytes = timeBytesAndSampleBytes;
+ this.sampleCount = sampleCount;
+ this.aggregationLevel = aggregationLevel;
+ this.notValid = notValid;
+ this.dontAggregate = dontAggregate;
+ }
+
+ public TimelineChunk(final long chunkId, final TimelineChunk other) {
+ this(chunkId, other.getSourceId(), other.getMetricId(), other.getStartTime(), other.getEndTime(), other.getTimeBytesAndSampleBytes(),
+ other.getSampleCount(), other.getAggregationLevel(), other.getNotValid(), other.getDontAggregate());
+ }
+
+ public long getChunkId() {
+ return chunkId;
+ }
+
+ public int getSourceId() {
+ return sourceId;
+ }
+
+ public int getMetricId() {
+ return metricId;
+ }
+
+ public DateTime getStartTime() {
+ return startTime;
+ }
+
+ public DateTime getEndTime() {
+ return endTime;
+ }
+
+ public TimeBytesAndSampleBytes getTimeBytesAndSampleBytes() {
+ return timeBytesAndSampleBytes;
+ }
+
+ public int getSampleCount() {
+ return sampleCount;
+ }
+
+ public int getAggregationLevel() {
+ return aggregationLevel;
+ }
+
+ public boolean getNotValid() {
+ return notValid;
+ }
+
+ public boolean getDontAggregate() {
+ return dontAggregate;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("TimelineChunk");
+ sb.append("{chunkId=").append(chunkId);
+ sb.append(", sourceId=").append(sourceId);
+ sb.append(", metricId=").append(metricId);
+ sb.append(", startTime=").append(startTime);
+ sb.append(", endTime=").append(endTime);
+ sb.append(", timeBytesAndSampleBytes=").append(timeBytesAndSampleBytes);
+ sb.append(", sampleCount=").append(sampleCount);
+ sb.append(", aggregationLevel=").append(aggregationLevel);
+ sb.append(", notValid=").append(notValid);
+ sb.append(", dontAggregate=").append(dontAggregate);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final TimelineChunk that = (TimelineChunk) o;
+
+ if (aggregationLevel != that.aggregationLevel) {
+ return false;
+ }
+ if (chunkId != that.chunkId) {
+ return false;
+ }
+ if (dontAggregate != that.dontAggregate) {
+ return false;
+ }
+ if (metricId != that.metricId) {
+ return false;
+ }
+ if (notValid != that.notValid) {
+ return false;
+ }
+ if (sampleCount != that.sampleCount) {
+ return false;
+ }
+ if (sourceId != that.sourceId) {
+ return false;
+ }
+ if (endTime != null ? !endTime.equals(that.endTime) : that.endTime != null) {
+ return false;
+ }
+ if (startTime != null ? !startTime.equals(that.startTime) : that.startTime != null) {
+ return false;
+ }
+ if (timeBytesAndSampleBytes != null ? !timeBytesAndSampleBytes.equals(that.timeBytesAndSampleBytes) : that.timeBytesAndSampleBytes != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = (int) (chunkId ^ (chunkId >>> 32));
+ result = 31 * result + sourceId;
+ result = 31 * result + metricId;
+ result = 31 * result + (startTime != null ? startTime.hashCode() : 0);
+ result = 31 * result + (endTime != null ? endTime.hashCode() : 0);
+ result = 31 * result + (timeBytesAndSampleBytes != null ? timeBytesAndSampleBytes.hashCode() : 0);
+ result = 31 * result + sampleCount;
+ result = 31 * result + aggregationLevel;
+ result = 31 * result + (notValid ? 1 : 0);
+ result = 31 * result + (dontAggregate ? 1 : 0);
+ return result;
+ }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/chunks/TimelineChunkBinder.java b/usage/src/main/java/com/ning/billing/usage/timeline/chunks/TimelineChunkBinder.java
new file mode 100644
index 0000000..60d7d25
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/chunks/TimelineChunkBinder.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.chunks;
+
+import java.lang.annotation.Annotation;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.sql.Types;
+
+import org.skife.jdbi.v2.SQLStatement;
+import org.skife.jdbi.v2.sqlobject.Binder;
+import org.skife.jdbi.v2.sqlobject.BinderFactory;
+import org.skife.jdbi.v2.sqlobject.BindingAnnotation;
+
+import com.ning.billing.usage.timeline.chunks.TimelineChunkBinder.TimelineChunkBinderFactory;
+import com.ning.billing.usage.timeline.codec.TimesAndSamplesCoder;
+import com.ning.billing.usage.timeline.util.DateTimeUtils;
+
+/**
+ * jdbi binder for TimelineChunk
+ */
+@BindingAnnotation(TimelineChunkBinderFactory.class)
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.PARAMETER})
+public @interface TimelineChunkBinder {
+
+ public static class TimelineChunkBinderFactory implements BinderFactory {
+
+ // Maximum size in bytes for a series of timesAndSamples to stay "in row" (stored as VARBINARY).
+ // Past this threshold, data is stored as a BLOB.
+ private static final int MAX_IN_ROW_BLOB_SIZE = 400;
+
+ public Binder build(final Annotation annotation) {
+ return new Binder<TimelineChunkBinder, TimelineChunk>() {
+ public void bind(final SQLStatement query, final TimelineChunkBinder binder, final TimelineChunk timelineChunk) {
+ query.bind("sourceId", timelineChunk.getSourceId())
+ .bind("metricId", timelineChunk.getMetricId())
+ .bind("sampleCount", timelineChunk.getSampleCount())
+ .bind("startTime", DateTimeUtils.unixSeconds(timelineChunk.getStartTime()))
+ .bind("endTime", DateTimeUtils.unixSeconds(timelineChunk.getEndTime()))
+ .bind("aggregationLevel", timelineChunk.getAggregationLevel())
+ .bind("notValid", timelineChunk.getNotValid() ? 1 : 0)
+ .bind("dontAggregate", timelineChunk.getDontAggregate() ? 1 : 0);
+
+ final byte[] times = timelineChunk.getTimeBytesAndSampleBytes().getTimeBytes();
+ final byte[] samples = timelineChunk.getTimeBytesAndSampleBytes().getSampleBytes();
+ final byte[] timesAndSamples = TimesAndSamplesCoder.combineTimesAndSamples(times, samples);
+ if (timelineChunk.getChunkId() == 0) {
+ query.bindNull("chunkId", Types.BIGINT);
+ } else {
+ query.bind("chunkId", timelineChunk.getChunkId());
+ }
+
+ if (timesAndSamples.length > MAX_IN_ROW_BLOB_SIZE) {
+ query.bindNull("inRowSamples", Types.VARBINARY)
+ .bind("blobSamples", timesAndSamples);
+ } else {
+ query.bind("inRowSamples", timesAndSamples)
+ .bindNull("blobSamples", Types.BLOB);
+ }
+ }
+ };
+ }
+ }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/chunks/TimelineChunkMapper.java b/usage/src/main/java/com/ning/billing/usage/timeline/chunks/TimelineChunkMapper.java
new file mode 100644
index 0000000..346b8be
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/chunks/TimelineChunkMapper.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.chunks;
+
+import java.sql.Blob;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+import org.joda.time.DateTime;
+import org.skife.jdbi.v2.StatementContext;
+import org.skife.jdbi.v2.tweak.ResultSetMapper;
+
+import com.ning.billing.usage.timeline.codec.TimesAndSamplesCoder;
+import com.ning.billing.usage.timeline.util.DateTimeUtils;
+
+/**
+ * jdbi mapper for TimelineChunk
+ */
+public class TimelineChunkMapper implements ResultSetMapper<TimelineChunk> {
+
+ @Override
+ public TimelineChunk map(final int index, final ResultSet rs, final StatementContext ctx) throws SQLException {
+ final int chunkId = rs.getInt("chunk_id");
+ final int sourceId = rs.getInt("source_id");
+ final int metricId = rs.getInt("sample_kind_id");
+ final int sampleCount = rs.getInt("sample_count");
+ final DateTime startTime = DateTimeUtils.dateTimeFromUnixSeconds(rs.getInt("start_time"));
+ final DateTime endTime = DateTimeUtils.dateTimeFromUnixSeconds(rs.getInt("end_time"));
+ final int aggregationLevel = rs.getInt("aggregation_level");
+ final boolean notValid = rs.getInt("not_valid") == 0 ? false : true;
+ final boolean dontAggregate = rs.getInt("dont_aggregate") == 0 ? false : true;
+
+ byte[] samplesAndTimes = rs.getBytes("in_row_samples");
+ if (rs.wasNull()) {
+ final Blob blobSamples = rs.getBlob("blob_samples");
+ if (rs.wasNull()) {
+ samplesAndTimes = new byte[4];
+ } else {
+ samplesAndTimes = blobSamples.getBytes(1, (int) blobSamples.length());
+ }
+ }
+
+ final TimeBytesAndSampleBytes bytesPair = TimesAndSamplesCoder.getTimesBytesAndSampleBytes(samplesAndTimes);
+ return new TimelineChunk(chunkId, sourceId, metricId, startTime, endTime, bytesPair, sampleCount,
+ aggregationLevel, notValid, dontAggregate);
+ }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/chunks/TimelineChunksViews.java b/usage/src/main/java/com/ning/billing/usage/timeline/chunks/TimelineChunksViews.java
new file mode 100644
index 0000000..782451e
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/chunks/TimelineChunksViews.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.chunks;
+
+public class TimelineChunksViews {
+
+ public static class Base {
+
+ }
+
+ public static class Compact extends Base {
+
+ }
+
+ public static class Loose extends Base {
+
+ }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/codec/DefaultSampleCoder.java b/usage/src/main/java/com/ning/billing/usage/timeline/codec/DefaultSampleCoder.java
new file mode 100644
index 0000000..43a0763
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/codec/DefaultSampleCoder.java
@@ -0,0 +1,526 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.codec;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.ning.billing.usage.timeline.chunks.TimelineChunk;
+import com.ning.billing.usage.timeline.samples.HalfFloat;
+import com.ning.billing.usage.timeline.samples.RepeatSample;
+import com.ning.billing.usage.timeline.samples.SampleBase;
+import com.ning.billing.usage.timeline.samples.SampleOpcode;
+import com.ning.billing.usage.timeline.samples.ScalarSample;
+import com.ning.billing.usage.timeline.times.DefaultTimelineCursor;
+import com.ning.billing.usage.timeline.times.TimelineCursor;
+
+/**
+ * Instances of this class encode sample streams. In addition, this class
+ * contains a collection of static methods providing lower-level encoding plumbing
+ */
+@SuppressWarnings("unchecked")
+public class DefaultSampleCoder implements SampleCoder {
+
+ private static final Logger log = LoggerFactory.getLogger(DefaultSampleCoder.class);
+ private static final BigInteger BIGINTEGER_ZERO_VALUE = new BigInteger("0");
+ private static final ScalarSample<Void> DOUBLE_ZERO_SAMPLE = new ScalarSample<Void>(SampleOpcode.DOUBLE_ZERO, null);
+ private static final ScalarSample<Void> INT_ZERO_SAMPLE = new ScalarSample<Void>(SampleOpcode.INT_ZERO, null);
+
+ // TODO: Figure out if 1/200 is an acceptable level of inaccuracy
+ // For the HalfFloat, which has a 10-bit mantissa, this means that it could differ
+ // in the last 3 bits of the mantissa and still be treated as matching.
+ public static final double MAX_FRACTION_ERROR = 1.0 / 200.0;
+ public static final double HALF_MAX_FRACTION_ERROR = MAX_FRACTION_ERROR / 2.0;
+
+ private static final double MIN_BYTE_DOUBLE_VALUE = ((double) Byte.MIN_VALUE) * (1.0 + HALF_MAX_FRACTION_ERROR);
+ private static final double MAX_BYTE_DOUBLE_VALUE = ((double) Byte.MAX_VALUE) * (1.0 + HALF_MAX_FRACTION_ERROR);
+
+ private static final double MIN_SHORT_DOUBLE_VALUE = ((double) Short.MIN_VALUE) * (1.0 + HALF_MAX_FRACTION_ERROR);
+ private static final double MAX_SHORT_DOUBLE_VALUE = ((double) Short.MAX_VALUE) * (1.0 + HALF_MAX_FRACTION_ERROR);
+
+ @SuppressWarnings("unused")
+ private static final double INVERSE_MAX_FRACTION_ERROR = 1.0 / MAX_FRACTION_ERROR;
+
+ @Override
+ public byte[] compressSamples(final List<ScalarSample> samples) {
+ final SampleAccumulator accumulator = new SampleAccumulator(this);
+ accumulator.addSampleList(samples);
+ return accumulator.getEncodedSamples().getEncodedBytes();
+ }
+
+ @Override
+ public List<ScalarSample> decompressSamples(final byte[] sampleBytes) throws IOException {
+ final List<ScalarSample> returnedSamples = new ArrayList<ScalarSample>();
+ final ByteArrayInputStream byteStream = new ByteArrayInputStream(sampleBytes);
+ final DataInputStream inputStream = new DataInputStream(byteStream);
+ while (true) {
+ final int opcodeByte;
+ opcodeByte = inputStream.read();
+ if (opcodeByte == -1) {
+ break; // At "eof"
+ }
+ final SampleOpcode opcode = SampleOpcode.getOpcodeFromIndex(opcodeByte);
+ switch (opcode) {
+ case REPEAT_BYTE:
+ case REPEAT_SHORT:
+ final int repeatCount = opcode == SampleOpcode.REPEAT_BYTE ? inputStream.readUnsignedByte() : inputStream.readUnsignedShort();
+ final SampleOpcode repeatedOpcode = SampleOpcode.getOpcodeFromIndex(inputStream.read());
+ final Object value = decodeScalarValue(inputStream, repeatedOpcode);
+ for (int i = 0; i < repeatCount; i++) {
+ returnedSamples.add(new ScalarSample(repeatedOpcode, value));
+ }
+ break;
+ default:
+ returnedSamples.add(new ScalarSample(opcode, decodeScalarValue(inputStream, opcode)));
+ break;
+ }
+ }
+ return returnedSamples;
+ }
+
+
+ /**
+ * This method writes the binary encoding of the sample to the outputStream. This encoding
+ * is the form saved in the db and scanned when read from the db.
+ *
+ * @param outputStream the stream to which bytes should be written
+ * @param sample the sample to be written
+ */
+ @Override
+ public void encodeSample(final DataOutputStream outputStream, final SampleBase sample) {
+ final SampleOpcode opcode = sample.getOpcode();
+ try {
+ // First put out the opcode value
+ switch (opcode) {
+ case REPEAT_BYTE:
+ case REPEAT_SHORT:
+ final RepeatSample r = (RepeatSample) sample;
+ final ScalarSample repeatee = r.getSampleRepeated();
+ outputStream.write(opcode.getOpcodeIndex());
+ if (opcode == SampleOpcode.REPEAT_BYTE) {
+ outputStream.write(r.getRepeatCount());
+ } else {
+ outputStream.writeShort(r.getRepeatCount());
+ }
+ encodeScalarValue(outputStream, repeatee.getOpcode(), repeatee.getSampleValue());
+ case NULL:
+ break;
+ default:
+ if (sample instanceof ScalarSample) {
+ encodeScalarValue(outputStream, opcode, ((ScalarSample) sample).getSampleValue());
+ } else {
+ log.error("In encodeSample, opcode {} is not ScalarSample; instead {}", opcode.name(), sample.getClass().getName());
+ }
+ }
+ } catch (IOException e) {
+ log.error(String.format("In encodeSample, IOException encoding opcode %s and value %s", opcode.name(), String.valueOf(sample)), e);
+ }
+ }
+
+ /**
+ * Output the scalar value into the output stream
+ *
+ * @param outputStream the stream to which bytes should be written
+ * @param value the sample value, interpreted according to the opcode
+ */
+ @Override
+ public void encodeScalarValue(final DataOutputStream outputStream, final SampleOpcode opcode, final Object value) {
+ try {
+ outputStream.write(opcode.getOpcodeIndex());
+ switch (opcode) {
+ case NULL:
+ case DOUBLE_ZERO:
+ case INT_ZERO:
+ break;
+ case BYTE:
+ case BYTE_FOR_DOUBLE:
+ outputStream.writeByte((Byte) value);
+ break;
+ case SHORT:
+ case SHORT_FOR_DOUBLE:
+ case HALF_FLOAT_FOR_DOUBLE:
+ outputStream.writeShort((Short) value);
+ break;
+ case INT:
+ outputStream.writeInt((Integer) value);
+ break;
+ case LONG:
+ outputStream.writeLong((Long) value);
+ break;
+ case FLOAT:
+ case FLOAT_FOR_DOUBLE:
+ outputStream.writeFloat((Float) value);
+ break;
+ case DOUBLE:
+ outputStream.writeDouble((Double) value);
+ break;
+ case STRING:
+ final String s = (String) value;
+ final byte[] bytes = s.getBytes("UTF-8");
+ outputStream.writeShort(s.length());
+ outputStream.write(bytes, 0, bytes.length);
+ break;
+ case BIGINT:
+ final String bs = value.toString();
+ // Only support bigints whose length can be encoded as a short
+ if (bs.length() > Short.MAX_VALUE) {
+ throw new IllegalStateException(String.format("In DefaultSampleCoder.encodeScalarValue(), the string length of the BigInteger is %d; too large to be represented in a Short", bs.length()));
+ }
+ final byte[] bbytes = bs.getBytes("UTF-8");
+ outputStream.writeShort(bs.length());
+ outputStream.write(bbytes, 0, bbytes.length);
+ break;
+ default:
+ final String err = String.format("In encodeScalarSample, opcode %s is unrecognized", opcode.name());
+ log.error(err);
+ throw new IllegalArgumentException(err);
+ }
+ } catch (IOException e) {
+ log.error(String.format("In encodeScalarValue, IOException encoding opcode %s and value %s", opcode.name(), String.valueOf(value)), e);
+ }
+ }
+
+ /**
+ * This routine returns a ScalarSample that may have a smaller representation than the
+ * ScalarSample argument. In particular, if tries hard to choose the most compact
+ * representation of double-precision values.
+ *
+ * @param sample A ScalarSample to be compressed
+ * @return Either the same ScalarSample is that input, for for some cases of opcode DOUBLE,
+ * a more compact ScalarSample which when processed returns a double value.
+ */
+ @Override
+ public ScalarSample compressSample(final ScalarSample sample) {
+ switch (sample.getOpcode()) {
+ case INT:
+ final int intValue = (Integer) sample.getSampleValue();
+ if (intValue == 0) {
+ return INT_ZERO_SAMPLE;
+ } else if (intValue >= Byte.MIN_VALUE && intValue <= Byte.MAX_VALUE) {
+ return new ScalarSample(SampleOpcode.BYTE, (byte) intValue);
+ } else if (intValue >= Short.MIN_VALUE && intValue <= Short.MAX_VALUE) {
+ return new ScalarSample(SampleOpcode.SHORT, (short) intValue);
+ } else {
+ return sample;
+ }
+ case LONG:
+ final long longValue = (Long) sample.getSampleValue();
+ if (longValue == 0) {
+ return INT_ZERO_SAMPLE;
+ } else if (longValue >= Byte.MIN_VALUE && longValue <= Byte.MAX_VALUE) {
+ return new ScalarSample(SampleOpcode.BYTE, (byte) longValue);
+ } else if (longValue >= Short.MIN_VALUE && longValue <= Short.MAX_VALUE) {
+ return new ScalarSample(SampleOpcode.SHORT, (short) longValue);
+ } else if (longValue >= Integer.MIN_VALUE && longValue <= Integer.MAX_VALUE) {
+ return new ScalarSample(SampleOpcode.INT, (int) longValue);
+ } else {
+ return sample;
+ }
+ case BIGINT:
+ final BigInteger bigValue = (BigInteger) sample.getSampleValue();
+ if (bigValue.compareTo(BIGINTEGER_ZERO_VALUE) == 0) {
+ return INT_ZERO_SAMPLE;
+ }
+ final int digits = 1 + bigValue.bitCount();
+ if (digits <= 8) {
+ return new ScalarSample(SampleOpcode.BYTE, (byte) bigValue.intValue());
+ } else if (digits <= 16) {
+ return new ScalarSample(SampleOpcode.SHORT, (short) bigValue.intValue());
+ } else if (digits <= 32) {
+ return new ScalarSample(SampleOpcode.INT, bigValue.intValue());
+ } else if (digits <= 64) {
+ return new ScalarSample(SampleOpcode.LONG, bigValue.longValue());
+ } else {
+ return sample;
+ }
+ case FLOAT:
+ return encodeFloatOrDoubleSample(sample, (double) ((Float) sample.getSampleValue()));
+ case DOUBLE:
+ return encodeFloatOrDoubleSample(sample, (Double) sample.getSampleValue());
+ default:
+ return sample;
+ }
+ }
+
+ private ScalarSample encodeFloatOrDoubleSample(final ScalarSample sample, final double value) {
+ // We prefer representations in the following order: byte, HalfFloat, short, float and int
+ // The criterion for using each representation is the fractional error
+ if (value == 0.0) {
+ return DOUBLE_ZERO_SAMPLE;
+ }
+ final boolean integral = value >= MIN_SHORT_DOUBLE_VALUE && value <= MAX_SHORT_DOUBLE_VALUE && (Math.abs((value - (double) ((int) value)) / value) <= MAX_FRACTION_ERROR);
+ if (integral && value >= MIN_BYTE_DOUBLE_VALUE && value <= MAX_BYTE_DOUBLE_VALUE) {
+ return new ScalarSample<Byte>(SampleOpcode.BYTE_FOR_DOUBLE, (byte) value);
+ } else if (integral && value >= MIN_SHORT_DOUBLE_VALUE && value <= MAX_SHORT_DOUBLE_VALUE) {
+ return new ScalarSample<Short>(SampleOpcode.SHORT_FOR_DOUBLE, (short) value);
+ } else {
+ final int halfFloatValue = HalfFloat.fromFloat((float) value);
+ if ((Math.abs(value - HalfFloat.toFloat(halfFloatValue)) / value) <= MAX_FRACTION_ERROR) {
+ return new ScalarSample<Short>(SampleOpcode.HALF_FLOAT_FOR_DOUBLE, (short) halfFloatValue);
+ } else if (value >= Float.MIN_VALUE && value <= Float.MAX_VALUE) {
+ return new ScalarSample<Float>(SampleOpcode.FLOAT_FOR_DOUBLE, (float) value);
+ } else {
+ return sample;
+ }
+ }
+ }
+
+ @Override
+ public Object decodeScalarValue(final DataInputStream inputStream, final SampleOpcode opcode) throws IOException {
+ switch (opcode) {
+ case NULL:
+ return null;
+ case DOUBLE_ZERO:
+ return 0.0;
+ case INT_ZERO:
+ return 0;
+ case BYTE:
+ return inputStream.readByte();
+ case SHORT:
+ return inputStream.readShort();
+ case INT:
+ return inputStream.readInt();
+ case LONG:
+ return inputStream.readLong();
+ case FLOAT:
+ return inputStream.readFloat();
+ case DOUBLE:
+ return inputStream.readDouble();
+ case STRING:
+ final short s = inputStream.readShort();
+ final byte[] bytes = new byte[s];
+ final int byteCount = inputStream.read(bytes, 0, s);
+ if (byteCount != s) {
+ log.error("Reading string came up short");
+ }
+ return new String(bytes, "UTF-8");
+ case BIGINT:
+ final short bs = inputStream.readShort();
+ final byte[] bbytes = new byte[bs];
+ final int bbyteCount = inputStream.read(bbytes, 0, bs);
+ if (bbyteCount != bs) {
+ log.error("Reading bigint came up short");
+ }
+ return new BigInteger(new String(bbytes, "UTF-8"), 10);
+ case BYTE_FOR_DOUBLE:
+ return (double) inputStream.readByte();
+ case SHORT_FOR_DOUBLE:
+ return (double) inputStream.readShort();
+ case FLOAT_FOR_DOUBLE:
+ final float floatForDouble = inputStream.readFloat();
+ return (double) floatForDouble;
+ case HALF_FLOAT_FOR_DOUBLE:
+ final float f = HalfFloat.toFloat(inputStream.readShort());
+ return (double) f;
+ default:
+ final String err = String.format("In decodeScalarSample, opcode %s unrecognized", opcode.name());
+ log.error(err);
+ throw new IllegalArgumentException(err);
+ }
+ }
+
+ /*
+ * This differs from decodeScalarValue because this delivers exactly the
+ * type in the byte stream. Specifically, it does not convert the arg
+ * of *_FOR_DOUBLE int a Double()
+ */
+ private Object decodeOpcodeArg(final DataInputStream inputStream, final SampleOpcode opcode) throws IOException {
+ switch (opcode) {
+ case NULL:
+ return null;
+ case DOUBLE_ZERO:
+ return 0.0;
+ case INT_ZERO:
+ return 0;
+ case BYTE:
+ return inputStream.readByte();
+ case SHORT:
+ return inputStream.readShort();
+ case INT:
+ return inputStream.readInt();
+ case LONG:
+ return inputStream.readLong();
+ case FLOAT:
+ return inputStream.readFloat();
+ case DOUBLE:
+ return inputStream.readDouble();
+ case STRING:
+ final short s = inputStream.readShort();
+ final byte[] bytes = new byte[s];
+ final int byteCount = inputStream.read(bytes, 0, s);
+ if (byteCount != s) {
+ log.error("Reading string came up short");
+ }
+ return new String(bytes, "UTF-8");
+ case BIGINT:
+ final short bs = inputStream.readShort();
+ final byte[] bbytes = new byte[bs];
+ final int bbyteCount = inputStream.read(bbytes, 0, bs);
+ if (bbyteCount != bs) {
+ log.error("Reading bigint came up short");
+ }
+ return new BigInteger(new String(bbytes, "UTF-8"), 10);
+ case BYTE_FOR_DOUBLE:
+ return inputStream.readByte();
+ case SHORT_FOR_DOUBLE:
+ return inputStream.readShort();
+ case FLOAT_FOR_DOUBLE:
+ return inputStream.readFloat();
+ case HALF_FLOAT_FOR_DOUBLE:
+ return inputStream.readShort();
+ default:
+ final String err = String.format("In decodeOpcodeArg(), opcode %s unrecognized", opcode.name());
+ log.error(err);
+ throw new IllegalArgumentException(err);
+ }
+ }
+
+ @Override
+ public double getMaxFractionError() {
+ return MAX_FRACTION_ERROR;
+ }
+
+ @Override
+ public byte[] combineSampleBytes(final List<byte[]> sampleBytesList) {
+ final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ final DataOutputStream dataStream = new DataOutputStream(outputStream);
+ try {
+ SampleBase lastSample = null;
+ for (final byte[] samples : sampleBytesList) {
+ final ByteArrayInputStream byteStream = new ByteArrayInputStream(samples);
+ final DataInputStream byteDataStream = new DataInputStream(byteStream);
+ while (true) {
+ final int opcodeByte = byteDataStream.read();
+ if (opcodeByte == -1) {
+ break;
+ }
+ final SampleOpcode opcode = SampleOpcode.getOpcodeFromIndex(opcodeByte);
+ switch (opcode) {
+ case REPEAT_BYTE:
+ case REPEAT_SHORT:
+ final int newRepeatCount = opcode == SampleOpcode.REPEAT_BYTE ? byteDataStream.read() : byteDataStream.readUnsignedShort();
+ final SampleOpcode newRepeatedOpcode = SampleOpcode.getOpcodeFromIndex(byteDataStream.read());
+ final Object newValue = decodeOpcodeArg(byteDataStream, newRepeatedOpcode);
+ final ScalarSample newRepeatedSample = new ScalarSample(newRepeatedOpcode, newValue);
+ if (lastSample == null) {
+ lastSample = new RepeatSample(newRepeatCount, new ScalarSample(newRepeatedOpcode, newValue));
+ } else if (lastSample instanceof RepeatSample) {
+ final RepeatSample repeatSample = (RepeatSample) lastSample;
+ final ScalarSample repeatedScalarSample = repeatSample.getSampleRepeated();
+ if (repeatedScalarSample.getOpcode() == newRepeatedOpcode &&
+ (newRepeatedOpcode.getNoArgs() ||
+ (ScalarSample.sameSampleValues(repeatedScalarSample.getSampleValue(), newValue) &&
+ repeatSample.getRepeatCount() + newRepeatCount < RepeatSample.MAX_SHORT_REPEAT_COUNT))) {
+ // We can just increment the count in the repeat instance
+ repeatSample.incrementRepeatCount(newRepeatCount);
+ } else {
+ encodeSample(dataStream, lastSample);
+ lastSample = new RepeatSample(newRepeatCount, newRepeatedSample);
+ }
+ } else if (lastSample.equals(newRepeatedSample)) {
+ lastSample = new RepeatSample(newRepeatCount + 1, newRepeatedSample);
+ } else {
+ encodeSample(dataStream, lastSample);
+ lastSample = new RepeatSample(newRepeatCount, newRepeatedSample);
+ }
+ break;
+ default:
+ final ScalarSample newSample = new ScalarSample(opcode, decodeOpcodeArg(byteDataStream, opcode));
+ if (lastSample == null) {
+ lastSample = newSample;
+ } else if (lastSample instanceof RepeatSample) {
+ final RepeatSample repeatSample = (RepeatSample) lastSample;
+ final ScalarSample repeatedScalarSample = repeatSample.getSampleRepeated();
+ if (newSample.equals(repeatedScalarSample)) {
+ repeatSample.incrementRepeatCount();
+ } else {
+ encodeSample(dataStream, lastSample);
+ lastSample = newSample;
+ }
+ } else if (lastSample.equals(newSample)) {
+ lastSample = new RepeatSample(2, newSample);
+ } else {
+ encodeSample(dataStream, lastSample);
+ lastSample = newSample;
+ }
+ }
+ }
+ }
+ if (lastSample != null) {
+ encodeSample(dataStream, lastSample);
+ }
+ dataStream.flush();
+ return outputStream.toByteArray();
+ } catch (Exception e) {
+ log.error("In combineSampleBytes(), exception combining sample byte arrays", e);
+ return new byte[0];
+ }
+ }
+
+ /**
+ * This invokes the processor on the values in the timeline bytes.
+ *
+ * @param chunk the timeline chuck to scan
+ * @param processor the callback to which values value counts are passed to be processed.
+ * @throws java.io.IOException
+ */
+ @Override
+ public void scan(final TimelineChunk chunk, final SampleProcessor processor) throws IOException {
+ //System.out.printf("Decoded: %s\n", new String(Hex.encodeHex(bytes)));
+ scan(chunk.getTimeBytesAndSampleBytes().getSampleBytes(), chunk.getTimeBytesAndSampleBytes().getTimeBytes(), chunk.getSampleCount(), processor);
+ }
+
+ @Override
+ public void scan(final byte[] samples, final byte[] times, final int sampleCount, final SampleProcessor processor) throws IOException {
+ final ByteArrayInputStream byteStream = new ByteArrayInputStream(samples);
+ final DataInputStream inputStream = new DataInputStream(byteStream);
+ final TimelineCursor timeCursor = new DefaultTimelineCursor(times, sampleCount);
+ int sampleNumber = 0;
+ while (true) {
+ final int opcodeByte;
+ opcodeByte = inputStream.read();
+ if (opcodeByte == -1) {
+ return; // At "eof"
+ }
+ final SampleOpcode opcode = SampleOpcode.getOpcodeFromIndex(opcodeByte);
+ switch (opcode) {
+ case REPEAT_BYTE:
+ case REPEAT_SHORT:
+ final int repeatCount = opcode == SampleOpcode.REPEAT_BYTE ? inputStream.readUnsignedByte() : inputStream.readUnsignedShort();
+ final SampleOpcode repeatedOpcode = SampleOpcode.getOpcodeFromIndex(inputStream.read());
+ final Object value = decodeScalarValue(inputStream, repeatedOpcode);
+ final SampleOpcode replacementOpcode = repeatedOpcode.getReplacement();
+ processor.processSamples(timeCursor, repeatCount, replacementOpcode, value);
+ sampleNumber += repeatCount;
+ timeCursor.skipToSampleNumber(sampleNumber);
+ break;
+ default:
+ processor.processSamples(timeCursor, 1, opcode.getReplacement(), decodeScalarValue(inputStream, opcode));
+ break;
+ }
+ }
+ }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/codec/EncodedBytesAndSampleCount.java b/usage/src/main/java/com/ning/billing/usage/timeline/codec/EncodedBytesAndSampleCount.java
new file mode 100644
index 0000000..62f0a3a
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/codec/EncodedBytesAndSampleCount.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.codec;
+
+import java.util.Arrays;
+
+public class EncodedBytesAndSampleCount {
+
+ private final byte[] encodedBytes;
+ private final int sampleCount;
+
+ public EncodedBytesAndSampleCount(final byte[] encodedBytes, final int sampleCount) {
+ this.encodedBytes = encodedBytes;
+ this.sampleCount = sampleCount;
+ }
+
+ public byte[] getEncodedBytes() {
+ return encodedBytes;
+ }
+
+ public int getSampleCount() {
+ return sampleCount;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("EncodedBytesAndSampleCount");
+ sb.append("{encodedBytes=").append(encodedBytes == null ? "null" : "");
+ for (int i = 0; encodedBytes != null && i < encodedBytes.length; ++i) {
+ sb.append(i == 0 ? "" : ", ").append(encodedBytes[i]);
+ }
+ sb.append(", sampleCount=").append(sampleCount);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final EncodedBytesAndSampleCount that = (EncodedBytesAndSampleCount) o;
+
+ if (sampleCount != that.sampleCount) {
+ return false;
+ }
+ if (!Arrays.equals(encodedBytes, that.encodedBytes)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = encodedBytes != null ? Arrays.hashCode(encodedBytes) : 0;
+ result = 31 * result + sampleCount;
+ return result;
+ }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/codec/SampleAccumulator.java b/usage/src/main/java/com/ning/billing/usage/timeline/codec/SampleAccumulator.java
new file mode 100644
index 0000000..50a1621
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/codec/SampleAccumulator.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.codec;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.util.List;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.ning.billing.usage.timeline.samples.NullSample;
+import com.ning.billing.usage.timeline.samples.RepeatSample;
+import com.ning.billing.usage.timeline.samples.SampleBase;
+import com.ning.billing.usage.timeline.samples.SampleOpcode;
+import com.ning.billing.usage.timeline.samples.ScalarSample;
+
+/**
+ * Accumulator of samples. Samples are compressed using a SampleCoder.
+ */
+public class SampleAccumulator {
+
+ private static final Logger log = LoggerFactory.getLogger(SampleAccumulator.class);
+ private static final int DEFAULT_CHUNK_BYTE_ARRAY_SIZE = 100;
+
+ private ByteArrayOutputStream byteStream;
+ private DataOutputStream outputStream;
+ private int sampleCount;
+ private SampleBase lastSample;
+ protected final SampleCoder sampleCoder;
+
+ public SampleAccumulator(final SampleCoder sampleCoder) {
+ this.sampleCoder = sampleCoder;
+ reset();
+ }
+
+ public SampleAccumulator(final byte[] bytes, final SampleBase lastSample, final int sampleCount, final SampleCoder sampleCoder) throws IOException {
+ reset();
+ this.byteStream.write(bytes);
+ this.lastSample = lastSample;
+ this.sampleCount = sampleCount;
+ this.sampleCoder = sampleCoder;
+ }
+
+ public void addSampleList(final List<ScalarSample> samples) {
+ for (final ScalarSample sample : samples) {
+ addSample(sample);
+ }
+ }
+
+ public synchronized void addSample(final ScalarSample sample) {
+ if (lastSample == null) {
+ lastSample = sample;
+ } else {
+ final SampleOpcode lastOpcode = lastSample.getOpcode();
+ final SampleOpcode sampleOpcode = sample.getOpcode();
+ if (lastSample instanceof RepeatSample) {
+ final RepeatSample repeatSample = (RepeatSample) lastSample;
+ final ScalarSample sampleRepeated = repeatSample.getSampleRepeated();
+ if (sampleRepeated.getOpcode() == sampleOpcode &&
+ (sampleOpcode.getNoArgs() || ScalarSample.sameSampleValues(sampleRepeated.getSampleValue(), sample.getSampleValue())) &&
+ repeatSample.getRepeatCount() < RepeatSample.MAX_SHORT_REPEAT_COUNT) {
+ // We can just increment the count in the repeat instance
+ repeatSample.incrementRepeatCount();
+ } else {
+ // A non-matching repeat - just add it
+ addLastSample();
+ lastSample = sample;
+ }
+ } else {
+ final ScalarSample lastScalarSample = (ScalarSample) lastSample;
+ if (sampleOpcode == lastOpcode &&
+ (sampleOpcode.getNoArgs() || ScalarSample.sameSampleValues(sample.getSampleValue(), lastScalarSample.getSampleValue()))) {
+ // Replace lastSample with repeat group
+ lastSample = new RepeatSample(2, lastScalarSample);
+ } else {
+ addLastSample();
+ lastSample = sample;
+ }
+ }
+ }
+ // In all cases, we got 1 more sample
+ sampleCount++;
+ }
+
+ public int getSampleCount() {
+ return sampleCount;
+ }
+
+ protected ByteArrayOutputStream getByteStream() {
+ return byteStream;
+ }
+
+ protected SampleBase getLastSample() {
+ return lastSample;
+ }
+
+ /**
+ * The log scanner can safely call this method, and know that the byte
+ * array will always end in a complete sample
+ *
+ * @return an instance containing the bytes and the counts of samples
+ */
+ public synchronized EncodedBytesAndSampleCount getEncodedSamples() {
+ if (lastSample != null) {
+ sampleCoder.encodeSample(outputStream, lastSample);
+ lastSample = null;
+ }
+ try {
+ outputStream.flush();
+ return new EncodedBytesAndSampleCount(byteStream.toByteArray(), sampleCount);
+ } catch (IOException e) {
+ log.error("In getEncodedSamples, IOException flushing outputStream", e);
+ // Do no harm - - this at least won't corrupt the encoding
+ return new EncodedBytesAndSampleCount(new byte[0], 0);
+ }
+ }
+
+ private synchronized void addLastSample() {
+ if (lastSample != null) {
+ sampleCoder.encodeSample(outputStream, lastSample);
+ lastSample = null;
+ }
+ }
+
+ public synchronized void reset() {
+ byteStream = new ByteArrayOutputStream(DEFAULT_CHUNK_BYTE_ARRAY_SIZE);
+ outputStream = new DataOutputStream(byteStream);
+ lastSample = null;
+ sampleCount = 0;
+ }
+
+ public synchronized void addPlaceholder(final int repeatCount) {
+ if (repeatCount > 0) {
+ addLastSample();
+ lastSample = new RepeatSample<Void>(repeatCount, new NullSample());
+ sampleCount += repeatCount;
+ }
+ }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/codec/SampleCoder.java b/usage/src/main/java/com/ning/billing/usage/timeline/codec/SampleCoder.java
new file mode 100644
index 0000000..b218f8e
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/codec/SampleCoder.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.codec;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.util.List;
+
+import com.ning.billing.usage.timeline.chunks.TimelineChunk;
+import com.ning.billing.usage.timeline.samples.SampleBase;
+import com.ning.billing.usage.timeline.samples.SampleOpcode;
+import com.ning.billing.usage.timeline.samples.ScalarSample;
+
+/**
+ * Samples compressor and decompressor
+ */
+public interface SampleCoder {
+
+ public byte[] compressSamples(final List<ScalarSample> samples);
+
+ public List<ScalarSample> decompressSamples(final byte[] sampleBytes) throws IOException;
+
+ /**
+ * This method writes the binary encoding of the sample to the outputStream. This encoding
+ * is the form saved in the db and scanned when read from the db.
+ *
+ * @param outputStream the stream to which bytes should be written
+ * @param sample the sample to be written
+ */
+ public void encodeSample(final DataOutputStream outputStream, final SampleBase sample);
+
+ /**
+ * Output the scalar value into the output stream
+ *
+ * @param outputStream the stream to which bytes should be written
+ * @param value the sample value, interpreted according to the opcode
+ */
+ public void encodeScalarValue(final DataOutputStream outputStream, final SampleOpcode opcode, final Object value);
+
+ /**
+ * This routine returns a ScalarSample that may have a smaller representation than the
+ * ScalarSample argument. In particular, if tries hard to choose the most compact
+ * representation of double-precision values.
+ *
+ * @param sample A ScalarSample to be compressed
+ * @return Either the same ScalarSample is that input, for for some cases of opcode DOUBLE,
+ * a more compact ScalarSample which when processed returns a double value.
+ */
+ public ScalarSample compressSample(final ScalarSample sample);
+
+ public Object decodeScalarValue(final DataInputStream inputStream, final SampleOpcode opcode) throws IOException;
+
+ public double getMaxFractionError();
+
+ public byte[] combineSampleBytes(final List<byte[]> sampleBytesList);
+
+ public void scan(final TimelineChunk chunk, final SampleProcessor processor) throws IOException;
+
+ public void scan(final byte[] samples, final byte[] times, final int sampleCount, final SampleProcessor processor) throws IOException;
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/codec/SampleProcessor.java b/usage/src/main/java/com/ning/billing/usage/timeline/codec/SampleProcessor.java
new file mode 100644
index 0000000..077a107
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/codec/SampleProcessor.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.codec;
+
+import com.ning.billing.usage.timeline.samples.SampleOpcode;
+import com.ning.billing.usage.timeline.times.TimelineCursor;
+
+public interface SampleProcessor {
+
+ /**
+ * Process sampleCount sequential samples with identical values. sampleCount will usually be 1,
+ * but may be larger than 1. Implementors may just loop processing identical values, but some
+ * implementations may optimize adding a bunch of repeated values
+ *
+ * @param timeCursor a TimeCursor object from which times can be found.
+ * @param sampleCount the count of sequential, identical values
+ * @param opcode the opcode of the sample value, which may not be a REPEAT opcode
+ * @param value the value of this kind of sample over the count of samples
+ */
+ public void processSamples(final TimelineCursor timeCursor,
+ final int sampleCount,
+ final SampleOpcode opcode,
+ final Object value);
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/codec/TimelineChunkAccumulator.java b/usage/src/main/java/com/ning/billing/usage/timeline/codec/TimelineChunkAccumulator.java
new file mode 100644
index 0000000..69ab1b5
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/codec/TimelineChunkAccumulator.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.codec;
+
+import java.io.IOException;
+
+import org.joda.time.DateTime;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.ning.billing.usage.timeline.chunks.TimelineChunk;
+import com.ning.billing.usage.timeline.samples.SampleBase;
+
+/**
+ * This class represents a sequence of values for a single attribute,
+ * e.g., "TP99 Response Time", for one source and one specific time range,
+ * as the object is being accumulated. It is not used to represent
+ * past timeline sequences; they are held in TimelineChunk objects.
+ * <p/>
+ * It accumulates samples in a byte array object. Readers can call
+ * getEncodedSamples() at any time to get the latest data.
+ */
+public class TimelineChunkAccumulator extends SampleAccumulator {
+
+ private static final Logger log = LoggerFactory.getLogger(TimelineChunkAccumulator.class);
+ private final int sourceId;
+ private final int metricId;
+
+ public TimelineChunkAccumulator(final int sourceId, final int metricId, final SampleCoder sampleCoder) {
+ super(sampleCoder);
+ this.sourceId = sourceId;
+ this.metricId = metricId;
+ }
+
+ private TimelineChunkAccumulator(final int sourceId, final int metricId, final byte[] bytes, final SampleBase lastSample, final int sampleCount, final SampleCoder sampleCoder) throws IOException {
+ super(bytes, lastSample, sampleCount, sampleCoder);
+ this.sourceId = sourceId;
+ this.metricId = metricId;
+ }
+
+ public TimelineChunkAccumulator deepCopy() throws IOException {
+ return new TimelineChunkAccumulator(sourceId, metricId, getByteStream().toByteArray(), getLastSample(), getSampleCount(), sampleCoder);
+ }
+
+ /**
+ * This method grabs the current encoded form, and resets the accumulator
+ */
+ public synchronized TimelineChunk extractTimelineChunkAndReset(final DateTime startTime, final DateTime endTime, final byte[] timeBytes) {
+ // Extract the chunk
+ final byte[] sampleBytes = getEncodedSamples().getEncodedBytes();
+ log.debug("Creating TimelineChunk for metricId {}, sampleCount {}", metricId, getSampleCount());
+ final TimelineChunk chunk = new TimelineChunk(0, sourceId, metricId, startTime, endTime, timeBytes, sampleBytes, getSampleCount());
+
+ // Reset this current accumulator
+ reset();
+
+ return chunk;
+ }
+
+ public int getSourceId() {
+ return sourceId;
+ }
+
+ public int getMetricId() {
+ return metricId;
+ }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/codec/TimelineChunkDecoded.java b/usage/src/main/java/com/ning/billing/usage/timeline/codec/TimelineChunkDecoded.java
new file mode 100644
index 0000000..9b3ae3e
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/codec/TimelineChunkDecoded.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.codec;
+
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+import org.joda.time.DateTime;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.ning.billing.usage.timeline.chunks.TimelineChunk;
+import com.ning.billing.usage.timeline.samples.SampleOpcode;
+import com.ning.billing.usage.timeline.times.TimelineCursor;
+
+import com.fasterxml.jackson.annotation.JsonValue;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+public class TimelineChunkDecoded {
+
+ private static final Logger log = LoggerFactory.getLogger(TimelineChunkDecoded.class);
+ private static final ObjectMapper objectMapper = new ObjectMapper();
+
+ private final TimelineChunk chunk;
+ private final SampleCoder sampleCoder;
+
+ public TimelineChunkDecoded(final TimelineChunk chunk, final SampleCoder sampleCoder) {
+ this.chunk = chunk;
+ this.sampleCoder = sampleCoder;
+ }
+
+ @JsonValue
+ @Override
+ public String toString() {
+ try {
+ final ByteArrayOutputStream out = new ByteArrayOutputStream();
+ final JsonGenerator generator = objectMapper.getJsonFactory().createJsonGenerator(out);
+ generator.writeStartObject();
+
+ generator.writeFieldName("metric");
+ generator.writeNumber(chunk.getMetricId());
+
+ generator.writeFieldName("decodedSamples");
+ generator.writeString(getDecodedSamples());
+
+ generator.writeEndObject();
+ generator.close();
+ return out.toString();
+ } catch (IOException e) {
+ log.error("IOException in toString()", e);
+ }
+
+ return null;
+ }
+
+ private String getDecodedSamples() throws IOException {
+ final DecodedSampleOutputProcessor processor = new DecodedSampleOutputProcessor();
+ sampleCoder.scan(chunk, processor);
+ return processor.getDecodedSamples();
+ }
+
+ private static final class DecodedSampleOutputProcessor implements SampleProcessor {
+
+ final StringBuilder builder = new StringBuilder();
+
+ @Override
+ public void processSamples(final TimelineCursor timeCursor, final int sampleCount, final SampleOpcode opcode, final Object value) {
+ if (builder.length() > 0) {
+ builder.append(", ");
+ }
+ final DateTime timestamp = timeCursor.getNextTime();
+ builder.append("at ").append(timestamp.toString("yyyy-MM-dd HH:mm:ss")).append(" ");
+ if (sampleCount > 1) {
+ builder.append(sampleCount).append(" of ");
+ }
+ builder.append(opcode.name().toLowerCase());
+ switch (opcode) {
+ case NULL:
+ case DOUBLE_ZERO:
+ case INT_ZERO:
+ break;
+ default:
+ builder.append("(").append(String.valueOf(value)).append(")");
+ break;
+ }
+ }
+
+ public String getDecodedSamples() {
+ return builder.toString();
+ }
+ }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/codec/TimeRangeSampleProcessor.java b/usage/src/main/java/com/ning/billing/usage/timeline/codec/TimeRangeSampleProcessor.java
new file mode 100644
index 0000000..f0ffc32
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/codec/TimeRangeSampleProcessor.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.codec;
+
+import org.joda.time.DateTime;
+
+import com.ning.billing.usage.timeline.samples.SampleOpcode;
+import com.ning.billing.usage.timeline.times.TimelineCursor;
+
+public abstract class TimeRangeSampleProcessor implements SampleProcessor {
+
+ private final DateTime startTime; // Inclusive
+ private final DateTime endTime; // Inclusive
+
+ public TimeRangeSampleProcessor(final DateTime startTime, final DateTime endTime) {
+ this.startTime = startTime;
+ this.endTime = endTime;
+ }
+
+ /**
+ * Process sampleCount sequential samples with identical values. sampleCount will usually be 1,
+ * but may be larger than 1. Implementors may just loop processing identical values, but some
+ * implementations may optimize adding a bunch of repeated values
+ *
+ * @param timeCursor a TimeCursor instance, which supplies successive int UNIX times
+ * @param sampleCount the count of sequential, identical values
+ * @param opcode the opcode of the sample value, which may not be a REPEAT opcode
+ * @param value the value of this kind of sample over the sampleCount samples
+ */
+ @Override
+ public void processSamples(final TimelineCursor timeCursor, final int sampleCount, final SampleOpcode opcode, final Object value) {
+ for (int i = 0; i < sampleCount; i++) {
+ // Check if the sample is in the right time range
+ final DateTime sampleTime = timeCursor.getNextTime();
+ if ((startTime == null || !sampleTime.isBefore(startTime)) && ((endTime == null || !sampleTime.isAfter(endTime)))) {
+ processOneSample(sampleTime, opcode, value);
+ }
+ }
+ }
+
+ public abstract void processOneSample(final DateTime time, final SampleOpcode opcode, final Object value);
+
+ public DateTime getStartTime() {
+ return startTime;
+ }
+
+ public DateTime getEndTime() {
+ return endTime;
+ }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/codec/TimesAndSamplesCoder.java b/usage/src/main/java/com/ning/billing/usage/timeline/codec/TimesAndSamplesCoder.java
new file mode 100644
index 0000000..5d2a179
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/codec/TimesAndSamplesCoder.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.codec;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.util.Arrays;
+
+import com.ning.billing.usage.timeline.chunks.TimeBytesAndSampleBytes;
+import com.ning.billing.usage.timeline.chunks.TimelineChunk;
+import com.ning.billing.usage.timeline.util.Hex;
+
+public class TimesAndSamplesCoder {
+
+ public static int getSizeOfTimeBytes(final byte[] timesAndSamples) {
+ final DataInputStream inputStream = new DataInputStream(new ByteArrayInputStream(timesAndSamples));
+ try {
+ return inputStream.readInt();
+ } catch (IOException e) {
+ throw new IllegalStateException(String.format("Exception reading timeByteCount in TimelineChunkMapper.map() for timesAndSamples %s",
+ new String(Hex.encodeHex(timesAndSamples))), e);
+ }
+ }
+
+ public static int getEncodedLength(final TimelineChunk chunk) {
+ return 4 + chunk.getTimeBytesAndSampleBytes().getTimeBytes().length +
+ chunk.getTimeBytesAndSampleBytes().getSampleBytes().length;
+ }
+
+ public static byte[] getTimeBytes(final byte[] timesAndSamples) {
+ final int timeByteCount = getSizeOfTimeBytes(timesAndSamples);
+ return Arrays.copyOfRange(timesAndSamples, 4, 4 + timeByteCount);
+ }
+
+ public static byte[] getSampleBytes(final byte[] timesAndSamples) {
+ final int timeByteCount = getSizeOfTimeBytes(timesAndSamples);
+ return Arrays.copyOfRange(timesAndSamples, 4 + timeByteCount, timesAndSamples.length);
+ }
+
+ public static TimeBytesAndSampleBytes getTimesBytesAndSampleBytes(final byte[] timesAndSamples) {
+ final int timeByteCount = getSizeOfTimeBytes(timesAndSamples);
+ final byte[] timeBytes = Arrays.copyOfRange(timesAndSamples, 4, 4 + timeByteCount);
+ final byte[] sampleBytes = Arrays.copyOfRange(timesAndSamples, 4 + timeByteCount, timesAndSamples.length);
+ return new TimeBytesAndSampleBytes(timeBytes, sampleBytes);
+ }
+
+ public static byte[] combineTimesAndSamples(final byte[] times, final byte[] samples) {
+ final int totalSamplesSize = 4 + times.length + samples.length;
+ final ByteArrayOutputStream baStream = new ByteArrayOutputStream(totalSamplesSize);
+ final DataOutputStream outputStream = new DataOutputStream(baStream);
+ try {
+ outputStream.writeInt(times.length);
+ outputStream.write(times);
+ outputStream.write(samples);
+ outputStream.flush();
+ return baStream.toByteArray();
+ } catch (IOException e) {
+ throw new IllegalStateException(String.format("Exception reading timeByteCount in TimelineChunkMapper.map() for times %s, samples %s",
+ new String(Hex.encodeHex(times)), new String(Hex.encodeHex(samples))), e);
+ }
+ }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/consumer/CSVConsumer.java b/usage/src/main/java/com/ning/billing/usage/timeline/consumer/CSVConsumer.java
new file mode 100644
index 0000000..879e944
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/consumer/CSVConsumer.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.consumer;
+
+import java.io.IOException;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTime;
+
+import com.ning.billing.usage.timeline.chunks.TimelineChunk;
+import com.ning.billing.usage.timeline.codec.SampleCoder;
+import com.ning.billing.usage.timeline.filter.DecimatingSampleFilter;
+
+public class CSVConsumer {
+
+ private CSVConsumer() {
+ }
+
+ public static String getSamplesAsCSV(final SampleCoder sampleCoder, final TimelineChunk chunk, final DecimatingSampleFilter rangeSampleProcessor) throws IOException {
+ sampleCoder.scan(chunk, rangeSampleProcessor);
+ return rangeSampleProcessor.getSampleConsumer().toString();
+ }
+
+ public static String getSamplesAsCSV(final SampleCoder sampleCoder, final TimelineChunk chunk, @Nullable final DateTime startTime, @Nullable final DateTime endTime) throws IOException {
+ final CSVOutputProcessor processor = new CSVOutputProcessor(startTime, endTime);
+ sampleCoder.scan(chunk, processor);
+ return processor.toString();
+ }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/consumer/CSVOutputProcessor.java b/usage/src/main/java/com/ning/billing/usage/timeline/consumer/CSVOutputProcessor.java
new file mode 100644
index 0000000..6e5aa11
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/consumer/CSVOutputProcessor.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.consumer;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTime;
+
+import com.ning.billing.usage.timeline.codec.TimeRangeSampleProcessor;
+import com.ning.billing.usage.timeline.samples.SampleOpcode;
+
+public class CSVOutputProcessor extends TimeRangeSampleProcessor {
+
+ private final SampleConsumer delegate = new CSVSampleConsumer();
+ private int sampleNumber = 0;
+
+ public CSVOutputProcessor(@Nullable final DateTime startTime, @Nullable final DateTime endTime) {
+ super(startTime, endTime);
+ }
+
+ @Override
+ public void processOneSample(final DateTime sampleTimestamp, final SampleOpcode opcode, final Object value) {
+ delegate.consumeSample(sampleNumber, opcode, value, sampleTimestamp);
+ sampleNumber++;
+ }
+
+ @Override
+ public String toString() {
+ return delegate.toString();
+ }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/consumer/CSVSampleConsumer.java b/usage/src/main/java/com/ning/billing/usage/timeline/consumer/CSVSampleConsumer.java
new file mode 100644
index 0000000..cde8725
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/consumer/CSVSampleConsumer.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.consumer;
+
+import org.joda.time.DateTime;
+
+import com.ning.billing.usage.timeline.samples.SampleOpcode;
+import com.ning.billing.usage.timeline.util.DateTimeUtils;
+
+public class CSVSampleConsumer implements SampleConsumer {
+
+ private final StringBuilder builder = new StringBuilder();
+ // Use our private counter because of the decimating filter
+ private int builderSampleNumber = 0;
+
+ @Override
+ public void consumeSample(final int sampleNumber, final SampleOpcode opcode, final Object value, final DateTime time) {
+ if (time != null) {
+ final String valueString = value == null ? "0" : value.toString();
+ if (builderSampleNumber > 0) {
+ builder.append(",");
+ }
+
+ builder.append(DateTimeUtils.unixSeconds(time))
+ .append(",")
+ .append(valueString);
+ builderSampleNumber++;
+ }
+ }
+
+ @Override
+ public synchronized String toString() {
+ final String value = builder.toString();
+ // Allow for re-use
+ builder.setLength(0);
+ builderSampleNumber = 0;
+ return value;
+ }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/consumer/SampleConsumer.java b/usage/src/main/java/com/ning/billing/usage/timeline/consumer/SampleConsumer.java
new file mode 100644
index 0000000..f6a04c3
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/consumer/SampleConsumer.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.consumer;
+
+import org.joda.time.DateTime;
+
+import com.ning.billing.usage.timeline.samples.SampleOpcode;
+
+public interface SampleConsumer {
+
+ public void consumeSample(int sampleNumber, SampleOpcode opcode, Object value, DateTime time);
+}
+
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/consumer/TimelineChunkConsumer.java b/usage/src/main/java/com/ning/billing/usage/timeline/consumer/TimelineChunkConsumer.java
new file mode 100644
index 0000000..1c403da
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/consumer/TimelineChunkConsumer.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.consumer;
+
+import com.ning.billing.usage.timeline.chunks.TimelineChunk;
+
+public interface TimelineChunkConsumer {
+
+ public void processTimelineChunk(TimelineChunk chunk);
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/filter/DecimatingSampleFilter.java b/usage/src/main/java/com/ning/billing/usage/timeline/filter/DecimatingSampleFilter.java
new file mode 100644
index 0000000..ccf26a2
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/filter/DecimatingSampleFilter.java
@@ -0,0 +1,247 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.filter;
+
+import org.joda.time.DateTime;
+import org.skife.config.TimeSpan;
+
+import com.ning.billing.usage.timeline.codec.TimeRangeSampleProcessor;
+import com.ning.billing.usage.timeline.consumer.SampleConsumer;
+import com.ning.billing.usage.timeline.samples.SampleOpcode;
+import com.ning.billing.usage.timeline.samples.ScalarSample;
+
+/**
+ * This SampleProcessor interpolates a stream of sample values to such that the
+ * number of outputs sent to the SampleConsumer is outputCount, which is less
+ * than sampleCount. It works by keeping a history of scanned samples
+ * representing at least one output sample, and makes a choice of what value to
+ * output from those scanned samples:
+ * <p/>
+ * The rules for sample generation are:
+ * <ul>
+ * <li>No averaging - - the sample returned is _always_ one of the scanned
+ * samples</li>
+ * <li>The output sample is always either the largest or the smallest of the
+ * scanned sample values</li>
+ * <li>Whether it is the largest or smallest depends on the "trend" of the
+ * samples:
+ * <ul>
+ * <li>If they are generally high-to-low then we output the low value.</li>
+ * <li>If they are generally low-to-high then we output the high value.</li>
+ * </ul>
+ * </ul>
+ * <p/>
+ * The rationale for these rules is the most interesting information is the
+ * peaks and valleys of measurements, and averaging is bad because it destroys
+ * peaks and valleys. A consequence of these rules is that quantities that
+ * bounce around a lot will generate graphs that are a solid band between the
+ * min and max values. But that's really an accurate reflection of the state. To
+ * get more information, you have to look at shorter time intervals. The class
+ * tries hard to make good choices amount
+ * <p/>
+ * Of course this sort of crude averaging isn't perfect, but at least it doesn't
+ * destroy peaks and valleys.
+ * <p/>
+ * TODO: Figure out if the time passed to SampleConsumer should be the time
+ * of the sample or the midpoint of the times between first and last sample.
+ */
+public class DecimatingSampleFilter extends TimeRangeSampleProcessor {
+
+ private final int outputCount;
+ private final SampleConsumer sampleConsumer;
+ private final TimeSpan pollingInterval;
+ private final DecimationMode decimationMode;
+ private double samplesPerOutput;
+ private double outputsPerSample;
+ private int ceilSamplesPerOutput;
+ private SampleState[] filterHistory;
+ private boolean initialized = false;
+
+ private double runningSum = 0.0;
+ private int sampleNumber = 0;
+
+ /**
+ * Build a DecimatingSampleFilter on which you call processSamples()
+ *
+ * @param startTime The start time we're considering values, or null, meaning all time
+ * @param endTime The end time we're considering values, or null, meaning all time
+ * @param outputCount The number of samples to generate
+ * @param sampleCount The number of samples to be scanned. sampleCount must be >= outputCount
+ * @param pollingInterval The polling interval, used to compute sample counts assuming no gaps
+ * @param decimationMode The decimation mode determines how samples will be combined to crate an output point.
+ * @param sampleConsumer The implementor of the SampleConsumer interface
+ */
+ public DecimatingSampleFilter(final DateTime startTime, final DateTime endTime, final int outputCount, final int sampleCount,
+ final TimeSpan pollingInterval, final DecimationMode decimationMode, final SampleConsumer sampleConsumer) {
+ super(startTime, endTime);
+ if (outputCount <= 0 || sampleCount <= 0 || outputCount > sampleCount) {
+ throw new IllegalArgumentException(String.format("In DecimatingSampleFilter, outputCount is %d but sampleCount is %d", outputCount, sampleCount));
+ }
+ this.outputCount = outputCount;
+ this.pollingInterval = pollingInterval;
+ this.decimationMode = decimationMode;
+ this.sampleConsumer = sampleConsumer;
+ initializeFilterHistory(sampleCount);
+ }
+
+ /**
+ * This form of the constructor delays initialization til we get the first sample
+ *
+ * @param startTime The start time we're considering values, or null, meaning all time
+ * @param endTime The end time we're considering values, or null, meaning all time
+ * @param outputCount The number of samples to generate
+ * @param pollingInterval The polling interval, used to compute sample counts assuming no gaps
+ * @param decimationMode The decimation mode determines how samples will be combined to crate an output point.
+ * @param sampleConsumer The implementor of the SampleConsumer interface
+ */
+ public DecimatingSampleFilter(final DateTime startTime, final DateTime endTime, final int outputCount, final TimeSpan pollingInterval,
+ final DecimationMode decimationMode, final SampleConsumer sampleConsumer) {
+ super(startTime, endTime);
+ this.outputCount = outputCount;
+ this.pollingInterval = pollingInterval;
+ this.decimationMode = decimationMode;
+ this.sampleConsumer = sampleConsumer;
+ }
+
+ private void initializeFilterHistory(final int sampleCount) {
+ if (outputCount <= 0 || sampleCount <= 0 || outputCount > sampleCount) {
+ throw new IllegalArgumentException(String.format("In DecimatingSampleFilter.initialize(), outputCount is %d but sampleCount is %d", outputCount, sampleCount));
+ }
+ this.samplesPerOutput = (double) sampleCount / (double) outputCount;
+ this.outputsPerSample = 1.0 / this.samplesPerOutput;
+ ceilSamplesPerOutput = (int) Math.ceil(samplesPerOutput);
+ filterHistory = new SampleState[ceilSamplesPerOutput];
+ initialized = true;
+ }
+
+ @Override
+ public void processOneSample(final DateTime time, final SampleOpcode opcode, final Object value) {
+ if (!initialized) {
+ // Estimate the sampleCount, assuming that there are no gaps
+ final long adjustedEndMillis = Math.min(getEndTime().getMillis(), System.currentTimeMillis());
+ final long millisTilEnd = adjustedEndMillis - time.getMillis();
+ final int sampleCount = Math.max(outputCount, (int) (millisTilEnd / pollingInterval.getMillis()));
+ initializeFilterHistory(sampleCount);
+ }
+ sampleNumber++;
+ final SampleState sampleState = new SampleState(opcode, value, ScalarSample.getDoubleValue(opcode, value), time);
+ final int historyIndex = sampleNumber % filterHistory.length;
+ filterHistory[historyIndex] = sampleState;
+ runningSum += outputsPerSample;
+ if (runningSum >= 1.0) {
+ runningSum -= 1.0;
+ if (opcode == SampleOpcode.STRING) {
+ // We don't have interpolation, so just output
+ // this one
+ sampleConsumer.consumeSample(sampleNumber, opcode, value, time);
+ } else {
+ // Time to output a sample - compare the sum of the first samples with the
+ // sum of the last samples making up the output, choosing the lowest value if
+ // if the first samples are larger, and the highest value if the last samples
+ // are larger
+ final int samplesInAverage = ceilSamplesPerOutput > 5 ? ceilSamplesPerOutput * 2 / 3 : Math.max(1, ceilSamplesPerOutput - 1);
+ final int samplesLeftOut = ceilSamplesPerOutput - samplesInAverage;
+ double max = Double.MIN_VALUE;
+ int maxIndex = 0;
+ int minIndex = 0;
+ double min = Double.MAX_VALUE;
+ double sum = 0.0;
+ double firstSum = 0.0;
+ double lastSum = 0.0;
+ for (int i = 0; i < ceilSamplesPerOutput; i++) {
+ final int index = (sampleNumber + ceilSamplesPerOutput - i) % ceilSamplesPerOutput;
+ final SampleState sample = filterHistory[index];
+ if (sample != null) {
+ final double doubleValue = sample.getDoubleValue();
+ sum += doubleValue;
+ if (doubleValue > max) {
+ max = doubleValue;
+ maxIndex = index;
+ }
+ if (doubleValue < min) {
+ min = doubleValue;
+ minIndex = index;
+ }
+ if (i < samplesInAverage) {
+ lastSum += doubleValue;
+ }
+ if (i >= samplesLeftOut) {
+ firstSum += doubleValue;
+ }
+ }
+ }
+ final SampleState firstSample = filterHistory[(sampleNumber + ceilSamplesPerOutput - (ceilSamplesPerOutput - 1)) % ceilSamplesPerOutput];
+ final SampleState lastSample = filterHistory[sampleNumber % ceilSamplesPerOutput];
+ final DateTime centerTime = firstSample != null ? new DateTime((firstSample.getTime().getMillis() + lastSample.getTime().getMillis()) >> 1) : lastSample.getTime();
+ switch (decimationMode) {
+ case PEAK_PICK:
+ if (firstSum > lastSum) {
+ // The sample window is generally down with time - - pick the minimum
+ final SampleState minSample = filterHistory[minIndex];
+ sampleConsumer.consumeSample(sampleNumber, minSample.getSampleOpcode(), minSample.getValue(), centerTime);
+ } else {
+ // The sample window is generally up with time - - pick the maximum
+ final SampleState maxSample = filterHistory[maxIndex];
+ sampleConsumer.consumeSample(sampleNumber, maxSample.getSampleOpcode(), maxSample.getValue(), centerTime);
+ }
+ break;
+ case AVERAGE:
+ final double average = sum / ceilSamplesPerOutput;
+ sampleConsumer.consumeSample(minIndex, SampleOpcode.DOUBLE, average, centerTime);
+ break;
+ default:
+ throw new IllegalStateException(String.format("The decimation filter mode %s is not recognized", decimationMode));
+ }
+ }
+ }
+ }
+
+ public SampleConsumer getSampleConsumer() {
+ return sampleConsumer;
+ }
+
+ private static class SampleState {
+
+ private final SampleOpcode sampleOpcode;
+ private final Object value;
+ private final double doubleValue;
+ private final DateTime time;
+
+ public SampleState(final SampleOpcode sampleOpcode, final Object value, final double doubleValue, final DateTime time) {
+ this.sampleOpcode = sampleOpcode;
+ this.value = value;
+ this.doubleValue = doubleValue;
+ this.time = time;
+ }
+
+ public SampleOpcode getSampleOpcode() {
+ return sampleOpcode;
+ }
+
+ public Object getValue() {
+ return value;
+ }
+
+ public double getDoubleValue() {
+ return doubleValue;
+ }
+
+ public DateTime getTime() {
+ return time;
+ }
+ }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/filter/DecimationMode.java b/usage/src/main/java/com/ning/billing/usage/timeline/filter/DecimationMode.java
new file mode 100644
index 0000000..0523bd7
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/filter/DecimationMode.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.filter;
+
+public enum DecimationMode {
+ PEAK_PICK,
+ AVERAGE;
+
+ public static DecimationMode fromString(final String modeString) {
+ for (final DecimationMode decimationMode : DecimationMode.values()) {
+ if (decimationMode.name().equalsIgnoreCase(modeString)) {
+ return decimationMode;
+ }
+ }
+ return null;
+ }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/metrics/MetricAndId.java b/usage/src/main/java/com/ning/billing/usage/timeline/metrics/MetricAndId.java
new file mode 100644
index 0000000..760eaf1
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/metrics/MetricAndId.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.metrics;
+
+public class MetricAndId {
+
+ private final String metric;
+ private final int metricId;
+
+ public MetricAndId(final String metric, final int metricId) {
+ this.metric = metric;
+ this.metricId = metricId;
+ }
+
+ public String getMetric() {
+ return metric;
+ }
+
+ public int getMetricId() {
+ return metricId;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("MetricAndId");
+ sb.append("{metric='").append(metric).append('\'');
+ sb.append(", metricId=").append(metricId);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final MetricAndId that = (MetricAndId) o;
+
+ if (metricId != that.metricId) {
+ return false;
+ }
+ if (metric != null ? !metric.equals(that.metric) : that.metric != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = metric != null ? metric.hashCode() : 0;
+ result = 31 * result + metricId;
+ return result;
+ }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/metrics/MetricAndIdMapper.java b/usage/src/main/java/com/ning/billing/usage/timeline/metrics/MetricAndIdMapper.java
new file mode 100644
index 0000000..2cba491
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/metrics/MetricAndIdMapper.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.metrics;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+import org.skife.jdbi.v2.StatementContext;
+import org.skife.jdbi.v2.tweak.ResultSetMapper;
+
+public class MetricAndIdMapper implements ResultSetMapper<MetricAndId> {
+
+ @Override
+ public MetricAndId map(final int index, final ResultSet r, final StatementContext ctx) throws SQLException {
+ return new MetricAndId(r.getString("sample_kind"), r.getInt("sample_kind_id"));
+ }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/metrics/SamplesForMetricAndSource.java b/usage/src/main/java/com/ning/billing/usage/timeline/metrics/SamplesForMetricAndSource.java
new file mode 100644
index 0000000..acb0cb7
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/metrics/SamplesForMetricAndSource.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.metrics;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class SamplesForMetricAndSource {
+
+ @JsonProperty
+ private final String sourceName;
+
+ @JsonProperty
+ private final String eventCategory;
+
+ @JsonProperty
+ private final String metric;
+
+ @JsonProperty
+ private final String samples;
+
+ @JsonCreator
+ public SamplesForMetricAndSource(@JsonProperty("sourceName") final String sourceName, @JsonProperty("eventCategory") final String eventCategory,
+ @JsonProperty("metric") final String metric, @JsonProperty("samples") final String samples) {
+ this.sourceName = sourceName;
+ this.eventCategory = eventCategory;
+ this.metric = metric;
+ this.samples = samples;
+ }
+
+ public String getSourceName() {
+ return sourceName;
+ }
+
+ public String getEventCategory() {
+ return eventCategory;
+ }
+
+ public String getMetric() {
+ return metric;
+ }
+
+ public String getSamples() {
+ return samples;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("SamplesForMetricAndSource");
+ sb.append("{eventCategory='").append(eventCategory).append('\'');
+ sb.append(", sourceName='").append(sourceName).append('\'');
+ sb.append(", metric='").append(metric).append('\'');
+ sb.append(", samples='").append(samples).append('\'');
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final SamplesForMetricAndSource that = (SamplesForMetricAndSource) o;
+
+ if (!eventCategory.equals(that.eventCategory)) {
+ return false;
+ }
+ if (!sourceName.equals(that.sourceName)) {
+ return false;
+ }
+ if (!metric.equals(that.metric)) {
+ return false;
+ }
+ if (!samples.equals(that.samples)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = sourceName.hashCode();
+ result = 31 * result + eventCategory.hashCode();
+ result = 31 * result + metric.hashCode();
+ result = 31 * result + samples.hashCode();
+ return result;
+ }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/PendingChunkMap.java b/usage/src/main/java/com/ning/billing/usage/timeline/PendingChunkMap.java
new file mode 100644
index 0000000..5337e77
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/PendingChunkMap.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline;
+
+import java.util.Map;
+
+import com.ning.billing.usage.timeline.chunks.TimelineChunk;
+
+public class PendingChunkMap {
+
+ private final TimelineSourceEventAccumulator accumulator;
+ private final long pendingChunkMapId;
+ private final Map<Integer, TimelineChunk> chunkMap;
+
+ public PendingChunkMap(final TimelineSourceEventAccumulator accumulator, final long pendingChunkMapId, final Map<Integer, TimelineChunk> chunkMap) {
+ this.accumulator = accumulator;
+ this.pendingChunkMapId = pendingChunkMapId;
+ this.chunkMap = chunkMap;
+ }
+
+ public TimelineSourceEventAccumulator getAccumulator() {
+ return accumulator;
+ }
+
+ public long getPendingChunkMapId() {
+ return pendingChunkMapId;
+ }
+
+ public Map<Integer, TimelineChunk> getChunkMap() {
+ return chunkMap;
+ }
+
+ public int getChunkCount() {
+ return chunkMap.size();
+ }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/persistent/CachingTimelineDao.java b/usage/src/main/java/com/ning/billing/usage/timeline/persistent/CachingTimelineDao.java
new file mode 100644
index 0000000..e6d74d7
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/persistent/CachingTimelineDao.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.persistent;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTime;
+import org.skife.jdbi.v2.exceptions.CallbackFailedException;
+import org.skife.jdbi.v2.exceptions.UnableToObtainConnectionException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.ning.billing.usage.timeline.categories.CategoryIdAndMetric;
+import com.ning.billing.usage.timeline.chunks.TimelineChunk;
+import com.ning.billing.usage.timeline.consumer.TimelineChunkConsumer;
+import com.ning.billing.usage.timeline.shutdown.StartTimes;
+import com.ning.billing.usage.timeline.sources.SourceIdAndMetricId;
+
+import com.google.common.collect.BiMap;
+import com.google.common.collect.ImmutableList;
+
+public class CachingTimelineDao implements TimelineDao {
+
+ private static final Logger log = LoggerFactory.getLogger(CachingTimelineDao.class);
+
+ private final BiMap<Integer, String> sourcesCache;
+ private final Map<Integer, Set<Integer>> sourceIdsMetricIdsCache;
+ private final BiMap<Integer, CategoryIdAndMetric> metricsCache;
+ private final BiMap<Integer, String> eventCategoriesCache;
+
+ private final TimelineDao delegate;
+
+ public CachingTimelineDao(final TimelineDao delegate) {
+ this.delegate = delegate;
+ sourcesCache = delegate.getSources();
+ metricsCache = delegate.getMetrics();
+ eventCategoriesCache = delegate.getEventCategories();
+ sourceIdsMetricIdsCache = new HashMap<Integer, Set<Integer>>();
+ for (final SourceIdAndMetricId both : delegate.getMetricIdsForAllSources()) {
+ final int sourceId = both.getSourceId();
+ final int metricId = both.getMetricId();
+ Set<Integer> metricIds = sourceIdsMetricIdsCache.get(sourceId);
+ if (metricIds == null) {
+ metricIds = new HashSet<Integer>();
+ sourceIdsMetricIdsCache.put(sourceId, metricIds);
+ }
+ metricIds.add(metricId);
+ }
+ }
+
+ @Override
+ public Integer getSourceId(final String source) throws UnableToObtainConnectionException, CallbackFailedException {
+ return sourcesCache.inverse().get(source);
+ }
+
+ @Override
+ public String getSource(final Integer sourceId) throws UnableToObtainConnectionException, CallbackFailedException {
+ return sourcesCache.get(sourceId);
+ }
+
+ @Override
+ public BiMap<Integer, String> getSources() throws UnableToObtainConnectionException, CallbackFailedException {
+ return delegate.getSources();
+ }
+
+ @Override
+ public synchronized int getOrAddSource(final String source) throws UnableToObtainConnectionException, CallbackFailedException {
+ Integer sourceId = sourcesCache.inverse().get(source);
+ if (sourceId == null) {
+ sourceId = delegate.getOrAddSource(source);
+ sourcesCache.put(sourceId, source);
+ }
+
+ return sourceId;
+ }
+
+ @Override
+ public Integer getEventCategoryId(final String eventCategory) throws UnableToObtainConnectionException, CallbackFailedException {
+ return eventCategoriesCache.inverse().get(eventCategory);
+ }
+
+ @Override
+ public String getEventCategory(final Integer eventCategoryId) throws UnableToObtainConnectionException {
+ return eventCategoriesCache.get(eventCategoryId);
+ }
+
+ @Override
+ public BiMap<Integer, String> getEventCategories() throws UnableToObtainConnectionException, CallbackFailedException {
+ return delegate.getEventCategories();
+ }
+
+ @Override
+ public int getOrAddEventCategory(final String eventCategory) throws UnableToObtainConnectionException, CallbackFailedException {
+ Integer eventCategoryId = eventCategoriesCache.inverse().get(eventCategory);
+ if (eventCategoryId == null) {
+ eventCategoryId = delegate.getOrAddEventCategory(eventCategory);
+ eventCategoriesCache.put(eventCategoryId, eventCategory);
+ }
+ return eventCategoryId;
+ }
+
+ @Override
+ public Integer getMetricId(final int eventCategoryId, final String metric) throws UnableToObtainConnectionException {
+ return metricsCache.inverse().get(new CategoryIdAndMetric(eventCategoryId, metric));
+ }
+
+ @Override
+ public CategoryIdAndMetric getCategoryIdAndMetric(final Integer metricId) throws UnableToObtainConnectionException {
+ return metricsCache.get(metricId);
+ }
+
+ @Override
+ public BiMap<Integer, CategoryIdAndMetric> getMetrics() throws UnableToObtainConnectionException, CallbackFailedException {
+ return delegate.getMetrics();
+ }
+
+ @Override
+ public synchronized int getOrAddMetric(final Integer sourceId, final Integer eventCategoryId, final String metric) throws UnableToObtainConnectionException, CallbackFailedException {
+ final CategoryIdAndMetric categoryIdAndMetric = new CategoryIdAndMetric(eventCategoryId, metric);
+ Integer metricId = metricsCache.inverse().get(categoryIdAndMetric);
+ if (metricId == null) {
+ metricId = delegate.getOrAddMetric(sourceId, eventCategoryId, metric);
+ metricsCache.put(metricId, categoryIdAndMetric);
+ }
+ if (sourceId != null) {
+ Set<Integer> metricIds = sourceIdsMetricIdsCache.get(sourceId);
+ if (metricIds == null) {
+ metricIds = new HashSet<Integer>();
+ sourceIdsMetricIdsCache.put(sourceId, metricIds);
+ }
+ metricIds.add(metricId);
+ }
+ return metricId;
+ }
+
+ @Override
+ public Iterable<Integer> getMetricIdsBySourceId(final Integer sourceId) throws UnableToObtainConnectionException, CallbackFailedException {
+ return ImmutableList.copyOf(sourceIdsMetricIdsCache.get(sourceId));
+ }
+
+ @Override
+ public Iterable<SourceIdAndMetricId> getMetricIdsForAllSources() throws UnableToObtainConnectionException, CallbackFailedException {
+ return delegate.getMetricIdsForAllSources();
+ }
+
+
+ @Override
+ public Long insertTimelineChunk(final TimelineChunk timelineChunk) throws UnableToObtainConnectionException, CallbackFailedException {
+ return delegate.insertTimelineChunk(timelineChunk);
+ }
+
+ @Override
+ public void getSamplesBySourceIdsAndMetricIds(final List<Integer> sourceIds, @Nullable final List<Integer> metricIds,
+ final DateTime startTime, final DateTime endTime, final TimelineChunkConsumer chunkConsumer) throws UnableToObtainConnectionException, CallbackFailedException {
+ delegate.getSamplesBySourceIdsAndMetricIds(sourceIds, metricIds, startTime, endTime, chunkConsumer);
+ }
+
+ @Override
+ public Integer insertLastStartTimes(final StartTimes startTimes) {
+ return delegate.insertLastStartTimes(startTimes);
+ }
+
+ @Override
+ public StartTimes getLastStartTimes() {
+ return delegate.getLastStartTimes();
+ }
+
+ @Override
+ public void deleteLastStartTimes() {
+ delegate.deleteLastStartTimes();
+ }
+
+ @Override
+ public void bulkInsertEventCategories(final List<String> categoryNames) throws UnableToObtainConnectionException, CallbackFailedException {
+ delegate.bulkInsertEventCategories(categoryNames);
+ }
+
+ @Override
+ public void bulkInsertSources(final List<String> sources) throws UnableToObtainConnectionException, CallbackFailedException {
+ delegate.bulkInsertSources(sources);
+ }
+
+ @Override
+ public void bulkInsertMetrics(final List<CategoryIdAndMetric> categoryAndKinds) {
+ delegate.bulkInsertMetrics(categoryAndKinds);
+ }
+
+ @Override
+ public void bulkInsertTimelineChunks(final List<TimelineChunk> timelineChunkList) {
+ delegate.bulkInsertTimelineChunks(timelineChunkList);
+ }
+
+ @Override
+ public void test() throws UnableToObtainConnectionException, CallbackFailedException {
+ delegate.test();
+ }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/persistent/DefaultTimelineDao.java b/usage/src/main/java/com/ning/billing/usage/timeline/persistent/DefaultTimelineDao.java
new file mode 100644
index 0000000..37ba9ae
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/persistent/DefaultTimelineDao.java
@@ -0,0 +1,255 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.persistent;
+
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTime;
+import org.skife.jdbi.v2.Handle;
+import org.skife.jdbi.v2.IDBI;
+import org.skife.jdbi.v2.Query;
+import org.skife.jdbi.v2.ResultIterator;
+import org.skife.jdbi.v2.exceptions.CallbackFailedException;
+import org.skife.jdbi.v2.exceptions.UnableToObtainConnectionException;
+import org.skife.jdbi.v2.sqlobject.stringtemplate.StringTemplate3StatementLocator;
+import org.skife.jdbi.v2.tweak.HandleCallback;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.ning.billing.usage.timeline.categories.CategoryIdAndMetric;
+import com.ning.billing.usage.timeline.chunks.TimelineChunk;
+import com.ning.billing.usage.timeline.chunks.TimelineChunkMapper;
+import com.ning.billing.usage.timeline.consumer.TimelineChunkConsumer;
+import com.ning.billing.usage.timeline.shutdown.StartTimes;
+import com.ning.billing.usage.timeline.sources.SourceIdAndMetricId;
+import com.ning.billing.usage.timeline.util.DateTimeUtils;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.BiMap;
+import com.google.common.collect.HashBiMap;
+import com.google.inject.Inject;
+
+public class DefaultTimelineDao implements TimelineDao {
+
+ private static final Logger log = LoggerFactory.getLogger(DefaultTimelineDao.class);
+ private static final Joiner JOINER = Joiner.on(",");
+
+ private final IDBI dbi;
+ private final TimelineChunkMapper timelineChunkMapper;
+ private final TimelineSqlDao delegate;
+
+ @Inject
+ public DefaultTimelineDao(final IDBI dbi) {
+ this.dbi = dbi;
+ this.timelineChunkMapper = new TimelineChunkMapper();
+ this.delegate = dbi.onDemand(TimelineSqlDao.class);
+ }
+
+ @Override
+ public String getSource(final Integer sourceId) throws UnableToObtainConnectionException, CallbackFailedException {
+ return delegate.getSource(sourceId);
+ }
+
+ @Override
+ public Integer getSourceId(final String source) throws UnableToObtainConnectionException, CallbackFailedException {
+ return delegate.getSourceId(source);
+ }
+
+ @Override
+ public BiMap<Integer, String> getSources() throws UnableToObtainConnectionException, CallbackFailedException {
+ final HashBiMap<Integer, String> accumulator = HashBiMap.create();
+ for (final Map<String, Object> metric : delegate.getSources()) {
+ accumulator.put(Integer.valueOf(metric.get("source_id").toString()), metric.get("source_name").toString());
+ }
+ return accumulator;
+ }
+
+ @Override
+ public synchronized int getOrAddSource(final String source) throws UnableToObtainConnectionException, CallbackFailedException {
+ delegate.begin();
+ delegate.addSource(source);
+ final Integer sourceId = delegate.getSourceId(source);
+ delegate.commit();
+
+ return sourceId;
+ }
+
+ @Override
+ public Integer getEventCategoryId(final String eventCategory) throws UnableToObtainConnectionException, CallbackFailedException {
+ return delegate.getEventCategoryId(eventCategory);
+ }
+
+ @Override
+ public String getEventCategory(final Integer eventCategoryId) throws UnableToObtainConnectionException, CallbackFailedException {
+ return delegate.getEventCategory(eventCategoryId);
+ }
+
+ @Override
+ public BiMap<Integer, String> getEventCategories() throws UnableToObtainConnectionException, CallbackFailedException {
+ final HashBiMap<Integer, String> accumulator = HashBiMap.create();
+ for (final Map<String, Object> eventCategory : delegate.getEventCategories()) {
+ accumulator.put(Integer.valueOf(eventCategory.get("event_category_id").toString()), eventCategory.get("event_category").toString());
+ }
+ return accumulator;
+ }
+
+ @Override
+ public synchronized int getOrAddEventCategory(final String eventCategory) throws UnableToObtainConnectionException, CallbackFailedException {
+ delegate.begin();
+ delegate.addEventCategory(eventCategory);
+ final Integer eventCategoryId = delegate.getEventCategoryId(eventCategory);
+ delegate.commit();
+
+ return eventCategoryId;
+ }
+
+ @Override
+ public Integer getMetricId(final int eventCategoryId, final String metric) throws UnableToObtainConnectionException, CallbackFailedException {
+ return delegate.getMetricId(eventCategoryId, metric);
+ }
+
+ @Override
+ public CategoryIdAndMetric getCategoryIdAndMetric(final Integer metricId) throws UnableToObtainConnectionException, CallbackFailedException {
+ return delegate.getEventCategoryIdAndMetric(metricId);
+ }
+
+ @Override
+ public BiMap<Integer, CategoryIdAndMetric> getMetrics() throws UnableToObtainConnectionException, CallbackFailedException {
+ final HashBiMap<Integer, CategoryIdAndMetric> accumulator = HashBiMap.create();
+ for (final Map<String, Object> metricInfo : delegate.getMetrics()) {
+ accumulator.put(Integer.valueOf(metricInfo.get("sample_kind_id").toString()),
+ new CategoryIdAndMetric((Integer) metricInfo.get("event_category_id"), metricInfo.get("sample_kind").toString()));
+ }
+ return accumulator;
+ }
+
+ @Override
+ public synchronized int getOrAddMetric(final Integer sourceId, final Integer eventCategoryId, final String metric) throws UnableToObtainConnectionException, CallbackFailedException {
+ delegate.begin();
+ delegate.addMetric(eventCategoryId, metric);
+ final Integer metricId = delegate.getMetricId(eventCategoryId, metric);
+ delegate.commit();
+
+ return metricId;
+ }
+
+ @Override
+ public Iterable<Integer> getMetricIdsBySourceId(final Integer sourceId) throws UnableToObtainConnectionException, CallbackFailedException {
+ return delegate.getMetricIdsBySourceId(sourceId);
+ }
+
+ @Override
+ public Iterable<SourceIdAndMetricId> getMetricIdsForAllSources() throws UnableToObtainConnectionException, CallbackFailedException {
+ return delegate.getMetricIdsForAllSources();
+ }
+
+ @Override
+ public Long insertTimelineChunk(final TimelineChunk timelineChunk) throws UnableToObtainConnectionException, CallbackFailedException {
+ delegate.begin();
+ delegate.insertTimelineChunk(timelineChunk);
+ final long timelineChunkId = delegate.getLastInsertedId();
+ delegate.commit();
+ return timelineChunkId;
+ }
+
+ @Override
+ public void getSamplesBySourceIdsAndMetricIds(final List<Integer> sourceIdList,
+ @Nullable final List<Integer> metricIdList,
+ final DateTime startTime,
+ final DateTime endTime,
+ final TimelineChunkConsumer chunkConsumer) {
+ dbi.withHandle(new HandleCallback<Void>() {
+ @Override
+ public Void withHandle(final Handle handle) throws Exception {
+ handle.setStatementLocator(new StringTemplate3StatementLocator(TimelineSqlDao.class));
+
+ ResultIterator<TimelineChunk> iterator = null;
+ try {
+ final Query<Map<String, Object>> query = handle
+ .createQuery("getSamplesBySourceIdsAndMetricIds")
+ .bind("startTime", DateTimeUtils.unixSeconds(startTime))
+ .bind("endTime", DateTimeUtils.unixSeconds(endTime))
+ .define("sourceIds", JOINER.join(sourceIdList));
+
+ if (metricIdList != null && !metricIdList.isEmpty()) {
+ query.define("metricIds", JOINER.join(metricIdList));
+ }
+
+ iterator = query
+ .map(timelineChunkMapper)
+ .iterator();
+
+ while (iterator.hasNext()) {
+ chunkConsumer.processTimelineChunk(iterator.next());
+ }
+ return null;
+ } finally {
+ if (iterator != null) {
+ try {
+ iterator.close();
+ } catch (Exception e) {
+ log.error("Exception closing TimelineChunkAndTimes iterator for sourceIds {} and metricIds {}", sourceIdList, metricIdList);
+ }
+ }
+ }
+ }
+ });
+ }
+
+ @Override
+ public Integer insertLastStartTimes(final StartTimes startTimes) {
+ return delegate.insertLastStartTimes(startTimes);
+ }
+
+ @Override
+ public StartTimes getLastStartTimes() {
+ return delegate.getLastStartTimes();
+ }
+
+ @Override
+ public void deleteLastStartTimes() {
+ delegate.deleteLastStartTimes();
+ }
+
+ @Override
+ public void test() throws UnableToObtainConnectionException, CallbackFailedException {
+ delegate.test();
+ }
+
+ @Override
+ public void bulkInsertSources(final List<String> sources) throws UnableToObtainConnectionException, CallbackFailedException {
+ delegate.bulkInsertSources(sources.iterator());
+ }
+
+ @Override
+ public void bulkInsertEventCategories(final List<String> categoryNames) throws UnableToObtainConnectionException, CallbackFailedException {
+ delegate.bulkInsertEventCategories(categoryNames.iterator());
+ }
+
+ @Override
+ public void bulkInsertMetrics(final List<CategoryIdAndMetric> categoryAndKinds) {
+ delegate.bulkInsertMetrics(categoryAndKinds.iterator());
+ }
+
+ @Override
+ public void bulkInsertTimelineChunks(final List<TimelineChunk> timelineChunkList) {
+ delegate.bulkInsertTimelineChunks(timelineChunkList.iterator());
+ }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/persistent/FileBackedBuffer.java b/usage/src/main/java/com/ning/billing/usage/timeline/persistent/FileBackedBuffer.java
new file mode 100644
index 0000000..9040b0d
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/persistent/FileBackedBuffer.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.persistent;
+
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.ning.billing.usage.timeline.sources.SourceSamplesForTimestamp;
+
+import com.fasterxml.jackson.core.JsonEncoding;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.dataformat.smile.SmileFactory;
+import com.fasterxml.jackson.dataformat.smile.SmileGenerator;
+import com.fasterxml.util.membuf.MemBuffersForBytes;
+import com.fasterxml.util.membuf.StreamyBytesMemBuffer;
+import com.google.common.annotations.VisibleForTesting;
+
+/**
+ * Backing buffer for a single TimelineSourceEventAccumulator that spools to disk
+ */
+public class FileBackedBuffer {
+
+ private static final Logger log = LoggerFactory.getLogger(FileBackedBuffer.class);
+
+ private static final SmileFactory smileFactory = new SmileFactory();
+ private static final ObjectMapper smileObjectMapper = new ObjectMapper(smileFactory);
+
+ static {
+ // Disable all magic for now as we don't write the Smile header (we share the same smileGenerator
+ // across multiple backend files)
+ smileFactory.configure(SmileGenerator.Feature.CHECK_SHARED_NAMES, false);
+ smileFactory.configure(SmileGenerator.Feature.CHECK_SHARED_STRING_VALUES, false);
+ }
+
+ private final String basePath;
+ private final String prefix;
+ private final boolean deleteFilesOnClose;
+ private final AtomicLong samplesforTimestampWritten = new AtomicLong();
+ private final Object recyclingMonitor = new Object();
+
+ private final StreamyBytesMemBuffer inputBuffer;
+ private StreamyBytesPersistentOutputStream out = null;
+ private SmileGenerator smileGenerator;
+
+ public FileBackedBuffer(final String basePath, final String prefix, final int segmentsSize, final int maxNbSegments) throws IOException {
+ this(basePath, prefix, true, segmentsSize, maxNbSegments);
+ }
+
+ public FileBackedBuffer(final String basePath, final String prefix, final boolean deleteFilesOnClose, final int segmentsSize, final int maxNbSegments) throws IOException {
+ this.basePath = basePath;
+ this.prefix = prefix;
+ this.deleteFilesOnClose = deleteFilesOnClose;
+
+ final MemBuffersForBytes bufs = new MemBuffersForBytes(segmentsSize, 1, maxNbSegments);
+ inputBuffer = bufs.createStreamyBuffer(1, maxNbSegments);
+
+ recycle();
+ }
+
+ public boolean append(final SourceSamplesForTimestamp sourceSamplesForTimestamp) {
+ try {
+ synchronized (recyclingMonitor) {
+ smileObjectMapper.writeValue(smileGenerator, sourceSamplesForTimestamp);
+ samplesforTimestampWritten.incrementAndGet();
+ return true;
+ }
+ } catch (IOException e) {
+ log.warn("Unable to backup samples", e);
+ return false;
+ }
+ }
+
+ /**
+ * Discard in-memory and on-disk data
+ */
+ public void discard() {
+ try {
+ recycle();
+ } catch (IOException e) {
+ log.warn("Exception discarding buffer", e);
+ }
+ }
+
+ private void recycle() throws IOException {
+ synchronized (recyclingMonitor) {
+ if (out != null && !out.isEmpty()) {
+ out.close();
+ }
+
+ out = new StreamyBytesPersistentOutputStream(basePath, prefix, inputBuffer, deleteFilesOnClose);
+ smileGenerator = smileFactory.createJsonGenerator(out, JsonEncoding.UTF8);
+ // Drop the Smile header
+ smileGenerator.flush();
+ out.reset();
+
+ samplesforTimestampWritten.set(0);
+ }
+ }
+
+ //@MonitorableManaged(description = "Return the approximate size of bytes on disk for samples not yet in the database", monitored = true, monitoringType = {MonitoringType.VALUE})
+ public long getBytesOnDisk() {
+ return out.getBytesOnDisk();
+ }
+
+ //@MonitorableManaged(description = "Return the approximate size of bytes in memory for samples not yet in the database", monitored = true, monitoringType = {MonitoringType.VALUE})
+ public long getBytesInMemory() {
+ return out.getBytesInMemory();
+ }
+
+ //@MonitorableManaged(description = "Return the approximate size of bytes available in memory (before spilling over to disk) for samples not yet in the database", monitored = true, monitoringType = {MonitoringType.VALUE})
+ public long getInMemoryAvailableSpace() {
+ return out.getInMemoryAvailableSpace();
+ }
+
+ @VisibleForTesting
+ public long getFilesCreated() {
+ return out.getCreatedFiles().size();
+ }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/persistent/Replayer.java b/usage/src/main/java/com/ning/billing/usage/timeline/persistent/Replayer.java
new file mode 100644
index 0000000..5896216
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/persistent/Replayer.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.persistent;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTime;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.ning.billing.usage.timeline.sources.SourceSamplesForTimestamp;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonToken;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.dataformat.smile.SmileFactory;
+import com.fasterxml.jackson.dataformat.smile.SmileParser;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Function;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Ordering;
+
+public class Replayer {
+
+ private static final Logger log = LoggerFactory.getLogger(Replayer.class);
+ private static final SmileFactory smileFactory = new SmileFactory();
+ private static final ObjectMapper smileMapper = new ObjectMapper(smileFactory);
+
+ static {
+ smileFactory.configure(SmileParser.Feature.REQUIRE_HEADER, false);
+ smileFactory.setCodec(smileMapper);
+ }
+
+ @VisibleForTesting
+ public static final Ordering<File> FILE_ORDERING = new Ordering<File>() {
+ @Override
+ public int compare(@Nullable final File left, @Nullable final File right) {
+ if (left == null || right == null) {
+ throw new NullPointerException();
+ }
+
+ // Order by the nano time
+ return left.getAbsolutePath().compareTo(right.getAbsolutePath());
+ }
+ };
+
+ private final String path;
+ private AtomicBoolean shuttingDown = new AtomicBoolean();
+
+ public Replayer(final String path) {
+ this.path = path;
+ }
+
+ // This method is only used by test code
+ public List<SourceSamplesForTimestamp> readAll() {
+ final List<SourceSamplesForTimestamp> samples = new ArrayList<SourceSamplesForTimestamp>();
+
+ readAll(true, null, new Function<SourceSamplesForTimestamp, Void>() {
+ @Override
+ public Void apply(@Nullable final SourceSamplesForTimestamp input) {
+ if (input != null) {
+ samples.add(input);
+ }
+ return null;
+ }
+ });
+
+ return samples;
+ }
+
+ public void initiateShutdown() {
+ shuttingDown.set(true);
+ }
+
+ public int readAll(final boolean deleteFiles, @Nullable final DateTime minStartTime, final Function<SourceSamplesForTimestamp, Void> fn) {
+ final Collection<File> files = ImmutableList.<File>copyOf(findCandidates());
+ int filesSkipped = 0;
+ for (final File file : FILE_ORDERING.sortedCopy(files)) {
+ try {
+ // Skip files whose last modification date is is earlier than the first start time.
+ if (minStartTime != null && file.lastModified() < minStartTime.getMillis()) {
+ filesSkipped++;
+ continue;
+ }
+ read(file, fn);
+ if (shuttingDown.get()) {
+ break;
+ }
+
+ if (deleteFiles) {
+ if (!file.delete()) {
+ log.warn("Unable to delete file: {}", file.getAbsolutePath());
+ }
+ }
+ } catch (IOException e) {
+ log.warn("Exception replaying file: {}", file.getAbsolutePath(), e);
+ }
+ }
+ return filesSkipped;
+ }
+
+ @VisibleForTesting
+ public void read(final File file, final Function<SourceSamplesForTimestamp, Void> fn) throws IOException {
+ final JsonParser smileParser = smileFactory.createJsonParser(file);
+ if (smileParser.nextToken() != JsonToken.START_ARRAY) {
+ return;
+ }
+
+ while (!shuttingDown.get() && smileParser.nextToken() != JsonToken.END_ARRAY) {
+ final SourceSamplesForTimestamp sourceSamplesForTimestamp = smileParser.readValueAs(SourceSamplesForTimestamp.class);
+ fn.apply(sourceSamplesForTimestamp);
+ }
+
+ smileParser.close();
+ }
+
+
+ public void purgeOldFiles(final DateTime purgeIfOlderDate) {
+ final File[] candidates = findCandidates();
+ for (final File file : candidates) {
+ if (file.lastModified() <= purgeIfOlderDate.getMillis()) {
+ if (!file.delete()) {
+ log.warn("Unable to delete file: {}", file.getAbsolutePath());
+ }
+ }
+ }
+ }
+
+ private File[] findCandidates() {
+ final File root = new File(path);
+ final FilenameFilter filter = new FilenameFilter() {
+ @Override
+ public boolean accept(final File file, final String s) {
+ return s.endsWith("bin");
+ }
+ };
+
+ return root.listFiles(filter);
+ }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/persistent/StreamyBytesPersistentOutputStream.java b/usage/src/main/java/com/ning/billing/usage/timeline/persistent/StreamyBytesPersistentOutputStream.java
new file mode 100644
index 0000000..1256583
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/persistent/StreamyBytesPersistentOutputStream.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.persistent;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.fasterxml.jackson.dataformat.smile.SmileConstants;
+import com.fasterxml.util.membuf.StreamyBytesMemBuffer;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.io.Files;
+
+public class StreamyBytesPersistentOutputStream extends OutputStream {
+
+ private static final Logger log = LoggerFactory.getLogger(StreamyBytesPersistentOutputStream.class);
+ private static final int BUF_SIZE = 0x1000; // 4K
+
+ private final String basePath;
+ private final String prefix;
+ private final StreamyBytesMemBuffer inputBuffer;
+ private final boolean deleteFilesOnClose;
+ private final List<String> createdFiles = new ArrayList<String>();
+
+ private long bytesOnDisk = 0L;
+
+ public StreamyBytesPersistentOutputStream(String basePath, final String prefix, final StreamyBytesMemBuffer inputBuffer, final boolean deleteFilesOnClose) {
+ if (!basePath.endsWith("/")) {
+ basePath += "/";
+ }
+ this.basePath = basePath;
+
+ this.prefix = prefix;
+ this.inputBuffer = inputBuffer;
+ this.deleteFilesOnClose = deleteFilesOnClose;
+ }
+
+ @Override
+ public void write(final int b) throws IOException {
+ final byte data = (byte) b;
+ write(new byte[]{data}, 0, 1);
+ }
+
+ @Override
+ public void write(final byte[] data, final int off, final int len) throws IOException {
+ if (!inputBuffer.tryAppend(data, off, len)) {
+ // Buffer full - need to flush
+ flushUnderlyingBufferAndReset();
+
+ if (!inputBuffer.tryAppend(data, off, len)) {
+ log.warn("Unable to append data: 1 byte lost");
+ }
+ }
+ }
+
+ @Override
+ public void close() throws IOException {
+ // Cleanup volatile data
+ inputBuffer.clear();
+
+ // Cleanup persistent data
+ if (deleteFilesOnClose) {
+ for (final String path : createdFiles) {
+ log.info("Discarding file: {}", path);
+ if (!new File(path).delete()) {
+ log.warn("Unable to discard file: {}", path);
+ }
+ }
+ }
+ }
+
+ private void flushUnderlyingBufferAndReset() {
+ synchronized (inputBuffer) {
+ if (inputBuffer.available() == 0) {
+ // Somebody beat you to it
+ return;
+ }
+
+ final String pathname = getFileName();
+ createdFiles.add(pathname);
+ log.debug("Flushing in-memory buffer to disk: {}", pathname);
+
+ try {
+ final File out = new File(pathname);
+ flushToFile(out);
+ } catch (IOException e) {
+ log.warn("Error flushing data", e);
+ } finally {
+ reset();
+ }
+ }
+ }
+
+ @VisibleForTesting
+ String getFileName() {
+ return basePath + "arecibo." + prefix + "." + System.nanoTime() + ".bin";
+ }
+
+ private void flushToFile(final File out) throws IOException {
+ final byte[] buf = new byte[BUF_SIZE];
+ FileOutputStream transfer = null;
+
+ int bytesTransferred = 0;
+ try {
+ transfer = Files.newOutputStreamSupplier(out).getOutput();
+
+ while (true) {
+ final int r = inputBuffer.readIfAvailable(buf);
+ if (r == 0) {
+ break;
+ }
+ transfer.write(buf, 0, r);
+ bytesTransferred += r;
+ }
+ } finally {
+ if (transfer != null) {
+ try {
+ transfer.write(SmileConstants.TOKEN_LITERAL_END_ARRAY);
+ bytesTransferred++;
+ bytesOnDisk += bytesTransferred;
+ } finally {
+ transfer.flush();
+ }
+ }
+ }
+ log.debug("Saved {} bytes to disk", bytesTransferred);
+ }
+
+ public void reset() {
+ inputBuffer.clear();
+ try {
+ write(SmileConstants.TOKEN_LITERAL_START_ARRAY);
+ } catch (IOException e) {
+ // Not sure how to recover?
+ }
+ }
+
+ public List<String> getCreatedFiles() {
+ return createdFiles;
+ }
+
+ public long getBytesOnDisk() {
+ return bytesOnDisk;
+ }
+
+ public long getBytesInMemory() {
+ return inputBuffer.getTotalPayloadLength();
+ }
+
+ public long getInMemoryAvailableSpace() {
+ return inputBuffer.getMaximumAvailableSpace();
+ }
+
+ public boolean isEmpty() {
+ return inputBuffer.isEmpty();
+ }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/persistent/TimelineDao.java b/usage/src/main/java/com/ning/billing/usage/timeline/persistent/TimelineDao.java
new file mode 100644
index 0000000..6cbdb34
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/persistent/TimelineDao.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.persistent;
+
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTime;
+import org.skife.jdbi.v2.exceptions.CallbackFailedException;
+import org.skife.jdbi.v2.exceptions.UnableToObtainConnectionException;
+
+import com.ning.billing.usage.timeline.categories.CategoryIdAndMetric;
+import com.ning.billing.usage.timeline.chunks.TimelineChunk;
+import com.ning.billing.usage.timeline.consumer.TimelineChunkConsumer;
+import com.ning.billing.usage.timeline.shutdown.StartTimes;
+import com.ning.billing.usage.timeline.sources.SourceIdAndMetricId;
+
+import com.google.common.collect.BiMap;
+
+public interface TimelineDao {
+
+ // Sources table
+
+ Integer getSourceId(String source) throws UnableToObtainConnectionException, CallbackFailedException;
+
+ String getSource(Integer sourceId) throws UnableToObtainConnectionException, CallbackFailedException;
+
+ BiMap<Integer, String> getSources() throws UnableToObtainConnectionException, CallbackFailedException;
+
+ int getOrAddSource(String source) throws UnableToObtainConnectionException, CallbackFailedException;
+
+ // Event categories table
+
+ Integer getEventCategoryId(String eventCategory) throws UnableToObtainConnectionException, CallbackFailedException;
+
+ String getEventCategory(Integer eventCategoryId) throws UnableToObtainConnectionException, CallbackFailedException;
+
+ BiMap<Integer, String> getEventCategories() throws UnableToObtainConnectionException, CallbackFailedException;
+
+ int getOrAddEventCategory(String eventCategory) throws UnableToObtainConnectionException, CallbackFailedException;
+
+ // Sample kinds table
+
+ Integer getMetricId(int eventCategory, String metric) throws UnableToObtainConnectionException, CallbackFailedException;
+
+ CategoryIdAndMetric getCategoryIdAndMetric(Integer metricId) throws UnableToObtainConnectionException, CallbackFailedException;
+
+ BiMap<Integer, CategoryIdAndMetric> getMetrics() throws UnableToObtainConnectionException, CallbackFailedException;
+
+ int getOrAddMetric(Integer sourceId, Integer eventCategoryId, String metric) throws UnableToObtainConnectionException, CallbackFailedException;
+
+ Iterable<Integer> getMetricIdsBySourceId(Integer sourceId) throws UnableToObtainConnectionException, CallbackFailedException;
+
+ Iterable<SourceIdAndMetricId> getMetricIdsForAllSources() throws UnableToObtainConnectionException, CallbackFailedException;
+
+ // Timelines tables
+
+ Long insertTimelineChunk(TimelineChunk timelineChunk) throws UnableToObtainConnectionException, CallbackFailedException;
+
+ void getSamplesBySourceIdsAndMetricIds(List<Integer> sourceIds,
+ @Nullable List<Integer> metricIds,
+ DateTime startTime,
+ DateTime endTime,
+ TimelineChunkConsumer chunkConsumer) throws UnableToObtainConnectionException, CallbackFailedException;
+
+ Integer insertLastStartTimes(StartTimes startTimes);
+
+ StartTimes getLastStartTimes();
+
+ void deleteLastStartTimes();
+
+ void bulkInsertSources(final List<String> sources) throws UnableToObtainConnectionException, CallbackFailedException;
+
+ void bulkInsertEventCategories(final List<String> categoryNames) throws UnableToObtainConnectionException, CallbackFailedException;
+
+ void bulkInsertMetrics(final List<CategoryIdAndMetric> categoryAndKinds);
+
+ void bulkInsertTimelineChunks(final List<TimelineChunk> timelineChunkList);
+
+ void test() throws UnableToObtainConnectionException, CallbackFailedException;
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/persistent/TimelineSqlDao.java b/usage/src/main/java/com/ning/billing/usage/timeline/persistent/TimelineSqlDao.java
new file mode 100644
index 0000000..ce6c5bd
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/persistent/TimelineSqlDao.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.persistent;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import org.skife.jdbi.v2.DefaultMapper;
+import org.skife.jdbi.v2.sqlobject.Bind;
+import org.skife.jdbi.v2.sqlobject.SqlBatch;
+import org.skife.jdbi.v2.sqlobject.SqlQuery;
+import org.skife.jdbi.v2.sqlobject.SqlUpdate;
+import org.skife.jdbi.v2.sqlobject.customizers.BatchChunkSize;
+import org.skife.jdbi.v2.sqlobject.customizers.Mapper;
+import org.skife.jdbi.v2.sqlobject.customizers.RegisterMapper;
+import org.skife.jdbi.v2.sqlobject.mixins.Transactional;
+import org.skife.jdbi.v2.sqlobject.mixins.Transmogrifier;
+import org.skife.jdbi.v2.sqlobject.stringtemplate.ExternalizedSqlViaStringTemplate3;
+
+import com.ning.billing.usage.timeline.categories.CategoryIdAndMetric;
+import com.ning.billing.usage.timeline.categories.CategoryIdAndMetricBinder;
+import com.ning.billing.usage.timeline.categories.CategoryIdAndMetricMapper;
+import com.ning.billing.usage.timeline.chunks.TimelineChunk;
+import com.ning.billing.usage.timeline.chunks.TimelineChunkBinder;
+import com.ning.billing.usage.timeline.shutdown.StartTimes;
+import com.ning.billing.usage.timeline.shutdown.StartTimesBinder;
+import com.ning.billing.usage.timeline.shutdown.StartTimesMapper;
+import com.ning.billing.usage.timeline.sources.SourceIdAndMetricId;
+import com.ning.billing.usage.timeline.sources.SourceIdAndMetricIdMapper;
+
+@ExternalizedSqlViaStringTemplate3()
+@RegisterMapper({CategoryIdAndMetricMapper.class, StartTimesMapper.class, SourceIdAndMetricIdMapper.class})
+public interface TimelineSqlDao extends Transactional<TimelineSqlDao>, Transmogrifier {
+
+ @SqlQuery
+ Integer getSourceId(@Bind("sourceName") final String source);
+
+ @SqlQuery
+ String getSource(@Bind("sourceId") final Integer sourceId);
+
+ @SqlQuery
+ @Mapper(DefaultMapper.class)
+ List<Map<String, Object>> getSources();
+
+ @SqlUpdate
+ void addSource(@Bind("sourceName") final String source);
+
+ @SqlBatch
+ @BatchChunkSize(1000)
+ void bulkInsertSources(@Bind("sourceName") Iterator<String> sourcesIterator);
+
+ @SqlQuery
+ Integer getEventCategoryId(@Bind("eventCategory") final String eventCategory);
+
+ @SqlQuery
+ String getEventCategory(@Bind("eventCategoryId") final Integer eventCategoryId);
+
+ @SqlUpdate
+ void addEventCategory(@Bind("eventCategory") final String eventCategory);
+
+ @SqlBatch
+ @BatchChunkSize(1000)
+ void bulkInsertEventCategories(@Bind("eventCategory") Iterator<String> cateogoryNames);
+
+ @SqlQuery
+ Iterable<Integer> getMetricIdsBySourceId(@Bind("sourceId") final Integer sourceId);
+
+ @SqlQuery
+ Iterable<SourceIdAndMetricId> getMetricIdsForAllSources();
+
+ @SqlQuery
+ Integer getMetricId(@Bind("eventCategoryId") final int eventCategoryId, @Bind("metric") final String metric);
+
+ @SqlQuery
+ CategoryIdAndMetric getEventCategoryIdAndMetric(@Bind("metricId") final Integer metricId);
+
+ @SqlUpdate
+ void addMetric(@Bind("eventCategoryId") final int eventCategoryId, @Bind("metric") final String metric);
+
+ @SqlBatch
+ @BatchChunkSize(1000)
+ void bulkInsertMetrics(@CategoryIdAndMetricBinder Iterator<CategoryIdAndMetric> categoriesAndMetrics);
+
+ @SqlQuery
+ @Mapper(DefaultMapper.class)
+ List<Map<String, Object>> getEventCategories();
+
+ @SqlQuery
+ @Mapper(DefaultMapper.class)
+ List<Map<String, Object>> getMetrics();
+
+ @SqlQuery
+ int getLastInsertedId();
+
+ @SqlQuery
+ long getHighestTimelineChunkId();
+
+ @SqlUpdate
+ void insertTimelineChunk(@TimelineChunkBinder final TimelineChunk timelineChunk);
+
+ @SqlBatch
+ @BatchChunkSize(1000)
+ void bulkInsertTimelineChunks(@TimelineChunkBinder Iterator<TimelineChunk> chunkIterator);
+
+ @SqlUpdate
+ Integer insertLastStartTimes(@StartTimesBinder final StartTimes startTimes);
+
+ @SqlQuery
+ StartTimes getLastStartTimes();
+
+ @SqlUpdate
+ void deleteLastStartTimes();
+
+ @SqlUpdate
+ void test();
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/samples/HalfFloat.java b/usage/src/main/java/com/ning/billing/usage/timeline/samples/HalfFloat.java
new file mode 100644
index 0000000..90f9e77
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/samples/HalfFloat.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.samples;
+
+public class HalfFloat {
+
+ private final short halfFloat;
+
+ public HalfFloat(final float input) {
+ halfFloat = (short)fromFloat(input);
+ }
+
+ public float getFloat() {
+ return toFloat((int)halfFloat);
+ }
+
+ // These two static methods were pinched from http://stackoverflow.com/questions/6162651/half-precision-floating-point-in-java/6162687#6162687
+ // The last comment on that page is the author saying "I hereby commit these to the public domain"
+
+ // Ignores the higher 16 bits
+ public static float toFloat(final int hbits) {
+ int mant = hbits & 0x03ff; // 10 bits mantissa
+ int exp = hbits & 0x7c00; // 5 bits exponent
+ if (exp == 0x7c00) { // NaN/Inf
+ exp = 0x3fc00; // -> NaN/Inf
+ }
+ else if (exp != 0) { // normalized value
+ exp += 0x1c000; // exp - 15 + 127
+ if(mant == 0 && exp > 0x1c400) { // smooth transition
+ return Float.intBitsToFloat((hbits & 0x8000) << 16 | exp << 13 | 0x3ff);
+ }
+ }
+ else if (mant != 0) { // && exp==0 -> subnormal
+ exp = 0x1c400; // make it normal
+ do {
+ mant <<= 1; // mantissa * 2
+ exp -= 0x400; // decrease exp by 1
+ } while ((mant & 0x400) == 0); // while not normal
+ mant &= 0x3ff; // discard subnormal bit
+ } // else +/-0 -> +/-0
+ return Float.intBitsToFloat( // combine all parts
+ (hbits & 0x8000) << 16 // sign << (31 - 15)
+ | (exp | mant) << 13); // value << (23 - 10)
+ }
+
+ // returns all higher 16 bits as 0 for all results
+ public static int fromFloat(final float fval) {
+ final int fbits = Float.floatToIntBits(fval);
+ final int sign = fbits >>> 16 & 0x8000; // sign only
+ int val = (fbits & 0x7fffffff) + 0x1000; // rounded value
+
+ if (val >= 0x47800000) { // might be or become NaN/Inf
+ if ((fbits & 0x7fffffff) >= 0x47800000) { // is or must become NaN/Inf
+
+ if (val < 0x7f800000) { // was value but too large
+ return sign | 0x7c00; // make it +/-Inf
+ }
+ return sign | 0x7c00 | // remains +/-Inf or NaN
+ (fbits & 0x007fffff) >>> 13; // keep NaN (and Inf) bits
+ }
+ return sign | 0x7bff; // unrounded not quite Inf
+ }
+ if(val >= 0x38800000) // remains normalized value
+ return sign | val - 0x38000000 >>> 13; // exp - 127 + 15
+ if(val < 0x33000000) // too small for subnormal
+ return sign; // becomes +/-0
+ val = (fbits & 0x7fffffff) >>> 23; // tmp exp for subnormal calc
+ return sign | ((fbits & 0x7fffff | 0x800000) // add subnormal bit
+ + (0x800000 >>> val - 102) >>> 126 - val); // round depending on cut off; div by 2^(1-(exp-127+15)) and >> 13 | exp=0
+ }
+
+ private static String describe(final int halfFloat) {
+ final int sign = (halfFloat >> 15) & 1;
+ final int exponent = (halfFloat >> 10) & 0x1f;
+ final double fraction = (double)(0x400 + (halfFloat & 0x3ff)) / (1.0 * 0x400);
+ final double product = fraction * Math.pow(2.0, exponent - 15) * (sign == 0 ? 1.0 : -1.0);
+ return String.format("HalfFloat %f, representation %x, sign %d, exponent 0x%x == 2**%d, fraction %f, product %f",
+ toFloat(halfFloat),
+ halfFloat,
+ sign,
+ exponent,
+ exponent - 15,
+ fraction,
+ product);
+ }
+
+ public static void main(String[] args) {
+ System.out.printf("%f double-converted = %f\n", 200.0, toFloat(fromFloat((float)200.0)));
+ System.out.printf("%s\n", describe(fromFloat(200.0f)));
+ }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/samples/NullSample.java b/usage/src/main/java/com/ning/billing/usage/timeline/samples/NullSample.java
new file mode 100644
index 0000000..efa99fa
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/samples/NullSample.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.samples;
+
+public class NullSample extends ScalarSample<Void> {
+
+ public NullSample() {
+ super(SampleOpcode.NULL, null);
+ }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/samples/RepeatSample.java b/usage/src/main/java/com/ning/billing/usage/timeline/samples/RepeatSample.java
new file mode 100644
index 0000000..990ef64
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/samples/RepeatSample.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.samples;
+
+/**
+ * A repeated value
+ *
+ * @param <T> A value consistent with the opcode
+ */
+public class RepeatSample<T> extends SampleBase {
+
+ public static final int MAX_BYTE_REPEAT_COUNT = 0xFF; // The maximum byte value
+ public static final int MAX_SHORT_REPEAT_COUNT = 0xFFFF; // The maximum short value
+
+ private final ScalarSample<T> sampleRepeated;
+
+ private int repeatCount;
+
+ public RepeatSample(final int repeatCount, final ScalarSample<T> sampleRepeated) {
+ super(SampleOpcode.REPEAT_BYTE);
+ this.repeatCount = repeatCount;
+ this.sampleRepeated = sampleRepeated;
+ }
+
+ public int getRepeatCount() {
+ return repeatCount;
+ }
+
+ public void incrementRepeatCount() {
+ repeatCount++;
+ }
+
+ public void incrementRepeatCount(final int addend) {
+ repeatCount += addend;
+ }
+
+ public ScalarSample<T> getSampleRepeated() {
+ return sampleRepeated;
+ }
+
+ @Override
+ public SampleOpcode getOpcode() {
+ return repeatCount > MAX_BYTE_REPEAT_COUNT ? SampleOpcode.REPEAT_SHORT : SampleOpcode.REPEAT_BYTE;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("RepeatSample");
+ sb.append("{sampleRepeated=").append(sampleRepeated);
+ sb.append(", repeatCount=").append(repeatCount);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final RepeatSample that = (RepeatSample) o;
+
+ if (repeatCount != that.repeatCount) {
+ return false;
+ }
+ if (sampleRepeated != null ? !sampleRepeated.equals(that.sampleRepeated) : that.sampleRepeated != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = sampleRepeated != null ? sampleRepeated.hashCode() : 0;
+ result = 31 * result + repeatCount;
+ return result;
+ }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/samples/SampleBase.java b/usage/src/main/java/com/ning/billing/usage/timeline/samples/SampleBase.java
new file mode 100644
index 0000000..25072e5
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/samples/SampleBase.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.samples;
+
+public abstract class SampleBase {
+
+ protected final SampleOpcode opcode;
+
+ public SampleBase(final SampleOpcode opcode) {
+ this.opcode = opcode;
+ }
+
+ public SampleOpcode getOpcode() {
+ return opcode;
+ }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/samples/SampleOpcode.java b/usage/src/main/java/com/ning/billing/usage/timeline/samples/SampleOpcode.java
new file mode 100644
index 0000000..a6823dd
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/samples/SampleOpcode.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.samples;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Opcode for samples encoding
+ */
+public enum SampleOpcode {
+ BYTE(1, 1),
+ SHORT(2, 2),
+ INT(3, 4),
+ LONG(4, 8),
+ FLOAT(5, 4),
+ DOUBLE(6, 8),
+ STRING(7, 0),
+ NULL(8, 0, true),
+ FLOAT_FOR_DOUBLE(10, 4, DOUBLE),
+ HALF_FLOAT_FOR_DOUBLE(11, 2, DOUBLE),
+ BYTE_FOR_DOUBLE(12, 1, DOUBLE),
+ SHORT_FOR_DOUBLE(13, 2, DOUBLE),
+ BIGINT(14, 0),
+ DOUBLE_ZERO(15, 0, true),
+ INT_ZERO(16, 0, true),
+ REPEAT_BYTE(0xff, 1, true), // A repeat operation in which the repeat count fits in an unsigned byte
+ REPEAT_SHORT(0xfe, 2, true); // A repeat operation in which the repeat count fits in an unsigned short
+
+ private static final Logger log = LoggerFactory.getLogger(SampleOpcode.class);
+
+ private int opcodeIndex;
+ private final int byteSize;
+ private final boolean repeater;
+ private final boolean noArgs;
+ private final SampleOpcode replacement;
+
+ private SampleOpcode(int opcodeIndex, int byteSize) {
+ this(opcodeIndex, byteSize, false);
+ }
+
+ private SampleOpcode(int opcodeIndex, int byteSize, boolean noArgs) {
+ this(opcodeIndex, byteSize, noArgs, false);
+ }
+
+ private SampleOpcode(int opcodeIndex, int byteSize, boolean noArgs, boolean repeater) {
+ this.opcodeIndex = opcodeIndex;
+ this.byteSize = byteSize;
+ this.noArgs = noArgs;
+ this.repeater = repeater;
+ this.replacement = this;
+ }
+
+ private SampleOpcode(int opcodeIndex, int byteSize, SampleOpcode replacement) {
+ this(opcodeIndex, byteSize, false, false, replacement);
+ }
+
+ private SampleOpcode(final int opcodeIndex, final int byteSize, final boolean noArgs, final boolean repeater, final SampleOpcode replacement) {
+ this.opcodeIndex = opcodeIndex;
+ this.byteSize = byteSize;
+ this.noArgs = noArgs;
+ this.repeater = repeater;
+ this.replacement = replacement;
+ }
+
+ public static SampleOpcode getOpcodeFromIndex(final int index) {
+ for (SampleOpcode opcode : values()) {
+ if (opcode.getOpcodeIndex() == index) {
+ return opcode;
+ }
+ }
+
+ final String s = String.format("In SampleOpcode.getOpcodefromIndex(), could not find opcode for index %d", index);
+ log.error(s);
+ throw new IllegalArgumentException(s);
+ }
+
+ public int getOpcodeIndex() {
+ return opcodeIndex;
+ }
+
+ public int getByteSize() {
+ return byteSize;
+ }
+
+ public boolean getNoArgs() {
+ return noArgs;
+ }
+
+ public boolean getRepeater() {
+ return repeater;
+ }
+
+ public SampleOpcode getReplacement() {
+ return replacement;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("SampleOpcode");
+ sb.append("{opcodeIndex=").append(opcodeIndex);
+ sb.append(", byteSize=").append(byteSize);
+ sb.append(", repeater=").append(repeater);
+ sb.append(", noArgs=").append(noArgs);
+ sb.append(", replacement=").append(replacement.name());
+ sb.append('}');
+ return sb.toString();
+ }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/samples/ScalarSample.java b/usage/src/main/java/com/ning/billing/usage/timeline/samples/ScalarSample.java
new file mode 100644
index 0000000..a4f3173
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/samples/ScalarSample.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.samples;
+
+import java.lang.reflect.InvocationTargetException;
+import java.math.BigInteger;
+import java.util.Map;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonValue;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.primitives.Ints;
+import com.google.common.primitives.Shorts;
+
+/**
+ * A sample value associated with its opcode
+ *
+ * @param <T> A value consistent with the opcode
+ */
+public class ScalarSample<T> extends SampleBase {
+
+ private static final String KEY_OPCODE = "O";
+ private static final String KEY_SAMPLE_CLASS = "K";
+ private static final String KEY_SAMPLE_VALUE = "V";
+
+ private final T sampleValue;
+
+ public static ScalarSample fromObject(final Object sampleValue) {
+ if (sampleValue == null) {
+ return new ScalarSample<Void>(SampleOpcode.NULL, null);
+ } else if (sampleValue instanceof Byte) {
+ return new ScalarSample<Byte>(SampleOpcode.BYTE, (Byte) sampleValue);
+ } else if (sampleValue instanceof Short) {
+ return new ScalarSample<Short>(SampleOpcode.SHORT, (Short) sampleValue);
+ } else if (sampleValue instanceof Integer) {
+ try {
+ // Can it fit in a short?
+ final short optimizedShort = Shorts.checkedCast(Long.valueOf(sampleValue.toString()));
+ return new ScalarSample<Short>(SampleOpcode.SHORT, optimizedShort);
+ } catch (IllegalArgumentException e) {
+ return new ScalarSample<Integer>(SampleOpcode.INT, (Integer) sampleValue);
+ }
+ } else if (sampleValue instanceof Long) {
+ try {
+ // Can it fit in a short?
+ final short optimizedShort = Shorts.checkedCast(Long.valueOf(sampleValue.toString()));
+ return new ScalarSample<Short>(SampleOpcode.SHORT, optimizedShort);
+ } catch (IllegalArgumentException e) {
+ try {
+ // Can it fit in an int?
+ final int optimizedLong = Ints.checkedCast(Long.valueOf(sampleValue.toString()));
+ return new ScalarSample<Integer>(SampleOpcode.INT, optimizedLong);
+ } catch (IllegalArgumentException ohWell) {
+ return new ScalarSample<Long>(SampleOpcode.LONG, (Long) sampleValue);
+ }
+ }
+ } else if (sampleValue instanceof Float) {
+ return new ScalarSample<Float>(SampleOpcode.FLOAT, (Float) sampleValue);
+ } else if (sampleValue instanceof Double) {
+ return new ScalarSample<Double>(SampleOpcode.DOUBLE, (Double) sampleValue);
+ } else {
+ return new ScalarSample<String>(SampleOpcode.STRING, sampleValue.toString());
+ }
+ }
+
+ public ScalarSample(final SampleOpcode opcode, final T sampleValue) {
+ super(opcode);
+ this.sampleValue = sampleValue;
+ }
+
+ public ScalarSample(final String opcode, final T sampleValue) {
+ this(SampleOpcode.valueOf(opcode), sampleValue);
+ }
+
+ public double getDoubleValue() {
+ final Object sampleValue = getSampleValue();
+ return getDoubleValue(getOpcode(), sampleValue);
+ }
+
+ public static double getDoubleValue(final SampleOpcode opcode, final Object sampleValue) {
+ switch (opcode) {
+ case NULL:
+ case DOUBLE_ZERO:
+ case INT_ZERO:
+ return 0.0;
+ case BYTE:
+ case BYTE_FOR_DOUBLE:
+ return (double) ((Byte) sampleValue);
+ case SHORT:
+ case SHORT_FOR_DOUBLE:
+ return (double) ((Short) sampleValue);
+ case INT:
+ return (double) ((Integer) sampleValue);
+ case LONG:
+ return (double) ((Long) sampleValue);
+ case FLOAT:
+ case FLOAT_FOR_DOUBLE:
+ return (double) ((Float) sampleValue);
+ case HALF_FLOAT_FOR_DOUBLE:
+ return (double) HalfFloat.toFloat((Short) sampleValue);
+ case DOUBLE:
+ return (Double) sampleValue;
+ case BIGINT:
+ return ((BigInteger) sampleValue).doubleValue();
+ default:
+ throw new IllegalArgumentException(String.format("In getDoubleValue(), sample opcode is %s, sample value is %s",
+ opcode.name(), String.valueOf(sampleValue)));
+ }
+ }
+
+ @JsonCreator
+ public ScalarSample(@JsonProperty(KEY_OPCODE) final byte opcodeIdx,
+ @JsonProperty(KEY_SAMPLE_CLASS) final Class klass,
+ @JsonProperty(KEY_SAMPLE_VALUE) final T sampleValue) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException {
+ super(SampleOpcode.getOpcodeFromIndex(opcodeIdx));
+ // Numerical classes have a String constructor
+ this.sampleValue = (T) klass.getConstructor(String.class).newInstance(sampleValue.toString());
+ }
+
+ @JsonValue
+ public Map<String, Object> toMap() {
+ // Work around type erasure by storing explicitly the sample class. This avoid deserializing shorts as integers
+ // at replay time for instance
+ return ImmutableMap.of(KEY_OPCODE, opcode.getOpcodeIndex(), KEY_SAMPLE_CLASS, sampleValue.getClass(), KEY_SAMPLE_VALUE, sampleValue);
+ }
+
+ public T getSampleValue() {
+ return sampleValue;
+ }
+
+ @Override
+ public String toString() {
+ return sampleValue.toString();
+ }
+
+ @Override
+ public boolean equals(final Object other) {
+ if (other == null || !(other instanceof SampleBase)) {
+ return false;
+ }
+ final ScalarSample otherSample = (ScalarSample) other;
+ final Object otherValue = otherSample.getSampleValue();
+ if (getOpcode() != otherSample.getOpcode()) {
+ return false;
+ } else if (!opcode.getNoArgs() && !(sameSampleValues(sampleValue, otherValue))) {
+ return false;
+ }
+ return true;
+ }
+
+ public static boolean sameSampleValues(final Object o1, final Object o2) {
+ if (o1 == o2) {
+ return true;
+ } else if (o1.getClass() == o2.getClass()) {
+ return o1.equals(o2);
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return sampleValue != null ? sampleValue.hashCode() : 0;
+ }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/shutdown/ShutdownSaveMode.java b/usage/src/main/java/com/ning/billing/usage/timeline/shutdown/ShutdownSaveMode.java
new file mode 100644
index 0000000..9ce0ebe
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/shutdown/ShutdownSaveMode.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.shutdown;
+
+public enum ShutdownSaveMode {
+ SAVE_ALL_TIMELINES, // Save all timelines in the db
+ SAVE_START_TIMES; // Save just the start times for each timeline, and use the replay facility to reconstruct the accumulators on startup
+
+ public static ShutdownSaveMode fromString(final String mode) {
+ for (final ShutdownSaveMode s : ShutdownSaveMode.values()) {
+ if (s.name().equalsIgnoreCase(mode)) {
+ return s;
+ }
+ }
+ throw new IllegalArgumentException(String.format("The argument %s was supposed to be a ShutdownSaveMode, but was not", mode));
+ }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/shutdown/StartTimes.java b/usage/src/main/java/com/ning/billing/usage/timeline/shutdown/StartTimes.java
new file mode 100644
index 0000000..7e1bc9d
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/shutdown/StartTimes.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.shutdown;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.joda.time.DateTime;
+
+/**
+ * This class is used solely as a Json mapping class when saving timelines in a database
+ * blob on shutdown, and restoring them on startup.
+ * <p/>
+ * The Map<Integer, Map<Integer, DateTime>> maps from sourceId to eventCategoryId to startTime.
+ */
+public class StartTimes {
+
+ private final DateTime timeInserted;
+ private final Map<Integer, Map<Integer, DateTime>> startTimesMap;
+ private DateTime minStartTime;
+
+ public StartTimes(final DateTime timeInserted, final Map<Integer, Map<Integer, DateTime>> startTimesMap) {
+ this.timeInserted = timeInserted;
+ this.startTimesMap = startTimesMap;
+ DateTime minDateTime = new DateTime(Long.MAX_VALUE);
+ for (final Map<Integer, DateTime> categoryMap : startTimesMap.values()) {
+ for (final DateTime startTime : categoryMap.values()) {
+ if (minDateTime.isAfter(startTime)) {
+ minDateTime = startTime;
+ }
+ }
+ }
+ this.minStartTime = minDateTime;
+ }
+
+ public StartTimes() {
+ this.timeInserted = new DateTime();
+ minStartTime = new DateTime(Long.MAX_VALUE);
+ this.startTimesMap = new HashMap<Integer, Map<Integer, DateTime>>();
+ }
+
+ public void addTime(final int sourceId, final int categoryId, final DateTime dateTime) {
+ Map<Integer, DateTime> sourceTimes = startTimesMap.get(sourceId);
+ if (sourceTimes == null) {
+ sourceTimes = new HashMap<Integer, DateTime>();
+ startTimesMap.put(sourceId, sourceTimes);
+ }
+ sourceTimes.put(categoryId, dateTime);
+ if (dateTime.isBefore(minStartTime)) {
+ minStartTime = dateTime;
+ }
+ }
+
+ public DateTime getStartTimeForSourceIdAndCategoryId(final int sourceId, final int categoryId) {
+ final Map<Integer, DateTime> sourceTimes = startTimesMap.get(sourceId);
+ if (sourceTimes != null) {
+ return sourceTimes.get(categoryId);
+ } else {
+ return null;
+ }
+ }
+
+ public Map<Integer, Map<Integer, DateTime>> getStartTimesMap() {
+ return startTimesMap;
+ }
+
+ public DateTime getTimeInserted() {
+ return timeInserted;
+ }
+
+ public DateTime getMinStartTime() {
+ return minStartTime;
+ }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/shutdown/StartTimesBinder.java b/usage/src/main/java/com/ning/billing/usage/timeline/shutdown/StartTimesBinder.java
new file mode 100644
index 0000000..aa174a3
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/shutdown/StartTimesBinder.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.shutdown;
+
+import java.io.IOException;
+import java.lang.annotation.Annotation;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.skife.jdbi.v2.SQLStatement;
+import org.skife.jdbi.v2.sqlobject.Binder;
+import org.skife.jdbi.v2.sqlobject.BinderFactory;
+import org.skife.jdbi.v2.sqlobject.BindingAnnotation;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.ning.billing.usage.timeline.shutdown.StartTimesBinder.StartTimesBinderFactory;
+import com.ning.billing.usage.timeline.util.DateTimeUtils;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+@BindingAnnotation(StartTimesBinderFactory.class)
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.PARAMETER})
+public @interface StartTimesBinder {
+
+ public static class StartTimesBinderFactory implements BinderFactory {
+
+ private static final Logger log = LoggerFactory.getLogger(StartTimesBinderFactory.class);
+ private static final ObjectMapper mapper = new ObjectMapper();
+
+ public Binder build(final Annotation annotation) {
+ return new Binder<StartTimesBinder, StartTimes>() {
+ public void bind(final SQLStatement query, final StartTimesBinder binder, final StartTimes startTimes) {
+ try {
+ final String s = mapper.writeValueAsString(startTimes.getStartTimesMap());
+ query.bind("startTimes", s)
+ .bind("timeInserted", DateTimeUtils.unixSeconds(startTimes.getTimeInserted()));
+ } catch (IOException e) {
+ log.error("Exception while binding StartTimes", e);
+ }
+ }
+ };
+ }
+ }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/shutdown/StartTimesMapper.java b/usage/src/main/java/com/ning/billing/usage/timeline/shutdown/StartTimesMapper.java
new file mode 100644
index 0000000..59d4b25
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/shutdown/StartTimesMapper.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.shutdown;
+
+import java.io.IOException;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.Map;
+
+import org.joda.time.DateTime;
+import org.skife.jdbi.v2.StatementContext;
+import org.skife.jdbi.v2.tweak.ResultSetMapper;
+
+import com.ning.billing.usage.timeline.util.DateTimeUtils;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+public class StartTimesMapper implements ResultSetMapper<StartTimes> {
+
+ private static final ObjectMapper mapper = new ObjectMapper();
+
+ @Override
+ public StartTimes map(final int index, final ResultSet r, final StatementContext ctx) throws SQLException {
+ try {
+ return new StartTimes(DateTimeUtils.dateTimeFromUnixSeconds(r.getInt("time_inserted")),
+ (Map<Integer, Map<Integer, DateTime>>) mapper.readValue(r.getBlob("start_times").getBinaryStream(), new TypeReference<Map<Integer, Map<Integer, DateTime>>>() {
+ }));
+ } catch (IOException e) {
+ throw new IllegalStateException(String.format("Could not decode the StartTimes map"), e);
+ }
+ }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/sources/SourceAndId.java b/usage/src/main/java/com/ning/billing/usage/timeline/sources/SourceAndId.java
new file mode 100644
index 0000000..424f8be
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/sources/SourceAndId.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.sources;
+
+/**
+ * This class represents one row in the sources table
+ */
+public class SourceAndId {
+
+ private final String source;
+ private final int sourceId;
+
+ public SourceAndId(final String source, final int sourceId) {
+ this.source = source;
+ this.sourceId = sourceId;
+ }
+
+ public String getSource() {
+ return source;
+ }
+
+ public int getSourceId() {
+ return sourceId;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("SourceAndId");
+ sb.append("{source='").append(source).append('\'');
+ sb.append(", sourceId=").append(sourceId);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final SourceAndId that = (SourceAndId) o;
+
+ if (sourceId != that.sourceId) {
+ return false;
+ }
+ if (source != null ? !source.equals(that.source) : that.source != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = source != null ? source.hashCode() : 0;
+ result = 31 * result + sourceId;
+ return result;
+ }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/sources/SourceIdAndMetricId.java b/usage/src/main/java/com/ning/billing/usage/timeline/sources/SourceIdAndMetricId.java
new file mode 100644
index 0000000..087eb45
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/sources/SourceIdAndMetricId.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.sources;
+
+public class SourceIdAndMetricId {
+
+ private final int sourceId;
+ private final int metricId;
+
+ public SourceIdAndMetricId(final int sourceId, final int metricId) {
+ this.sourceId = sourceId;
+ this.metricId = metricId;
+ }
+
+ public int getSourceId() {
+ return sourceId;
+ }
+
+ public int getMetricId() {
+ return metricId;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("SourceIdAndMetricId");
+ sb.append("{sourceId=").append(sourceId);
+ sb.append(", metricId=").append(metricId);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final SourceIdAndMetricId that = (SourceIdAndMetricId) o;
+
+ if (metricId != that.metricId) {
+ return false;
+ }
+ if (sourceId != that.sourceId) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = sourceId;
+ result = 31 * result + metricId;
+ return result;
+ }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/sources/SourceIdAndMetricIdMapper.java b/usage/src/main/java/com/ning/billing/usage/timeline/sources/SourceIdAndMetricIdMapper.java
new file mode 100644
index 0000000..2959bd5
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/sources/SourceIdAndMetricIdMapper.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.sources;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+import org.skife.jdbi.v2.StatementContext;
+import org.skife.jdbi.v2.tweak.ResultSetMapper;
+
+public class SourceIdAndMetricIdMapper implements ResultSetMapper<SourceIdAndMetricId> {
+
+ @Override
+ public SourceIdAndMetricId map(final int index, final ResultSet rs, final StatementContext ctx) throws SQLException {
+ return new SourceIdAndMetricId(rs.getInt("source_id"), rs.getInt("sample_kind_id"));
+ }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/sources/SourceMapper.java b/usage/src/main/java/com/ning/billing/usage/timeline/sources/SourceMapper.java
new file mode 100644
index 0000000..1e7f218
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/sources/SourceMapper.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.sources;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+import org.skife.jdbi.v2.StatementContext;
+import org.skife.jdbi.v2.tweak.ResultSetMapper;
+
+public class SourceMapper implements ResultSetMapper<SourceAndId> {
+
+ @Override
+ public SourceAndId map(final int index, final ResultSet r, final StatementContext ctx) throws SQLException {
+ return new SourceAndId(r.getString("source"), r.getInt("id"));
+ }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/sources/SourceSamplesForTimestamp.java b/usage/src/main/java/com/ning/billing/usage/timeline/sources/SourceSamplesForTimestamp.java
new file mode 100644
index 0000000..62dee6b
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/sources/SourceSamplesForTimestamp.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.sources;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.joda.time.DateTime;
+
+import com.ning.billing.usage.timeline.samples.ScalarSample;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonValue;
+import com.google.common.collect.ImmutableMap;
+
+/**
+ * Instances of this class represent samples sent from one source and one
+ * category, e.g., JVM, representing one point in time.
+ */
+@SuppressWarnings("unchecked")
+public class SourceSamplesForTimestamp {
+
+ private static final String KEY_SOURCE = "H";
+ private static final String KEY_CATEGORY = "V";
+ private static final String KEY_TIMESTAMP = "T";
+ private static final String KEY_SAMPLES = "S";
+
+ private final Integer sourceId;
+ private final String category;
+ private final DateTime timestamp;
+ // A map from sample id to sample value for that timestamp
+ private final Map<Integer, ScalarSample> samples;
+
+ public SourceSamplesForTimestamp(final int sourceId, final String category, final DateTime timestamp) {
+ this(sourceId, category, timestamp, new HashMap<Integer, ScalarSample>());
+ }
+
+ @JsonCreator
+ public SourceSamplesForTimestamp(@JsonProperty(KEY_SOURCE) final Integer sourceId, @JsonProperty(KEY_CATEGORY) final String category,
+ @JsonProperty(KEY_TIMESTAMP) final DateTime timestamp, @JsonProperty(KEY_SAMPLES) final Map<Integer, ScalarSample> samples) {
+ this.sourceId = sourceId;
+ this.category = category;
+ this.timestamp = timestamp;
+ this.samples = samples;
+ }
+
+ public int getSourceId() {
+ return sourceId;
+ }
+
+ public String getCategory() {
+ return category;
+ }
+
+ public DateTime getTimestamp() {
+ return timestamp;
+ }
+
+ public Map<Integer, ScalarSample> getSamples() {
+ return samples;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("SourceSamplesForTimestamp");
+ sb.append("{category='").append(category).append('\'');
+ sb.append(", sourceId=").append(sourceId);
+ sb.append(", timestamp=").append(timestamp);
+ sb.append(", samples=").append(samples);
+ sb.append('}');
+
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final SourceSamplesForTimestamp that = (SourceSamplesForTimestamp) o;
+
+ if (category != null ? !category.equals(that.category) : that.category != null) {
+ return false;
+ }
+ if (samples != null ? !samples.equals(that.samples) : that.samples != null) {
+ return false;
+ }
+ if (sourceId != null ? !sourceId.equals(that.sourceId) : that.sourceId != null) {
+ return false;
+ }
+ if (timestamp != null ? !timestamp.equals(that.timestamp) : that.timestamp != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = sourceId != null ? sourceId.hashCode() : 0;
+ result = 31 * result + (category != null ? category.hashCode() : 0);
+ result = 31 * result + (timestamp != null ? timestamp.hashCode() : 0);
+ result = 31 * result + (samples != null ? samples.hashCode() : 0);
+ return result;
+ }
+
+ @JsonValue
+ public Map<String, Object> toMap() {
+ return ImmutableMap.of(KEY_SOURCE, sourceId, KEY_CATEGORY, category, KEY_TIMESTAMP, timestamp, KEY_SAMPLES, samples);
+ }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/TimelineEventHandler.java b/usage/src/main/java/com/ning/billing/usage/timeline/TimelineEventHandler.java
new file mode 100644
index 0000000..f36c1df
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/TimelineEventHandler.java
@@ -0,0 +1,577 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTime;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.ning.billing.config.UsageConfig;
+import com.ning.billing.usage.timeline.chunks.TimelineChunk;
+import com.ning.billing.usage.timeline.codec.SampleCoder;
+import com.ning.billing.usage.timeline.codec.TimelineChunkAccumulator;
+import com.ning.billing.usage.timeline.persistent.FileBackedBuffer;
+import com.ning.billing.usage.timeline.persistent.Replayer;
+import com.ning.billing.usage.timeline.persistent.TimelineDao;
+import com.ning.billing.usage.timeline.samples.ScalarSample;
+import com.ning.billing.usage.timeline.shutdown.ShutdownSaveMode;
+import com.ning.billing.usage.timeline.shutdown.StartTimes;
+import com.ning.billing.usage.timeline.sources.SourceSamplesForTimestamp;
+import com.ning.billing.usage.timeline.times.TimelineCoder;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Function;
+import com.google.common.collect.ImmutableList;
+import com.google.inject.Inject;
+
+public class TimelineEventHandler {
+
+ private static final Logger log = LoggerFactory.getLogger(TimelineEventHandler.class);
+ private final ScheduledExecutorService purgeThread = Executors.newSingleThreadScheduledExecutor();
+ private static final Comparator<TimelineChunk> CHUNK_COMPARATOR = new Comparator<TimelineChunk>() {
+
+ @Override
+ public int compare(final TimelineChunk o1, final TimelineChunk o2) {
+ final int hostDiff = o1.getSourceId() - o1.getSourceId();
+ if (hostDiff < 0) {
+ return -1;
+ } else if (hostDiff > 0) {
+ return 1;
+ } else {
+ final int metricIdDiff = o1.getMetricId() - o2.getMetricId();
+ if (metricIdDiff < 0) {
+ return -1;
+ } else if (metricIdDiff > 0) {
+ return 1;
+ } else {
+ final long startTimeDiff = o1.getStartTime().getMillis() - o2.getStartTime().getMillis();
+ if (startTimeDiff < 0) {
+ return -1;
+ } else if (startTimeDiff > 0) {
+ return 1;
+ } else {
+ return 0;
+ }
+ }
+ }
+ }
+ };
+
+ // A TimelineSourceEventAccumulator records attributes for a specific host and event type.
+ // This cache maps sourceId -> categoryId -> accumulator
+ //
+ // TODO: There are still timing windows in the use of accumulators. Enumerate them and
+ // either fix them or prove they are benign
+ private final Map<Integer, SourceAccumulatorsAndUpdateDate> accumulators = new ConcurrentHashMap<Integer, SourceAccumulatorsAndUpdateDate>();
+
+ private final UsageConfig config;
+ private final TimelineDao timelineDAO;
+ private final TimelineCoder timelineCoder;
+ private final SampleCoder sampleCoder;
+ private final BackgroundDBChunkWriter backgroundWriter;
+ private final FileBackedBuffer backingBuffer;
+
+ private final ShutdownSaveMode shutdownSaveMode;
+ private final AtomicBoolean shuttingDown = new AtomicBoolean();
+ private final AtomicBoolean replaying = new AtomicBoolean();
+
+ private final AtomicLong eventsDiscarded = new AtomicLong(0L);
+ private final AtomicLong eventsReceivedAfterShuttingDown = new AtomicLong();
+ private final AtomicLong handledEventCount = new AtomicLong();
+ private final AtomicLong addedSourceEventAccumulatorMapCount = new AtomicLong();
+ private final AtomicLong addedSourceEventAccumulatorCount = new AtomicLong();
+ private final AtomicLong getInMemoryChunksCallCount = new AtomicLong();
+ private final AtomicLong accumulatorDeepCopyCount = new AtomicLong();
+ private final AtomicLong inMemoryChunksReturnedCount = new AtomicLong();
+ private final AtomicLong replayCount = new AtomicLong();
+ private final AtomicLong replaySamplesFoundCount = new AtomicLong();
+ private final AtomicLong replaySamplesOutsideTimeRangeCount = new AtomicLong();
+ private final AtomicLong replaySamplesProcessedCount = new AtomicLong();
+ private final AtomicLong forceCommitCallCount = new AtomicLong();
+ private final AtomicLong purgedAccumsBecauseSourceNotUpdated = new AtomicLong();
+ private final AtomicLong purgedAccumsBecauseCategoryNotUpdated = new AtomicLong();
+
+ @Inject
+ public TimelineEventHandler(final UsageConfig config, final TimelineDao timelineDAO, final TimelineCoder timelineCoder, final SampleCoder sampleCoder, final BackgroundDBChunkWriter backgroundWriter, final FileBackedBuffer fileBackedBuffer) {
+ this.config = config;
+ this.timelineDAO = timelineDAO;
+ this.timelineCoder = timelineCoder;
+ this.sampleCoder = sampleCoder;
+ this.backgroundWriter = backgroundWriter;
+ this.backingBuffer = fileBackedBuffer;
+ this.shutdownSaveMode = ShutdownSaveMode.fromString(config.getShutdownSaveMode());
+ }
+
+ private void saveAccumulators() {
+ for (final Map.Entry<Integer, SourceAccumulatorsAndUpdateDate> entry : accumulators.entrySet()) {
+ final int sourceId = entry.getKey();
+ final Map<Integer, TimelineSourceEventAccumulator> hostAccumulators = entry.getValue().getCategoryAccumulators();
+ for (final Map.Entry<Integer, TimelineSourceEventAccumulator> accumulatorEntry : hostAccumulators.entrySet()) {
+ final int categoryId = accumulatorEntry.getKey();
+ final TimelineSourceEventAccumulator accumulator = accumulatorEntry.getValue();
+ log.debug("Saving Timeline for sourceId [{}] and categoryId [{}]", sourceId, categoryId);
+ accumulator.extractAndQueueTimelineChunks();
+ }
+ }
+ }
+
+ private void saveStartTimes(final StartTimes startTimes) {
+ for (final Map.Entry<Integer, SourceAccumulatorsAndUpdateDate> entry : accumulators.entrySet()) {
+ final int sourceId = entry.getKey();
+ final Map<Integer, TimelineSourceEventAccumulator> hostAccumulators = entry.getValue().getCategoryAccumulators();
+ for (final Map.Entry<Integer, TimelineSourceEventAccumulator> accumulatorEntry : hostAccumulators.entrySet()) {
+ final int categoryId = accumulatorEntry.getKey();
+ final TimelineSourceEventAccumulator accumulator = accumulatorEntry.getValue();
+ log.debug("Saving Timeline start time for sourceId [{}] and category [{}]", sourceId, categoryId);
+ startTimes.addTime(sourceId, categoryId, accumulator.getStartTime());
+ }
+ }
+ }
+
+ public synchronized void purgeOldSourcesAndAccumulators(final DateTime purgeIfBeforeDate) {
+ final List<Integer> oldSourceIds = new ArrayList<Integer>();
+ for (final Map.Entry<Integer, SourceAccumulatorsAndUpdateDate> entry : accumulators.entrySet()) {
+ final int sourceId = entry.getKey();
+ final SourceAccumulatorsAndUpdateDate accumulatorsAndDate = entry.getValue();
+ final DateTime lastUpdatedDate = accumulatorsAndDate.getLastUpdateDate();
+ if (lastUpdatedDate.isBefore(purgeIfBeforeDate)) {
+ oldSourceIds.add(sourceId);
+ purgedAccumsBecauseSourceNotUpdated.incrementAndGet();
+ for (final TimelineSourceEventAccumulator categoryAccumulator : accumulatorsAndDate.getCategoryAccumulators().values()) {
+ categoryAccumulator.extractAndQueueTimelineChunks();
+ }
+ } else {
+ final List<Integer> categoryIdsToPurge = new ArrayList<Integer>();
+ final Map<Integer, TimelineSourceEventAccumulator> categoryMap = accumulatorsAndDate.getCategoryAccumulators();
+ for (final Map.Entry<Integer, TimelineSourceEventAccumulator> eventEntry : categoryMap.entrySet()) {
+ final int categoryId = eventEntry.getKey();
+ final TimelineSourceEventAccumulator categoryAccumulator = eventEntry.getValue();
+ final DateTime latestTime = categoryAccumulator.getLatestSampleAddTime();
+ if (latestTime != null && latestTime.isBefore(purgeIfBeforeDate)) {
+ purgedAccumsBecauseCategoryNotUpdated.incrementAndGet();
+ categoryAccumulator.extractAndQueueTimelineChunks();
+ categoryIdsToPurge.add(categoryId);
+ }
+ }
+ for (final int categoryId : categoryIdsToPurge) {
+ categoryMap.remove(categoryId);
+ }
+ }
+ }
+ for (final int sourceIdToPurge : oldSourceIds) {
+ accumulators.remove(sourceIdToPurge);
+ }
+ }
+
+ /**
+ * Main entry point to the timeline subsystem. Record a series of sample for a given source, at a given timestamp.
+ *
+ * @param sourceName name of the source
+ * @param eventType event category
+ * @param eventTimestamp event timestamp
+ * @param samples samples to record
+ */
+ public void record(final String sourceName, final String eventType, final DateTime eventTimestamp, final Map<String, Object> samples) {
+ if (shuttingDown.get()) {
+ eventsReceivedAfterShuttingDown.incrementAndGet();
+ return;
+ }
+ try {
+ handledEventCount.incrementAndGet();
+
+ // Find the sourceId
+ final int sourceId = timelineDAO.getOrAddSource(sourceName);
+
+ // Extract and parse samples
+ final Map<Integer, ScalarSample> scalarSamples = new LinkedHashMap<Integer, ScalarSample>();
+ convertSamplesToScalarSamples(sourceId, eventType, samples, scalarSamples);
+
+ if (scalarSamples.isEmpty()) {
+ eventsDiscarded.incrementAndGet();
+ return;
+ }
+
+ final SourceSamplesForTimestamp sourceSamples = new SourceSamplesForTimestamp(sourceId, eventType, eventTimestamp, scalarSamples);
+ if (!replaying.get()) {
+ // Start by saving locally the samples
+ backingBuffer.append(sourceSamples);
+ }
+ // Then add them to the in-memory accumulator
+ processSamples(sourceSamples);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public TimelineSourceEventAccumulator getOrAddSourceEventAccumulator(final int sourceId, final int categoryId, final DateTime firstSampleTime) {
+ return this.getOrAddSourceEventAccumulator(sourceId, categoryId, firstSampleTime, (int) config.getTimelineLength().getMillis());
+ }
+
+ public synchronized TimelineSourceEventAccumulator getOrAddSourceEventAccumulator(final int sourceId, final int categoryId, final DateTime firstSampleTime, final int timelineLengthMillis) {
+ SourceAccumulatorsAndUpdateDate sourceAccumulatorsAndUpdateDate = accumulators.get(sourceId);
+ if (sourceAccumulatorsAndUpdateDate == null) {
+ addedSourceEventAccumulatorMapCount.incrementAndGet();
+ sourceAccumulatorsAndUpdateDate = new SourceAccumulatorsAndUpdateDate(new HashMap<Integer, TimelineSourceEventAccumulator>(), new DateTime());
+ accumulators.put(sourceId, sourceAccumulatorsAndUpdateDate);
+ }
+ sourceAccumulatorsAndUpdateDate.markUpdated();
+ final Map<Integer, TimelineSourceEventAccumulator> hostCategoryAccumulators = sourceAccumulatorsAndUpdateDate.getCategoryAccumulators();
+ TimelineSourceEventAccumulator accumulator = hostCategoryAccumulators.get(categoryId);
+ if (accumulator == null) {
+ addedSourceEventAccumulatorCount.incrementAndGet();
+ accumulator = new TimelineSourceEventAccumulator(timelineDAO, timelineCoder, sampleCoder, backgroundWriter, sourceId, categoryId, firstSampleTime, timelineLengthMillis);
+ hostCategoryAccumulators.put(categoryId, accumulator);
+ log.debug("Created new Timeline for sourceId [{}] and category [{}]", sourceId, categoryId);
+ }
+ return accumulator;
+ }
+
+ @VisibleForTesting
+ public void processSamples(final SourceSamplesForTimestamp hostSamples) throws ExecutionException, IOException {
+ final int sourceId = hostSamples.getSourceId();
+ final String category = hostSamples.getCategory();
+ final int categoryId = timelineDAO.getEventCategoryId(category);
+ final DateTime timestamp = hostSamples.getTimestamp();
+ final TimelineSourceEventAccumulator accumulator = getOrAddSourceEventAccumulator(sourceId, categoryId, timestamp);
+ accumulator.addSourceSamples(hostSamples);
+ }
+
+ public Collection<? extends TimelineChunk> getInMemoryTimelineChunks(final Integer sourceId, @Nullable final DateTime filterStartTime, @Nullable final DateTime filterEndTime) throws IOException, ExecutionException {
+ return getInMemoryTimelineChunks(sourceId, ImmutableList.copyOf(timelineDAO.getMetricIdsBySourceId(sourceId)), filterStartTime, filterEndTime);
+ }
+
+ public Collection<? extends TimelineChunk> getInMemoryTimelineChunks(final Integer sourceId, final Integer metricId, @Nullable final DateTime filterStartTime, @Nullable final DateTime filterEndTime) throws IOException, ExecutionException {
+ return getInMemoryTimelineChunks(sourceId, ImmutableList.<Integer>of(metricId), filterStartTime, filterEndTime);
+ }
+
+ public synchronized Collection<? extends TimelineChunk> getInMemoryTimelineChunks(final Integer sourceId, final List<Integer> metricIds, @Nullable final DateTime filterStartTime, @Nullable final DateTime filterEndTime) throws IOException, ExecutionException {
+ getInMemoryChunksCallCount.incrementAndGet();
+ // Check first if there is an in-memory accumulator for this host
+ final SourceAccumulatorsAndUpdateDate sourceAccumulatorsAndDate = accumulators.get(sourceId);
+ if (sourceAccumulatorsAndDate == null) {
+ return ImmutableList.of();
+ }
+
+ // Now, filter each accumulator for this host
+ final List<TimelineChunk> samplesBySourceName = new ArrayList<TimelineChunk>();
+ for (final TimelineSourceEventAccumulator accumulator : sourceAccumulatorsAndDate.getCategoryAccumulators().values()) {
+ for (final TimelineChunk chunk : accumulator.getPendingTimelineChunks()) {
+ if ((filterStartTime != null && chunk.getEndTime().isBefore(filterStartTime)) ||
+ (filterEndTime != null && chunk.getStartTime().isAfter(filterEndTime)) ||
+ !metricIds.contains(chunk.getMetricId())) {
+ continue;
+ } else {
+ samplesBySourceName.add(chunk);
+ }
+ }
+ final List<DateTime> accumulatorTimes = accumulator.getTimes();
+ if (accumulatorTimes.size() == 0) {
+ continue;
+ }
+ final DateTime accumulatorStartTime = accumulator.getStartTime();
+ final DateTime accumulatorEndTime = accumulator.getEndTime();
+
+ // Check if the time filters apply
+ if ((filterStartTime != null && accumulatorEndTime.isBefore(filterStartTime)) || (filterEndTime != null && accumulatorStartTime.isAfter(filterEndTime))) {
+ // Ignore this accumulator
+ continue;
+ }
+
+ // This accumulator is in the right time range, now return only the sample kinds specified
+ final byte[] timeBytes = timelineCoder.compressDateTimes(accumulatorTimes);
+ for (final TimelineChunkAccumulator chunkAccumulator : accumulator.getTimelines().values()) {
+ if (metricIds.contains(chunkAccumulator.getMetricId())) {
+ // Extract the timeline for this chunk by copying it and reading encoded bytes
+ accumulatorDeepCopyCount.incrementAndGet();
+ final TimelineChunkAccumulator chunkAccumulatorCopy = chunkAccumulator.deepCopy();
+ final TimelineChunk timelineChunk = chunkAccumulatorCopy.extractTimelineChunkAndReset(accumulatorStartTime, accumulatorEndTime, timeBytes);
+ samplesBySourceName.add(timelineChunk);
+ }
+ }
+ }
+ inMemoryChunksReturnedCount.addAndGet(samplesBySourceName.size());
+ Collections.sort(samplesBySourceName, CHUNK_COMPARATOR);
+ return samplesBySourceName;
+ }
+
+ @VisibleForTesting
+ void convertSamplesToScalarSamples(final Integer sourceId, final String eventType, final Map<String, Object> inputSamples, final Map<Integer, ScalarSample> outputSamples) {
+ if (inputSamples == null) {
+ return;
+ }
+ final Integer eventCategoryId = timelineDAO.getOrAddEventCategory(eventType);
+
+ for (final String attributeName : inputSamples.keySet()) {
+ final Integer metricId = timelineDAO.getOrAddMetric(sourceId, eventCategoryId, attributeName);
+ final Object sample = inputSamples.get(attributeName);
+
+ outputSamples.put(metricId, ScalarSample.fromObject(sample));
+ }
+ }
+
+ public void replay(final String spoolDir) {
+ replayCount.incrementAndGet();
+ log.info("Starting replay of files in {}", spoolDir);
+ final Replayer replayer = new Replayer(spoolDir);
+ StartTimes lastStartTimes = null;
+ if (shutdownSaveMode == ShutdownSaveMode.SAVE_START_TIMES) {
+ lastStartTimes = timelineDAO.getLastStartTimes();
+ if (lastStartTimes == null) {
+ log.info("Did not find startTimes");
+ } else {
+ log.info("Retrieved startTimes from the db");
+ }
+ }
+ final StartTimes startTimes = lastStartTimes;
+ final DateTime minStartTime = lastStartTimes == null ? null : startTimes.getMinStartTime();
+ final long found = replaySamplesFoundCount.get();
+ final long outsideTimeRange = replaySamplesOutsideTimeRangeCount.get();
+ final long processed = replaySamplesProcessedCount.get();
+
+ try {
+ // Read all files in the spool directory and delete them after process, if
+ // startTimes is null.
+ replaying.set(true);
+ final int filesSkipped = replayer.readAll(startTimes == null, minStartTime, new Function<SourceSamplesForTimestamp, Void>() {
+ @Override
+ public Void apply(@Nullable final SourceSamplesForTimestamp hostSamples) {
+ if (hostSamples != null) {
+ replaySamplesFoundCount.incrementAndGet();
+ boolean useSamples = true;
+ try {
+ final int sourceId = hostSamples.getSourceId();
+ final String category = hostSamples.getCategory();
+ final int categoryId = timelineDAO.getEventCategoryId(category);
+ // If startTimes is non-null and the samples come from before the first time for
+ // the given host and event category, ignore the samples
+ if (startTimes != null) {
+ final DateTime timestamp = hostSamples.getTimestamp();
+ final DateTime categoryStartTime = startTimes.getStartTimeForSourceIdAndCategoryId(sourceId, categoryId);
+ if (timestamp == null ||
+ timestamp.isBefore(startTimes.getMinStartTime()) ||
+ (categoryStartTime != null && timestamp.isBefore(categoryStartTime))) {
+ replaySamplesOutsideTimeRangeCount.incrementAndGet();
+ useSamples = false;
+ }
+ }
+ if (useSamples) {
+ replaySamplesProcessedCount.incrementAndGet();
+ processSamples(hostSamples);
+ }
+ } catch (Exception e) {
+ log.warn("Got exception replaying sample, data potentially lost! {}", hostSamples.toString());
+ }
+ }
+
+ return null;
+ }
+ });
+ if (shutdownSaveMode == ShutdownSaveMode.SAVE_START_TIMES) {
+ timelineDAO.deleteLastStartTimes();
+ log.info("Deleted old startTimes");
+ }
+ log.info(String.format("Replay completed; %d files skipped, samples read %d, samples outside time range %d, samples used %d",
+ filesSkipped, replaySamplesFoundCount.get() - found, replaySamplesOutsideTimeRangeCount.get() - outsideTimeRange, replaySamplesProcessedCount.get() - processed));
+ } catch (RuntimeException e) {
+ // Catch the exception to make the collector start properly
+ log.error("Ignoring error when replaying the data", e);
+ } finally {
+ replaying.set(false);
+ }
+ }
+
+ public void forceCommit() {
+ forceCommitCallCount.incrementAndGet();
+ saveAccumulators();
+ backingBuffer.discard();
+ log.info("Timelines committed");
+ }
+
+ public void commitAndShutdown() {
+ shuttingDown.set(true);
+ final boolean doingFastShutdown = shutdownSaveMode == ShutdownSaveMode.SAVE_START_TIMES;
+ if (doingFastShutdown) {
+ final StartTimes startTimes = new StartTimes();
+ saveStartTimes(startTimes);
+ timelineDAO.insertLastStartTimes(startTimes);
+ log.info("During shutdown, saved timeline start times in the db");
+ } else {
+ saveAccumulators();
+ log.info("During shutdown, saved timeline accumulators");
+ }
+ performShutdown();
+ backingBuffer.discard();
+ }
+
+ private void performShutdown() {
+ backgroundWriter.initiateShutdown();
+ while (!backgroundWriter.getShutdownFinished()) {
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ purgeThread.shutdown();
+ }
+
+ private synchronized void purgeFilesAndAccumulators() {
+ this.purgeFilesAndAccumulators(new DateTime().minus(config.getTimelineLength().getMillis()), new DateTime().minus(2 * config.getTimelineLength().getMillis()));
+ }
+
+ // TODO: We have a bad interaction between startTimes and purging: If the system is down
+ // for two hours, we may not want it to purge everything. Figure out what to do about this.
+ private synchronized void purgeFilesAndAccumulators(final DateTime purgeAccumulatorsIfBefore, final DateTime purgeFilesIfBefore) {
+ purgeOldSourcesAndAccumulators(purgeAccumulatorsIfBefore);
+ final Replayer replayer = new Replayer(config.getSpoolDir());
+ replayer.purgeOldFiles(purgeFilesIfBefore);
+ }
+
+ public void startHandlerThreads() {
+ purgeThread.scheduleWithFixedDelay(new Runnable() {
+ @Override
+ public void run() {
+ purgeFilesAndAccumulators();
+ }
+ },
+ config.getTimelineLength().getMillis(),
+ config.getTimelineLength().getMillis(),
+ TimeUnit.MILLISECONDS);
+ }
+
+ // We use the lastUpdateDate to purge sources and their accumulators from the map
+ private static class SourceAccumulatorsAndUpdateDate {
+
+ private final Map<Integer, TimelineSourceEventAccumulator> categoryAccumulators;
+ private DateTime lastUpdateDate;
+
+ public SourceAccumulatorsAndUpdateDate(final Map<Integer, TimelineSourceEventAccumulator> categoryAccumulators, final DateTime lastUpdateDate) {
+ this.categoryAccumulators = categoryAccumulators;
+ this.lastUpdateDate = lastUpdateDate;
+ }
+
+ public Map<Integer, TimelineSourceEventAccumulator> getCategoryAccumulators() {
+ return categoryAccumulators;
+ }
+
+ public DateTime getLastUpdateDate() {
+ return lastUpdateDate;
+ }
+
+ public void markUpdated() {
+ lastUpdateDate = new DateTime();
+ }
+ }
+
+ @VisibleForTesting
+ public Collection<TimelineSourceEventAccumulator> getAccumulators() {
+ final List<TimelineSourceEventAccumulator> inMemoryAccumulator = new ArrayList<TimelineSourceEventAccumulator>();
+ for (final SourceAccumulatorsAndUpdateDate sourceEventAccumulatorMap : accumulators.values()) {
+ inMemoryAccumulator.addAll(sourceEventAccumulatorMap.getCategoryAccumulators().values());
+ }
+
+ return inMemoryAccumulator;
+ }
+
+ @VisibleForTesting
+ public FileBackedBuffer getBackingBuffer() {
+ return backingBuffer;
+ }
+
+ public long getEventsDiscarded() {
+ return eventsDiscarded.get();
+ }
+
+ public long getSourceEventAccumulatorCount() {
+ return accumulators.size();
+ }
+
+ public long getEventsReceivedAfterShuttingDown() {
+ return eventsReceivedAfterShuttingDown.get();
+ }
+
+ public long getHandledEventCount() {
+ return handledEventCount.get();
+ }
+
+ public long getAddedSourceEventAccumulatorMapCount() {
+ return addedSourceEventAccumulatorMapCount.get();
+ }
+
+ public long getAddedSourceEventAccumulatorCount() {
+ return addedSourceEventAccumulatorCount.get();
+ }
+
+ public long getGetInMemoryChunksCallCount() {
+ return getInMemoryChunksCallCount.get();
+ }
+
+ public long getAccumulatorDeepCopyCount() {
+ return accumulatorDeepCopyCount.get();
+ }
+
+ public long getInMemoryChunksReturnedCount() {
+ return inMemoryChunksReturnedCount.get();
+ }
+
+ public long getReplayCount() {
+ return replayCount.get();
+ }
+
+ public long getReplaySamplesFoundCount() {
+ return replaySamplesFoundCount.get();
+ }
+
+ public long getReplaySamplesOutsideTimeRangeCount() {
+ return replaySamplesOutsideTimeRangeCount.get();
+ }
+
+ public long getReplaySamplesProcessedCount() {
+ return replaySamplesProcessedCount.get();
+ }
+
+ public long getForceCommitCallCount() {
+ return forceCommitCallCount.get();
+ }
+
+ public long getPurgedAccumsBecauseSourceNotUpdated() {
+ return purgedAccumsBecauseSourceNotUpdated.get();
+ }
+
+ public long getPurgedAccumsBecauseCategoryNotUpdated() {
+ return purgedAccumsBecauseCategoryNotUpdated.get();
+ }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/TimelineSourceEventAccumulator.java b/usage/src/main/java/com/ning/billing/usage/timeline/TimelineSourceEventAccumulator.java
new file mode 100644
index 0000000..d352dd9
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/TimelineSourceEventAccumulator.java
@@ -0,0 +1,314 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.joda.time.DateTime;
+import org.joda.time.format.DateTimeFormatter;
+import org.joda.time.format.ISODateTimeFormat;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.ning.billing.usage.timeline.chunks.TimelineChunk;
+import com.ning.billing.usage.timeline.codec.SampleCoder;
+import com.ning.billing.usage.timeline.codec.TimelineChunkAccumulator;
+import com.ning.billing.usage.timeline.persistent.TimelineDao;
+import com.ning.billing.usage.timeline.samples.NullSample;
+import com.ning.billing.usage.timeline.samples.RepeatSample;
+import com.ning.billing.usage.timeline.samples.ScalarSample;
+import com.ning.billing.usage.timeline.sources.SourceSamplesForTimestamp;
+import com.ning.billing.usage.timeline.times.TimelineCoder;
+
+/**
+ * This class represents a collection of timeline chunks, one for each
+ * metric belonging to one event category, each over a specific time period,
+ * from a single source. This class is used to accumulate samples
+ * to be written to the database; a separate streaming class with
+ * much less overhead is used to "play back" the samples read from
+ * the db in response to queries.
+ * <p/>
+ * All subordinate timelines contain the same number of samples.
+ * <p/>
+ * When enough samples have accumulated, typically one hour's worth,
+ * in-memory samples are made into TimelineChunks, one chunk for each metricId
+ * maintained by the accumulator.
+ * <p/>
+ * These new chunks are organized as PendingChunkMaps, kept in a local list and also
+ * handed off to a PendingChunkMapConsumer to written to the db by a background process. At some
+ * in the future, that background process will call markPendingChunkMapConsumed(),
+ * passing the id of a PendingChunkMap. This causes the PendingChunkMap
+ * to be removed from the local list maintained by the TimelineSourceEventAccumulator.
+ * <p/>
+ * Queries that cause the TimelineSourceEventAccumulator instance to return memory
+ * chunks also return any chunks in PendingChunkMaps in the local list of pending chunks.
+ */
+public class TimelineSourceEventAccumulator {
+
+ private static final Logger log = LoggerFactory.getLogger(TimelineSourceEventAccumulator.class);
+ private static final DateTimeFormatter dateFormatter = ISODateTimeFormat.dateTime();
+ private static final NullSample nullSample = new NullSample();
+ private static final boolean checkEveryAccess = Boolean.parseBoolean(System.getProperty("killbill.usage.checkEveryAccess"));
+ private static final Random rand = new Random(0);
+
+ private final Map<Integer, SampleSequenceNumber> metricIdCounters = new HashMap<Integer, SampleSequenceNumber>();
+ private final List<PendingChunkMap> pendingChunkMaps = new ArrayList<PendingChunkMap>();
+ private long pendingChunkMapIdCounter = 1;
+
+ private final BackgroundDBChunkWriter backgroundWriter;
+ private final TimelineCoder timelineCoder;
+ private final SampleCoder sampleCoder;
+ private final Integer timelineLengthMillis;
+ private final int sourceId;
+ private final int eventCategoryId;
+ // This is the time when we want to end the chunk. Setting the value randomly
+ // when the TimelineSourceEventAccumulator is created provides a mechanism to
+ // distribute the db writes
+ private DateTime chunkEndTime = null;
+ private DateTime startTime = null;
+ private DateTime endTime = null;
+ private DateTime latestSampleAddTime;
+ private long sampleSequenceNumber = 0;
+ private int sampleCount = 0;
+
+ /**
+ * Maps the sample kind id to the accumulator for that sample kind
+ */
+ private final Map<Integer, TimelineChunkAccumulator> timelines = new ConcurrentHashMap<Integer, TimelineChunkAccumulator>();
+
+ /**
+ * Holds the sampling times of the samples
+ */
+ private final List<DateTime> times = new ArrayList<DateTime>();
+
+ public TimelineSourceEventAccumulator(final TimelineDao dao, final TimelineCoder timelineCoder, final SampleCoder sampleCoder,
+ final BackgroundDBChunkWriter backgroundWriter, final int sourceId, final int eventCategoryId,
+ final DateTime firstSampleTime, final Integer timelineLengthMillis) {
+ this.timelineLengthMillis = timelineLengthMillis;
+ this.backgroundWriter = backgroundWriter;
+ this.timelineCoder = timelineCoder;
+ this.sampleCoder = sampleCoder;
+ this.sourceId = sourceId;
+ this.eventCategoryId = eventCategoryId;
+ // Set the end-of-chunk time by tossing a random number, to evenly distribute the db writeback load.
+ this.chunkEndTime = timelineLengthMillis != null ? firstSampleTime.plusMillis(rand.nextInt(timelineLengthMillis)) : null;
+ }
+
+ /*
+ * This constructor is used for testing; it writes chunks as soon as they are
+ * created, but because the chunkEndTime is way in the future, doesn't initiate
+ * chunk writes.
+ */
+ public TimelineSourceEventAccumulator(final TimelineDao timelineDAO, final TimelineCoder timelineCoder, final SampleCoder sampleCoder,
+ final Integer sourceId, final int eventTypeId, final DateTime firstSampleTime) {
+ this(timelineDAO, timelineCoder, sampleCoder, new BackgroundDBChunkWriter(timelineDAO, null, true), sourceId, eventTypeId, firstSampleTime, Integer.MAX_VALUE);
+ }
+
+ @SuppressWarnings("unchecked")
+ // TODO - we can probably do better than synchronize the whole method
+ public synchronized void addSourceSamples(final SourceSamplesForTimestamp samples) {
+ final DateTime timestamp = samples.getTimestamp();
+
+ if (chunkEndTime != null && chunkEndTime.isBefore(timestamp)) {
+ extractAndQueueTimelineChunks();
+ startTime = timestamp;
+ chunkEndTime = timestamp.plusMillis(timelineLengthMillis);
+ }
+
+ if (startTime == null) {
+ startTime = timestamp;
+ }
+ if (endTime == null) {
+ endTime = timestamp;
+ } else if (!timestamp.isAfter(endTime)) {
+ log.warn("Adding samples for host {}, timestamp {} is not after the end time {}; ignored",
+ new Object[]{sourceId, dateFormatter.print(timestamp), dateFormatter.print(endTime)});
+ return;
+ }
+ sampleSequenceNumber++;
+ latestSampleAddTime = new DateTime();
+ for (final Map.Entry<Integer, ScalarSample> entry : samples.getSamples().entrySet()) {
+ final Integer metricId = entry.getKey();
+ final SampleSequenceNumber counter = metricIdCounters.get(metricId);
+ if (counter != null) {
+ counter.setSequenceNumber(sampleSequenceNumber);
+ } else {
+ metricIdCounters.put(metricId, new SampleSequenceNumber(sampleSequenceNumber));
+ }
+ final ScalarSample sample = entry.getValue();
+ TimelineChunkAccumulator timeline = timelines.get(metricId);
+ if (timeline == null) {
+ timeline = new TimelineChunkAccumulator(sourceId, metricId, sampleCoder);
+ if (sampleCount > 0) {
+ addPlaceholders(timeline, sampleCount);
+ }
+ timelines.put(metricId, timeline);
+ }
+ final ScalarSample compressedSample = sampleCoder.compressSample(sample);
+ timeline.addSample(compressedSample);
+ }
+ for (final Map.Entry<Integer, SampleSequenceNumber> entry : metricIdCounters.entrySet()) {
+ final SampleSequenceNumber counter = entry.getValue();
+ if (counter.getSequenceNumber() < sampleSequenceNumber) {
+ counter.setSequenceNumber(sampleSequenceNumber);
+ final int metricId = entry.getKey();
+ final TimelineChunkAccumulator timeline = timelines.get(metricId);
+ timeline.addSample(nullSample);
+ }
+ }
+ // Now we can update the state
+ endTime = timestamp;
+ sampleCount++;
+ times.add(timestamp);
+
+ if (checkEveryAccess) {
+ checkSampleCounts(sampleCount);
+ }
+ }
+
+ private void addPlaceholders(final TimelineChunkAccumulator timeline, int countToAdd) {
+ final int maxRepeatSamples = RepeatSample.MAX_SHORT_REPEAT_COUNT;
+ while (countToAdd >= maxRepeatSamples) {
+ timeline.addPlaceholder((byte) maxRepeatSamples);
+ countToAdd -= maxRepeatSamples;
+ }
+ if (countToAdd > 0) {
+ timeline.addPlaceholder((byte) countToAdd);
+ }
+ }
+
+ /**
+ * This method queues a map of TimelineChunks extracted from the TimelineChunkAccumulators
+ * to be written to the db. When memory chunks are requested, any queued chunk will be included
+ * in the list.
+ */
+ public synchronized void extractAndQueueTimelineChunks() {
+ if (times.size() > 0) {
+ final Map<Integer, TimelineChunk> chunkMap = new HashMap<Integer, TimelineChunk>();
+ final byte[] timeBytes = timelineCoder.compressDateTimes(times);
+ for (final Map.Entry<Integer, TimelineChunkAccumulator> entry : timelines.entrySet()) {
+ final int metricId = entry.getKey();
+ final TimelineChunkAccumulator accumulator = entry.getValue();
+ final TimelineChunk chunk = accumulator.extractTimelineChunkAndReset(startTime, endTime, timeBytes);
+ chunkMap.put(metricId, chunk);
+ }
+ times.clear();
+ sampleCount = 0;
+ final long counter = pendingChunkMapIdCounter++;
+ final PendingChunkMap newChunkMap = new PendingChunkMap(this, counter, chunkMap);
+ pendingChunkMaps.add(newChunkMap);
+ backgroundWriter.addPendingChunkMap(newChunkMap);
+ }
+ }
+
+ public synchronized void markPendingChunkMapConsumed(final long pendingChunkMapId) {
+ final PendingChunkMap pendingChunkMap = pendingChunkMaps.size() > 0 ? pendingChunkMaps.get(0) : null;
+ if (pendingChunkMap == null) {
+ log.error("In TimelineSourceEventAccumulator.markPendingChunkMapConsumed(), could not find the map for {}", pendingChunkMapId);
+ } else if (pendingChunkMapId != pendingChunkMap.getPendingChunkMapId()) {
+ log.error("In TimelineSourceEventAccumulator.markPendingChunkMapConsumed(), the next map has id {}, but we're consuming id {}",
+ pendingChunkMap.getPendingChunkMapId(), pendingChunkMapId);
+ } else {
+ pendingChunkMaps.remove(0);
+ }
+ }
+
+ public synchronized List<TimelineChunk> getPendingTimelineChunks() {
+ final List<TimelineChunk> timelineChunks = new ArrayList<TimelineChunk>();
+ for (final PendingChunkMap pendingChunkMap : pendingChunkMaps) {
+ timelineChunks.addAll(pendingChunkMap.getChunkMap().values());
+ }
+ return timelineChunks;
+ }
+
+ /**
+ * Make sure all timelines have the sample count passed in; otherwise log
+ * discrepancies and return false
+ *
+ * @param assertedCount The sample count that all timelines are supposed to have
+ * @return true if all timelines have the right count; false otherwise
+ */
+ public boolean checkSampleCounts(final int assertedCount) {
+ boolean success = true;
+ if (assertedCount != sampleCount) {
+ log.error("For host {}, start time {}, the SourceTimeLines sampleCount {} is not equal to the assertedCount {}",
+ new Object[]{sourceId, dateFormatter.print(startTime), sampleCount, assertedCount});
+ success = false;
+ }
+ for (final Map.Entry<Integer, TimelineChunkAccumulator> entry : timelines.entrySet()) {
+ final int metricId = entry.getKey();
+ final TimelineChunkAccumulator timeline = entry.getValue();
+ final int lineSampleCount = timeline.getSampleCount();
+ if (lineSampleCount != assertedCount) {
+ log.error("For host {}, start time {}, sample kind id {}, the sampleCount {} is not equal to the assertedCount {}",
+ new Object[]{sourceId, dateFormatter.print(startTime), metricId, lineSampleCount, assertedCount});
+ success = false;
+ }
+ }
+ return success;
+ }
+
+ public int getSourceId() {
+ return sourceId;
+ }
+
+ public int getEventCategoryId() {
+ return eventCategoryId;
+ }
+
+ public DateTime getStartTime() {
+ return startTime;
+ }
+
+ public DateTime getEndTime() {
+ return endTime;
+ }
+
+ public Map<Integer, TimelineChunkAccumulator> getTimelines() {
+ return timelines;
+ }
+
+ public List<DateTime> getTimes() {
+ return times;
+ }
+
+ public DateTime getLatestSampleAddTime() {
+ return latestSampleAddTime;
+ }
+
+ private static class SampleSequenceNumber {
+
+ private long sequenceNumber;
+
+ public SampleSequenceNumber(final long sequenceNumber) {
+ this.sequenceNumber = sequenceNumber;
+ }
+
+ public long getSequenceNumber() {
+ return sequenceNumber;
+ }
+
+ public void setSequenceNumber(final long sequenceNumber) {
+ this.sequenceNumber = sequenceNumber;
+ }
+ }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/times/DefaultTimelineCoder.java b/usage/src/main/java/com/ning/billing/usage/timeline/times/DefaultTimelineCoder.java
new file mode 100644
index 0000000..620b7e0
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/times/DefaultTimelineCoder.java
@@ -0,0 +1,350 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.times;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.joda.time.DateTime;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.ning.billing.usage.timeline.util.DateTimeUtils;
+import com.ning.billing.usage.timeline.util.Hex;
+
+public class DefaultTimelineCoder implements TimelineCoder {
+
+ public static final Logger log = LoggerFactory.getLogger(TimelineCoder.class);
+ public static final int MAX_SHORT_REPEAT_COUNT = 0xFFFF;
+ public static final int MAX_BYTE_REPEAT_COUNT = 0xFF;
+
+ /**
+ * Convert the array of unix times to a compressed timeline, and return the byte array
+ * representing that compressed timeline
+ *
+ * @param times an int array giving the unix times to be compressed
+ * @return the compressed timeline
+ */
+ @Override
+ public byte[] compressDateTimes(final List<DateTime> times) {
+ final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ final DataOutputStream dataStream = new DataOutputStream(outputStream);
+ try {
+ int lastTime = 0;
+ int lastDelta = 0;
+ int repeatCount = 0;
+ for (final DateTime time : times) {
+ final int newTime = DateTimeUtils.unixSeconds(time);
+ if (lastTime == 0) {
+ lastTime = newTime;
+ writeTime(0, lastTime, dataStream);
+ continue;
+ } else if (newTime < lastTime) {
+ log.warn("In TimelineCoder.compressTimes(), newTime {} is < lastTime {}; ignored", newTime, lastTime);
+ continue;
+ }
+ final int delta = newTime - lastTime;
+ final boolean deltaWorks = delta <= TimelineOpcode.MAX_DELTA_TIME;
+ final boolean sameDelta = repeatCount > 0 && delta == lastDelta;
+ if (deltaWorks) {
+ if (sameDelta) {
+ repeatCount++;
+ if (repeatCount == MAX_SHORT_REPEAT_COUNT) {
+ writeRepeatedDelta(delta, repeatCount, dataStream);
+ repeatCount = 0;
+ }
+ } else {
+ if (repeatCount > 0) {
+ writeRepeatedDelta(lastDelta, repeatCount, dataStream);
+ }
+ repeatCount = 1;
+ }
+ lastDelta = delta;
+ } else {
+ if (repeatCount > 0) {
+ writeRepeatedDelta(lastDelta, repeatCount, dataStream);
+ }
+ writeTime(0, newTime, dataStream);
+ repeatCount = 0;
+ lastDelta = 0;
+ }
+ lastTime = newTime;
+ }
+ if (repeatCount > 0) {
+ writeRepeatedDelta(lastDelta, repeatCount, dataStream);
+ }
+ dataStream.flush();
+ return outputStream.toByteArray();
+ } catch (IOException e) {
+ log.error("Exception compressing times list of length {}", times.size(), e);
+ return null;
+ }
+ }
+
+ @Override
+ public byte[] combineTimelines(final List<byte[]> timesList, final Integer sampleCount) {
+ final byte[] timeBytes = combineTimelines(timesList);
+ final int combinedSampleCount = countTimeBytesSamples(timeBytes);
+ if (sampleCount != null && sampleCount != combinedSampleCount) {
+ final StringBuilder builder = new StringBuilder();
+ builder.append("In compressTimelineTimes(), combined sample count is ")
+ .append(combinedSampleCount)
+ .append(", but sample count is ")
+ .append(sampleCount)
+ .append(", combined TimeBytes ")
+ .append(Hex.encodeHex(timeBytes))
+ .append(", ")
+ .append(timesList.size())
+ .append(" chunks");
+ for (final byte[] bytes : timesList) {
+ builder.append(", ")
+ .append(Hex.encodeHex(bytes));
+ }
+ log.error(builder.toString());
+ }
+ return timeBytes;
+ }
+
+ private byte[] combineTimelines(final List<byte[]> timesList) {
+ final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ final DataOutputStream dataStream = new DataOutputStream(outputStream);
+ try {
+ int lastTime = 0;
+ int lastDelta = 0;
+ int repeatCount = 0;
+ int chunkCounter = 0;
+ for (byte[] times : timesList) {
+ final ByteArrayInputStream byteStream = new ByteArrayInputStream(times);
+ final DataInputStream byteDataStream = new DataInputStream(byteStream);
+ int byteCursor = 0;
+ while (true) {
+ // Part 1: Get the opcode, and come up with newTime, newCount and newDelta
+ final int opcode = byteDataStream.read();
+ if (opcode == -1) {
+ break;
+ }
+ byteCursor++;
+ int newTime = 0;
+ int newCount = 0;
+ int newDelta = 0;
+ boolean useNewDelta = false;
+ boolean nonDeltaTime = false;
+ if (opcode == TimelineOpcode.FULL_TIME.getOpcodeIndex()) {
+ newTime = byteDataStream.readInt();
+ if (newTime < lastTime) {
+ log.warn("In TimelineCoder.combineTimeLines(), the fulltime read is %d, but the lastTime is %d; setting newTime to lastTime",
+ newTime, lastTime);
+ newTime = lastTime;
+ }
+ byteCursor += 4;
+ if (lastTime == 0) {
+ writeTime(0, newTime, dataStream);
+ lastTime = newTime;
+ lastDelta = 0;
+ repeatCount = 0;
+ continue;
+ } else if (newTime - lastTime <= TimelineOpcode.MAX_DELTA_TIME) {
+ newDelta = newTime - lastTime;
+ useNewDelta = true;
+ newCount = 1;
+ } else {
+ nonDeltaTime = true;
+ }
+ } else if (opcode <= TimelineOpcode.MAX_DELTA_TIME) {
+ newTime = lastTime + opcode;
+ newDelta = opcode;
+ useNewDelta = true;
+ newCount = 1;
+ } else if (opcode == TimelineOpcode.REPEATED_DELTA_TIME_BYTE.getOpcodeIndex()) {
+ newCount = byteDataStream.read();
+ newDelta = byteDataStream.read();
+ useNewDelta = true;
+ byteCursor += 2;
+ if (lastTime != 0) {
+ newTime = lastTime + newDelta * newCount;
+ } else {
+ throw new IllegalStateException(String.format("In TimelineCoder.combineTimelines, lastTime is 0 byte opcode = %d, byteCursor %d, chunkCounter %d, chunk %s",
+ opcode, byteCursor, chunkCounter, new String(Hex.encodeHex(times))));
+ }
+ } else if (opcode == TimelineOpcode.REPEATED_DELTA_TIME_SHORT.getOpcodeIndex()) {
+ newCount = byteDataStream.readUnsignedShort();
+ newDelta = byteDataStream.read();
+ useNewDelta = true;
+ byteCursor += 3;
+ if (lastTime != 0) {
+ newTime = lastTime + newDelta * newCount;
+ }
+ } else {
+ throw new IllegalStateException(String.format("In TimelineCoder.combineTimelines, Unrecognized byte opcode = %d, byteCursor %d, chunkCounter %d, chunk %s",
+ opcode, byteCursor, chunkCounter, new String(Hex.encodeHex(times))));
+ }
+ // Part 2: Combine existing state represented in lastTime, lastDelta and repeatCount with newTime, newCount and newDelta
+ if (lastTime == 0) {
+ log.error("In combineTimelines(), lastTime is 0; byteCursor {}, chunkCounter {}, times {}", new Object[]{byteCursor, chunkCounter, new String(Hex.encodeHex(times))});
+ } else if (repeatCount > 0) {
+ if (lastDelta == newDelta && newCount > 0) {
+ repeatCount += newCount;
+ lastTime = newTime;
+ } else {
+ writeRepeatedDelta(lastDelta, repeatCount, dataStream);
+ if (useNewDelta) {
+ lastDelta = newDelta;
+ repeatCount = newCount;
+ lastTime = newTime;
+ } else {
+ writeTime(lastTime, newTime, dataStream);
+ lastTime = newTime;
+ lastDelta = 0;
+ repeatCount = 0;
+ }
+ }
+ } else if (nonDeltaTime) {
+ writeTime(lastTime, newTime, dataStream);
+ lastTime = newTime;
+ lastDelta = 0;
+ repeatCount = 0;
+ } else if (lastDelta == 0) {
+ lastTime = newTime;
+ repeatCount = newCount;
+ lastDelta = newDelta;
+ }
+ }
+ chunkCounter++;
+ }
+
+ if (repeatCount > 0) {
+ writeRepeatedDelta(lastDelta, repeatCount, dataStream);
+ }
+ dataStream.flush();
+
+ return outputStream.toByteArray();
+ } catch (Exception e) {
+ log.error("In combineTimesLines(), exception combining timelines", e);
+ return new byte[0];
+ }
+ }
+
+ @Override
+ public List<DateTime> decompressDateTimes(final byte[] compressedTimes) {
+ final List<DateTime> dateTimeList = new ArrayList<DateTime>(compressedTimes.length * 4);
+ final ByteArrayInputStream byteStream = new ByteArrayInputStream(compressedTimes);
+ final DataInputStream byteDataStream = new DataInputStream(byteStream);
+ int opcode;
+ int lastTime = 0;
+ try {
+ while (true) {
+ opcode = byteDataStream.read();
+ if (opcode == -1) {
+ break;
+ }
+
+ if (opcode == TimelineOpcode.FULL_TIME.getOpcodeIndex()) {
+ lastTime = byteDataStream.readInt();
+ dateTimeList.add(DateTimeUtils.dateTimeFromUnixSeconds(lastTime));
+ } else if (opcode == TimelineOpcode.REPEATED_DELTA_TIME_BYTE.getOpcodeIndex()) {
+ final int repeatCount = byteDataStream.readUnsignedByte();
+ final int delta = byteDataStream.readUnsignedByte();
+ for (int i = 0; i < repeatCount; i++) {
+ lastTime = lastTime + delta;
+ dateTimeList.add(DateTimeUtils.dateTimeFromUnixSeconds(lastTime));
+ }
+ } else if (opcode == TimelineOpcode.REPEATED_DELTA_TIME_SHORT.getOpcodeIndex()) {
+ final int repeatCount = byteDataStream.readUnsignedShort();
+ final int delta = byteDataStream.readUnsignedByte();
+ for (int i = 0; i < repeatCount; i++) {
+ lastTime = lastTime + delta;
+ dateTimeList.add(DateTimeUtils.dateTimeFromUnixSeconds(lastTime));
+ }
+ } else {
+ // The opcode is itself a singleton delta
+ lastTime = lastTime + opcode;
+ dateTimeList.add(DateTimeUtils.dateTimeFromUnixSeconds(lastTime));
+ }
+ }
+ } catch (IOException e) {
+ log.error("In decompressTimes(), exception decompressing", e);
+ }
+
+ return dateTimeList;
+ }
+
+ @Override
+ public int countTimeBytesSamples(final byte[] timeBytes) {
+ int count = 0;
+ try {
+ final ByteArrayInputStream byteStream = new ByteArrayInputStream(timeBytes);
+ final DataInputStream byteDataStream = new DataInputStream(byteStream);
+ int opcode;
+ while ((opcode = byteDataStream.read()) != -1) {
+ if (opcode == TimelineOpcode.FULL_TIME.getOpcodeIndex()) {
+ byteDataStream.readInt();
+ count++;
+ } else if (opcode <= TimelineOpcode.MAX_DELTA_TIME) {
+ count++;
+ } else if (opcode == TimelineOpcode.REPEATED_DELTA_TIME_BYTE.getOpcodeIndex()) {
+ count += byteDataStream.read();
+ //noinspection ResultOfMethodCallIgnored
+ byteDataStream.read();
+ } else if (opcode == TimelineOpcode.REPEATED_DELTA_TIME_SHORT.getOpcodeIndex()) {
+ count += byteDataStream.readUnsignedShort();
+ //noinspection ResultOfMethodCallIgnored
+ byteDataStream.read();
+ } else {
+ throw new IllegalStateException(String.format("In TimelineCoder.countTimeBytesSamples(), unrecognized opcode %d", opcode));
+ }
+ }
+ return count;
+ } catch (IOException e) {
+ log.error("IOException while counting timeline samples", e);
+ return count;
+ }
+ }
+
+ private void writeRepeatedDelta(final int delta, final int repeatCount, final DataOutputStream dataStream) throws IOException {
+ if (repeatCount > 1) {
+ if (repeatCount > MAX_BYTE_REPEAT_COUNT) {
+ dataStream.writeByte(TimelineOpcode.REPEATED_DELTA_TIME_SHORT.getOpcodeIndex());
+ dataStream.writeShort(repeatCount);
+ } else if (repeatCount == 2) {
+ dataStream.writeByte(delta);
+ } else {
+ dataStream.writeByte(TimelineOpcode.REPEATED_DELTA_TIME_BYTE.getOpcodeIndex());
+ dataStream.writeByte(repeatCount);
+ }
+ }
+ dataStream.writeByte(delta);
+ }
+
+ private void writeTime(final int lastTime, final int newTime, final DataOutputStream dataStream) throws IOException {
+ if (newTime > lastTime) {
+ final int delta = (newTime - lastTime);
+ if (delta <= TimelineOpcode.MAX_DELTA_TIME) {
+ dataStream.writeByte(delta);
+ } else {
+ dataStream.writeByte(TimelineOpcode.FULL_TIME.getOpcodeIndex());
+ dataStream.writeInt(newTime);
+ }
+ } else if (newTime == lastTime) {
+ dataStream.writeByte(0);
+ }
+ }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/times/DefaultTimelineCursor.java b/usage/src/main/java/com/ning/billing/usage/timeline/times/DefaultTimelineCursor.java
new file mode 100644
index 0000000..4831e5c
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/times/DefaultTimelineCursor.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.times;
+
+import java.io.ByteArrayInputStream;
+import java.io.DataInputStream;
+import java.io.IOException;
+
+import org.joda.time.DateTime;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.ning.billing.usage.timeline.util.DateTimeUtils;
+
+public class DefaultTimelineCursor implements TimelineCursor {
+
+ private static final Logger log = LoggerFactory.getLogger(DefaultTimelineCursor.class);
+
+ private final DataInputStream timelineDataStream;
+ private int sampleCount;
+ private int sampleNumber;
+ private int byteCursor;
+ private int lastValue;
+ private int delta;
+ private int repeatCount;
+
+ public DefaultTimelineCursor(final byte[] times, final int sampleCount) {
+ this.timelineDataStream = new DataInputStream(new ByteArrayInputStream(times));
+ this.sampleCount = sampleCount;
+ this.sampleNumber = 0;
+ this.byteCursor = 0;
+ this.lastValue = 0;
+ this.delta = 0;
+ this.repeatCount = 0;
+ }
+
+ private int getNextTimeInternal() {
+ try {
+ if (repeatCount > 0) {
+ repeatCount--;
+ lastValue += delta;
+ } else {
+ final int nextOpcode = timelineDataStream.read();
+ byteCursor++;
+ if (nextOpcode == -1) {
+ return nextOpcode;
+ }
+ if (nextOpcode == TimelineOpcode.FULL_TIME.getOpcodeIndex()) {
+ lastValue = timelineDataStream.readInt();
+ byteCursor += 4;
+ } else if (nextOpcode == TimelineOpcode.REPEATED_DELTA_TIME_BYTE.getOpcodeIndex()) {
+ repeatCount = timelineDataStream.readUnsignedByte() - 1;
+ delta = timelineDataStream.read();
+ byteCursor += 2;
+ lastValue += delta;
+ } else if (nextOpcode == TimelineOpcode.REPEATED_DELTA_TIME_SHORT.getOpcodeIndex()) {
+ repeatCount = timelineDataStream.readUnsignedShort() - 1;
+ delta = timelineDataStream.read();
+ byteCursor += 3;
+ lastValue += delta;
+ } else if (nextOpcode <= TimelineOpcode.MAX_DELTA_TIME) {
+ lastValue += nextOpcode;
+ } else {
+ throw new IllegalStateException(String.format("In TimeIterator.getNextTime(), unknown opcode %x at offset %d", nextOpcode, byteCursor));
+ }
+ }
+ sampleNumber++;
+ if (sampleNumber > sampleCount) {
+ log.error("In TimeIterator.getNextTime(), after update, sampleNumber %d > sampleCount %d", sampleNumber, sampleCount);
+ }
+ return lastValue;
+ } catch (IOException e) {
+ log.error("IOException in TimeIterator.getNextTime()", e);
+ return -1;
+ }
+ }
+
+ @Override
+ public void skipToSampleNumber(final int finalSampleNumber) {
+ if (finalSampleNumber > sampleCount) {
+ log.error("In TimeIterator.skipToSampleNumber(), finalSampleCount {} > sampleCount {}", finalSampleNumber, sampleCount);
+ }
+ while (sampleNumber < finalSampleNumber) {
+ try {
+ if (repeatCount > 0) {
+ final int countToSkipInRepeat = Math.min(finalSampleNumber - sampleNumber, repeatCount);
+ sampleNumber += countToSkipInRepeat;
+ repeatCount -= countToSkipInRepeat;
+ lastValue += countToSkipInRepeat * delta;
+ } else {
+ final int nextOpcode = timelineDataStream.read();
+ if (nextOpcode == -1) {
+ return;
+ }
+ byteCursor++;
+ if (nextOpcode == TimelineOpcode.FULL_TIME.getOpcodeIndex()) {
+ lastValue = timelineDataStream.readInt();
+ byteCursor += 4;
+ sampleNumber++;
+ } else if (nextOpcode == TimelineOpcode.REPEATED_DELTA_TIME_BYTE.getOpcodeIndex()) {
+ repeatCount = timelineDataStream.readUnsignedByte() - 1;
+ delta = timelineDataStream.read();
+ byteCursor += 2;
+ lastValue += delta;
+ sampleNumber++;
+ } else if (nextOpcode == TimelineOpcode.REPEATED_DELTA_TIME_SHORT.getOpcodeIndex()) {
+ repeatCount = timelineDataStream.readUnsignedShort() - 1;
+ delta = timelineDataStream.read();
+ byteCursor += 3;
+ lastValue += delta;
+ sampleNumber++;
+ } else if (nextOpcode <= TimelineOpcode.MAX_DELTA_TIME) {
+ lastValue += nextOpcode;
+ sampleNumber++;
+ } else {
+ throw new IllegalStateException(String.format("In TimeIterator.skipToSampleNumber(), unknown opcode %x at offset %d", nextOpcode, byteCursor));
+ }
+ }
+ } catch (IOException e) {
+ log.error("IOException in TimeIterator.getNextTime()", e);
+ }
+ }
+ }
+
+ @Override
+ public DateTime getNextTime() {
+ final int nextTime = getNextTimeInternal();
+ if (nextTime == -1) {
+ throw new IllegalStateException(String.format("In DecodedSampleOutputProcessor.getNextTime(), got -1 from timeCursor.getNextTimeInternal()"));
+ } else {
+ return DateTimeUtils.dateTimeFromUnixSeconds(nextTime);
+ }
+ }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/times/TimelineCoder.java b/usage/src/main/java/com/ning/billing/usage/timeline/times/TimelineCoder.java
new file mode 100644
index 0000000..f010417
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/times/TimelineCoder.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.times;
+
+import java.util.List;
+
+import org.joda.time.DateTime;
+
+public interface TimelineCoder {
+
+ /**
+ * Compress the list of DateTimes, producing the bytes of a timeline
+ *
+ * @param dateTimes a list of DateTimes to compress
+ * @return the bytes of the resulting timeline
+ */
+ public byte[] compressDateTimes(final List<DateTime> dateTimes);
+
+ /**
+ * Decompress the timeline bytes argument, returning a list of DateTimes
+ * Currently only used by tests.
+ *
+ * @param compressedTimes the timeline bytes
+ * @return a list of DateTimes representing the timeline times
+ */
+ public List<DateTime> decompressDateTimes(final byte[] compressedTimes);
+
+ /**
+ * Recode and combine the list of timeline byte objects, returning a single timeline.
+ * If the sampleCount is non-null and is not equal to the sum of the sample counts
+ * of the list of timelines, throw an error
+ *
+ * @param timesList a list of timeline byte arrays
+ * @param sampleCount the expected count of samples for all timeline byte arrays
+ * @return the combined timeline
+ */
+ public byte[] combineTimelines(final List<byte[]> timesList, final Integer sampleCount);
+
+ /**
+ * Return a count of the time samples in the timeline provided
+ *
+ * @param timeBytes the bytes of a timeline
+ * @return the count of samples represented in the timeline
+ */
+ public int countTimeBytesSamples(final byte[] timeBytes);
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/times/TimelineCursor.java b/usage/src/main/java/com/ning/billing/usage/timeline/times/TimelineCursor.java
new file mode 100644
index 0000000..474526a
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/times/TimelineCursor.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.times;
+
+import org.joda.time.DateTime;
+
+public interface TimelineCursor {
+
+ /**
+ * Skip to the given sample number within the timeline, where 0 is the first sample number
+ *
+ * @param finalSampleNumber the sample number to skip to
+ */
+ public void skipToSampleNumber(final int finalSampleNumber);
+
+ /**
+ * Return the DateTime for the next sample
+ *
+ * @return the DateTime for the next sample. If we've run out of samples, return null
+ */
+ public DateTime getNextTime();
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/times/TimelineOpcode.java b/usage/src/main/java/com/ning/billing/usage/timeline/times/TimelineOpcode.java
new file mode 100644
index 0000000..73b3420
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/times/TimelineOpcode.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.times;
+
+/**
+ * Opcodes are 1-byte entities. Any "opcode" whose value is 240 or less is treated
+ * as a time delta to be added to the previous time value.
+ */
+public enum TimelineOpcode {
+ FULL_TIME(0xFF), // Followed by 4 bytes of int value
+ REPEATED_DELTA_TIME_BYTE(0xFE), // Followed by a byte repeat count byte, 1-255, and then by a 1-byte delta whose value is 1-240
+ REPEATED_DELTA_TIME_SHORT(0xFD); // Followed by a repeat count short, 1-65535, and then by a 1-byte delta whose value is 1-240
+
+ public static final int MAX_DELTA_TIME = 0xF0; // 240: Leaves room for 16 other opcodes, of which 3 are used
+
+ private final int opcodeIndex;
+
+ private TimelineOpcode(final int opcodeIndex) {
+ this.opcodeIndex = opcodeIndex;
+ }
+
+ public int getOpcodeIndex() {
+ return opcodeIndex;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("TimelineOpcode");
+ sb.append("{opcodeIndex=").append(opcodeIndex);
+ sb.append('}');
+ return sb.toString();
+ }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/util/DateTimeUtils.java b/usage/src/main/java/com/ning/billing/usage/timeline/util/DateTimeUtils.java
new file mode 100644
index 0000000..c5222d7
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/util/DateTimeUtils.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.util;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+
+public class DateTimeUtils {
+
+ public static DateTime dateTimeFromUnixSeconds(final int unixTime) {
+ return new DateTime(((long) unixTime) * 1000L, DateTimeZone.UTC);
+ }
+
+ public static int unixSeconds(final DateTime dateTime) {
+ final long millis = dateTime.toDateTime(DateTimeZone.UTC).getMillis();
+ return (int) (millis / 1000L);
+ }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/util/Hex.java b/usage/src/main/java/com/ning/billing/usage/timeline/util/Hex.java
new file mode 100644
index 0000000..032b943
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/util/Hex.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.util;
+
+/**
+ * Hex utilities from commons-codec
+ */
+public class Hex {
+
+ /**
+ * Used to build output as Hex
+ */
+ private static final char[] DIGITS_LOWER = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
+
+ /**
+ * Used to build output as Hex
+ */
+ private static final char[] DIGITS_UPPER = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
+
+ private Hex() {
+ }
+
+ /**
+ * Converts an array of bytes into an array of characters representing the hexadecimal values of each byte in order.
+ * The returned array will be double the length of the passed array, as it takes two characters to represent any
+ * given byte.
+ *
+ * @param data a byte[] to convert to Hex characters
+ * @return A char[] containing hexadecimal characters
+ */
+ public static char[] encodeHex(final byte[] data) {
+ return encodeHex(data, true);
+ }
+
+ /**
+ * Converts an array of bytes into an array of characters representing the hexadecimal values of each byte in order.
+ * The returned array will be double the length of the passed array, as it takes two characters to represent any
+ * given byte.
+ *
+ * @param data a byte[] to convert to Hex characters
+ * @param toLowerCase <code>true</code> converts to lowercase, <code>false</code> to uppercase
+ * @return A char[] containing hexadecimal characters
+ * @since 1.4
+ */
+ public static char[] encodeHex(final byte[] data, final boolean toLowerCase) {
+ return encodeHex(data, toLowerCase ? DIGITS_LOWER : DIGITS_UPPER);
+ }
+
+ /**
+ * Converts an array of bytes into an array of characters representing the hexadecimal values of each byte in order.
+ * The returned array will be double the length of the passed array, as it takes two characters to represent any
+ * given byte.
+ *
+ * @param data a byte[] to convert to Hex characters
+ * @param toDigits the output alphabet
+ * @return A char[] containing hexadecimal characters
+ * @since 1.4
+ */
+ public static char[] encodeHex(final byte[] data, final char[] toDigits) {
+ final int l = data.length;
+ final char[] out = new char[l << 1];
+ // two characters form the hex value.
+ for (int i = 0, j = 0; i < l; i++) {
+ out[j++] = toDigits[(0xF0 & data[i]) >>> 4];
+ out[j++] = toDigits[0x0F & data[i]];
+ }
+
+ return out;
+ }
+
+ /**
+ * Converts an array of characters representing hexadecimal values into an array of bytes of those same values. The
+ * returned array will be half the length of the passed array, as it takes two characters to represent any given
+ * byte. An exception is thrown if the passed char array has an odd number of elements.
+ *
+ * @param data An array of characters containing hexadecimal digits
+ * @return A byte array containing binary data decoded from the supplied char array.
+ * @throws IllegalArgumentException Thrown if an odd number or illegal of characters is supplied
+ */
+ public static byte[] decodeHex(final char[] data) throws IllegalArgumentException {
+ final int len = data.length;
+ if ((len & 0x01) != 0) {
+ throw new IllegalArgumentException("Odd number of characters.");
+ }
+
+ final byte[] out = new byte[len >> 1];
+
+ // two characters form the hex value.
+ for (int i = 0, j = 0; j < len; i++) {
+ int f = toDigit(data[j], j) << 4;
+ j++;
+ f = f | toDigit(data[j], j);
+ j++;
+ out[i] = (byte) (f & 0xFF);
+ }
+
+ return out;
+ }
+
+ /**
+ * Converts a hexadecimal character to an integer.
+ *
+ * @param ch A character to convert to an integer digit
+ * @param index The index of the character in the source
+ * @return An integer
+ * @throws IllegalArgumentException Thrown if ch is an illegal hex character
+ */
+ private static int toDigit(final char ch, final int index) throws IllegalArgumentException {
+ final int digit = Character.digit(ch, 16);
+ if (digit == -1) {
+ throw new IllegalArgumentException("Illegal hexadecimal character " + ch + " at index " + index);
+ }
+ return digit;
+ }
+}
diff --git a/usage/src/main/resources/com/ning/billing/usage/ddl.sql b/usage/src/main/resources/com/ning/billing/usage/ddl.sql
new file mode 100644
index 0000000..fab0ce2
--- /dev/null
+++ b/usage/src/main/resources/com/ning/billing/usage/ddl.sql
@@ -0,0 +1,67 @@
+create table sources (
+ record_id int(11) unsigned not null auto_increment
+, bundle_id char(36) default null
+, subscription_id char(36) default null
+, created_date datetime default null
+, created_by varchar(50) default null
+, updated_date datetime default null
+, updated_by varchar(50) default null
+, account_record_id int(11) unsigned default null
+, tenant_record_id int(11) unsigned default null
+, primary key(record_id)
+, index created_dt_record_id_dx (created_dt, record_id)
+) engine = innodb default charset = latin1;
+
+create table event_categories (
+ event_category_id integer not null auto_increment
+, event_category varchar(256) not null
+, tenant_record_id int(11) unsigned default null
+, primary key(record_id)
+, unique index event_category_unq (event_category)
+) engine = innodb default charset = latin1;
+
+create table metrics (
+ record_id int(11) unsigned not null auto_increment
+, event_category_id integer not null
+, metric varchar(256) not null
+, tenant_record_id int(11) unsigned default null
+, primary key(record_id)
+, unique index metric_unq (event_category_id, metric)
+) engine = innodb default charset = latin1;
+
+create table timeline_chunks (
+ record_id bigint not null auto_increment
+, source_id integer not null
+, metric_id integer not null
+, sample_count integer not null
+, start_time integer not null
+, end_time integer not null
+, not_valid tinyint default 0
+, aggregation_level tinyint default 0
+, dont_aggregate tinyint default 0
+, in_row_samples varbinary(400) default null
+, blob_samples mediumblob default null
+, primary key(record_id)
+, unique index source_id_timeline_chunk_metric_idx (source_id, metric_id, start_time, aggregation_level)
+, index valid_agg_host_start_time (not_valid, aggregation_level, source_id, metric_id, start_time)
+) engine = innodb default charset = latin1;
+
+create table last_start_times (
+ time_inserted int not null primary key
+, start_times mediumtext not null
+) engine = innodb default charset = latin1;
+
+insert ignore into timeline_chunks(chunk_id, source_id, metric_id, sample_count, start_time, end_time, in_row_samples, blob_samples)
+ values (0, 0, 0, 0, 0, 0, null, null);
+
+create table timeline_rolled_up_chunk (
+ record_id bigint not null auto_increment
+, source_id integer not null
+, metric_id integer not null
+, start_time date not null
+, end_time date not null
+, value bigint not null
+, account_record_id int(11) unsigned default null
+, tenant_record_id int(11) unsigned default null
+, primary key(record_id)
+) engine = innodb default charset = latin1;
diff --git a/usage/src/main/resources/com/ning/billing/usage/timeline/aggregator/TimelineAggregatorSqlDao.sql.stg b/usage/src/main/resources/com/ning/billing/usage/timeline/aggregator/TimelineAggregatorSqlDao.sql.stg
new file mode 100644
index 0000000..341ea3a
--- /dev/null
+++ b/usage/src/main/resources/com/ning/billing/usage/timeline/aggregator/TimelineAggregatorSqlDao.sql.stg
@@ -0,0 +1,60 @@
+group TimelineAggregatorDAO;
+
+getStreamingAggregationCandidates() ::= <<
+ select
+ chunk_id
+ , source_id
+ , metric_id
+ , start_time
+ , end_time
+ , in_row_samples
+ , blob_samples
+ , sample_count
+ , aggregation_level
+ , not_valid
+ , dont_aggregate
+ from timeline_chunks
+ where source_id != 0 and aggregation_level = :aggregationLevel and not_valid = 0
+ order by source_id, metric_id, start_time
+ >>
+
+ getAggregationCandidatesForSourceIdAndMetricIds(metricIds) ::= <<
+ select
+ chunk_id
+ , source_id
+ , metric_id
+ , start_time
+ , end_time
+ , in_row_samples
+ , blob_samples
+ , sample_count
+ , aggregation_level
+ , not_valid
+ , dont_aggregate
+ from timeline_chunks
+ where source_id = :source_id
+ and metric_id in (<metricIds>)
+ ;
+>>
+
+getLastInsertedId() ::= <<
+ select last_insert_id();
+>>
+
+makeTimelineChunkValid() ::= <<
+ update timeline_chunks
+ set not_valid = 0
+ where chunk_id = :chunkId
+ ;
+>>
+
+makeTimelineChunksInvalid(chunkIds) ::=<<
+ update timeline_chunks
+ set not_valid = 1
+ where chunk_id in (<chunkIds>)
+ ;
+>>
+
+deleteTimelineChunks(chunkIds) ::=<<
+ delete from timeline_chunks where chunk_id in (<chunkIds>);
+>>
diff --git a/usage/src/main/resources/com/ning/billing/usage/timeline/persistent/TimelineSqlDao.sql.stg b/usage/src/main/resources/com/ning/billing/usage/timeline/persistent/TimelineSqlDao.sql.stg
new file mode 100644
index 0000000..1e0364b
--- /dev/null
+++ b/usage/src/main/resources/com/ning/billing/usage/timeline/persistent/TimelineSqlDao.sql.stg
@@ -0,0 +1,181 @@
+group TimelineSqlDao;
+
+getSource() ::= <<
+ select
+ source_name
+ from sources
+ where source_id = :sourceId
+ ;
+>>
+
+getSources() ::= <<
+ select
+ source_id
+ , source_name
+ from sources
+ ;
+>>
+
+addSource() ::= <<
+ insert ignore into sources (source_name, created_dt)
+ values (:sourceName, unix_timestamp());
+>>
+
+getEventCategories() ::= <<
+ select event_category_id, event_category
+ from event_categories
+ order by event_category_id asc
+ ;
+>>
+
+getEventCategoryId() ::= <<
+ select
+ event_category_id
+ from event_categories
+ where event_category = :eventCategory
+ ;
+>>
+
+getEventCategory() ::= <<
+ select
+ event_category
+ from event_categories
+ where event_category_id = :eventCategoryId
+ ;
+>>
+
+addEventCategory() ::= <<
+ insert ignore into event_categories (event_category)
+ values (:eventCategory);
+>>
+
+getMetricId() ::= <<
+ select
+ metric_id
+ from metrics
+ where metric = :metric
+ and event_category_id = :eventCategoryId
+ ;
+>>
+
+getEventCategoryIdAndMetric() ::= <<
+ select
+ event_category_id
+ , metric
+ from metrics
+ where metric_id = :metricId
+ ;
+>>
+
+getMetric() ::= <<
+ select
+ metric
+ from metrics
+ where metric_id = :metricId
+ ;
+>>
+
+addMetric() ::= <<
+ insert ignore into metrics (event_category_id, metric)
+ values (:eventCategoryId, :metric);
+>>
+
+getMetricIdsBySourceId() ::= <<
+ select distinct metric_id
+ from timeline_chunks c
+ where source_id = :sourceId
+ ;
+>>
+
+getMetricIdsForAllSources() ::= <<
+ select distinct metric_id, source_id
+ from timeline_chunks c
+ ;
+>>
+
+getMetrics() ::= <<
+ select
+ metric_id
+ , event_category_id
+ , metric
+ from metrics
+ ;
+>>
+
+getLastInsertedId() ::= <<
+ select last_insert_id();
+>>
+
+insertTimelineChunk() ::= <<
+ insert into timeline_chunks (record_id, source_id, metric_id, sample_count, start_time, end_time, in_row_samples, blob_samples, aggregation_level, not_valid, dont_aggregate)
+ values (:chunkId, :sourceId, :metricId, :sampleCount, :startTime, :endTime, :inRowSamples, :blobSamples, :aggregationLevel, :notValid, :dontAggregate);
+>>
+
+getSamplesBySourceIdsAndMetricIds(sourceIds, metricIds) ::= <<
+ select
+ source_id
+ , metric_id
+ , record_id
+ , sample_count
+ , in_row_samples
+ , blob_samples
+ , start_time
+ , end_time
+ , aggregation_level
+ , not_valid
+ , dont_aggregate
+ from timeline_chunks
+ where end_time >= :startTime
+ and start_time \<= :endTime
+ and source_id in (<sourceIds>)
+ <if(metricIds)>
+ and metric_id in (<metricIds>)
+ <endif>
+ and not_valid = 0
+ order by source_id, metric_id, start_time asc
+ ;
+>>
+
+insertLastStartTimes() ::= <<
+ insert into last_start_times (time_inserted, start_times)
+ values (:timeInserted, :startTimes)
+>>
+
+getLastStartTimes() ::= <<
+ select time_inserted, start_times
+ from last_start_times
+ order by time_inserted desc
+ limit 1
+>>
+
+deleteLastStartTimes() ::= <<
+ delete from last_start_times
+>>
+
+bulkInsertSources() ::= <<
+ insert into sources (source_name, created_dt)
+ values (:sourceName, unix_timestamp());
+>>
+
+bulkInsertEventCategories() ::= <<
+ insert into event_categories (event_category)
+ values (:eventCategory);
+>>
+
+bulkInsertMetrics() ::= <<
+ insert into metrics (event_category_id, metric)
+ values (:eventCategoryId, :metric);
+>>
+
+bulkInsertTimelineChunks() ::= <<
+ insert into timeline_chunks (record_id, source_id, metric_id, sample_count, start_time, end_time, not_valid, dont_aggregate, aggregation_level, in_row_samples, blob_samples)
+ values (:chunkId, :sourceId, :metricId, :sampleCount, :startTime, :endTime, :dontAggregate, :notValid, :aggregationLevel, :inRowSamples, :blobSamples);
+>>
+
+getHighestTimelineChunkId() ::= <<
+ select record_id from timeline_chunks order by record_id desc limit 1;
+>>
+
+test() ::= <<
+ select 1;
+>>
diff --git a/usage/src/test/java/com/ning/billing/usage/timeline/categories/TestCategoryAndMetrics.java b/usage/src/test/java/com/ning/billing/usage/timeline/categories/TestCategoryAndMetrics.java
new file mode 100644
index 0000000..4d455b1
--- /dev/null
+++ b/usage/src/test/java/com/ning/billing/usage/timeline/categories/TestCategoryAndMetrics.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.categories;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.ning.billing.usage.UsageTestSuite;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+public class TestCategoryAndMetrics extends UsageTestSuite {
+
+ @Test(groups = "fast")
+ public void testMapping() throws Exception {
+ final CategoryAndMetrics kinds = new CategoryAndMetrics("JVM");
+ kinds.addMetric("GC");
+ kinds.addMetric("CPU");
+
+ final ObjectMapper mapper = new ObjectMapper();
+ final String json = mapper.writeValueAsString(kinds);
+ Assert.assertEquals("{\"eventCategory\":\"JVM\",\"metrics\":[\"GC\",\"CPU\"]}", json);
+
+ final CategoryAndMetrics kindsFromJson = mapper.readValue(json, CategoryAndMetrics.class);
+ Assert.assertEquals(kindsFromJson, kinds);
+ }
+
+ @Test(groups = "fast")
+ public void testComparison() throws Exception {
+ final CategoryAndMetrics aKinds = new CategoryAndMetrics("JVM");
+ aKinds.addMetric("GC");
+ aKinds.addMetric("CPU");
+ Assert.assertEquals(aKinds.compareTo(aKinds), 0);
+
+ final CategoryAndMetrics bKinds = new CategoryAndMetrics("JVM");
+ bKinds.addMetric("GC");
+ bKinds.addMetric("CPU");
+ Assert.assertEquals(aKinds.compareTo(bKinds), 0);
+ Assert.assertEquals(bKinds.compareTo(aKinds), 0);
+
+ final CategoryAndMetrics cKinds = new CategoryAndMetrics("JVM");
+ cKinds.addMetric("GC");
+ cKinds.addMetric("CPU");
+ cKinds.addMetric("Something else");
+ Assert.assertTrue(aKinds.compareTo(cKinds) < 0);
+ Assert.assertTrue(cKinds.compareTo(aKinds) > 0);
+
+ final CategoryAndMetrics dKinds = new CategoryAndMetrics("ZVM");
+ dKinds.addMetric("GC");
+ Assert.assertTrue(aKinds.compareTo(dKinds) < 0);
+ Assert.assertTrue(dKinds.compareTo(aKinds) > 0);
+ }
+}
diff --git a/usage/src/test/java/com/ning/billing/usage/timeline/chunks/TestTimeBytesAndSampleBytes.java b/usage/src/test/java/com/ning/billing/usage/timeline/chunks/TestTimeBytesAndSampleBytes.java
new file mode 100644
index 0000000..a6620c5
--- /dev/null
+++ b/usage/src/test/java/com/ning/billing/usage/timeline/chunks/TestTimeBytesAndSampleBytes.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.chunks;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.ning.billing.usage.UsageTestSuite;
+
+public class TestTimeBytesAndSampleBytes extends UsageTestSuite {
+
+ @Test(groups = "fast")
+ public void testGetters() throws Exception {
+ final byte[] timeBytes = new byte[]{0x1, 0x2, 0x3};
+ final byte[] sampleBytes = new byte[]{0xA, 0xB, 0xC};
+ final TimeBytesAndSampleBytes timeBytesAndSampleBytes = new TimeBytesAndSampleBytes(timeBytes, sampleBytes);
+
+ Assert.assertEquals(timeBytesAndSampleBytes.getTimeBytes(), timeBytes);
+ Assert.assertEquals(timeBytesAndSampleBytes.getSampleBytes(), sampleBytes);
+ }
+
+ @Test(groups = "fast")
+ public void testEquals() throws Exception {
+ final byte[] timeBytes = new byte[]{0x1, 0x2, 0x3};
+ final byte[] sampleBytes = new byte[]{0xA, 0xB, 0xC};
+
+ final TimeBytesAndSampleBytes timeBytesAndSampleBytes = new TimeBytesAndSampleBytes(timeBytes, sampleBytes);
+ Assert.assertEquals(timeBytesAndSampleBytes, timeBytesAndSampleBytes);
+
+ final TimeBytesAndSampleBytes sameTimeBytesAndSampleBytes = new TimeBytesAndSampleBytes(timeBytes, sampleBytes);
+ Assert.assertEquals(sameTimeBytesAndSampleBytes, timeBytesAndSampleBytes);
+
+ final TimeBytesAndSampleBytes otherTimeBytesAndSampleBytes = new TimeBytesAndSampleBytes(sampleBytes, timeBytes);
+ Assert.assertNotEquals(otherTimeBytesAndSampleBytes, timeBytesAndSampleBytes);
+ }
+}
diff --git a/usage/src/test/java/com/ning/billing/usage/timeline/chunks/TestTimelineChunk.java b/usage/src/test/java/com/ning/billing/usage/timeline/chunks/TestTimelineChunk.java
new file mode 100644
index 0000000..e8c73f2
--- /dev/null
+++ b/usage/src/test/java/com/ning/billing/usage/timeline/chunks/TestTimelineChunk.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.chunks;
+
+import org.joda.time.DateTime;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.ning.billing.usage.UsageTestSuite;
+import com.ning.billing.util.clock.Clock;
+import com.ning.billing.util.clock.ClockMock;
+
+public class TestTimelineChunk extends UsageTestSuite {
+
+ private final Clock clock = new ClockMock();
+
+ @Test(groups = "fast")
+ public void testGetters() throws Exception {
+ final long chunkId = 0L;
+ final int sourceId = 1;
+ final int metricId = 2;
+ final DateTime startTime = clock.getUTCNow();
+ final DateTime endTime = startTime.plusDays(2);
+ final byte[] timeBytes = new byte[]{0x1, 0x2, 0x3};
+ final byte[] sampleBytes = new byte[]{0xA, 0xB, 0xC};
+ final TimelineChunk timelineChunk = new TimelineChunk(chunkId, sourceId, metricId, startTime, endTime, timeBytes, sampleBytes, timeBytes.length);
+
+ Assert.assertEquals(timelineChunk.getChunkId(), chunkId);
+ Assert.assertEquals(timelineChunk.getSourceId(), sourceId);
+ Assert.assertEquals(timelineChunk.getMetricId(), metricId);
+ Assert.assertEquals(timelineChunk.getStartTime(), startTime);
+ Assert.assertEquals(timelineChunk.getEndTime(), endTime);
+ Assert.assertEquals(timelineChunk.getTimeBytesAndSampleBytes().getTimeBytes(), timeBytes);
+ Assert.assertEquals(timelineChunk.getTimeBytesAndSampleBytes().getSampleBytes(), sampleBytes);
+ Assert.assertEquals(timelineChunk.getAggregationLevel(), 0);
+ Assert.assertFalse(timelineChunk.getNotValid());
+ Assert.assertFalse(timelineChunk.getDontAggregate());
+ }
+
+ @Test(groups = "fast")
+ public void testEquals() throws Exception {
+ final long chunkId = 0L;
+ final int sourceId = 1;
+ final int metricId = 2;
+ final DateTime startTime = clock.getUTCNow();
+ final DateTime endTime = startTime.plusDays(2);
+ final byte[] timeBytes = new byte[]{0x1, 0x2, 0x3};
+ final byte[] sampleBytes = new byte[]{0xA, 0xB, 0xC};
+
+ final TimelineChunk timelineChunk = new TimelineChunk(chunkId, sourceId, metricId, startTime, endTime, timeBytes, sampleBytes, timeBytes.length);
+ Assert.assertEquals(timelineChunk, timelineChunk);
+
+ final TimelineChunk sameTimelineChunk = new TimelineChunk(chunkId, sourceId, metricId, startTime, endTime, timeBytes, sampleBytes, timeBytes.length);
+ Assert.assertEquals(sameTimelineChunk, timelineChunk);
+
+ final TimelineChunk otherTimelineChunk = new TimelineChunk(sourceId, sourceId, metricId, startTime, endTime, timeBytes, sampleBytes, timeBytes.length);
+ Assert.assertNotEquals(otherTimelineChunk, timelineChunk);
+ }
+}
diff --git a/usage/src/test/java/com/ning/billing/usage/timeline/codec/TestEncodedBytesAndSampleCount.java b/usage/src/test/java/com/ning/billing/usage/timeline/codec/TestEncodedBytesAndSampleCount.java
new file mode 100644
index 0000000..4ab2631
--- /dev/null
+++ b/usage/src/test/java/com/ning/billing/usage/timeline/codec/TestEncodedBytesAndSampleCount.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.codec;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.ning.billing.usage.UsageTestSuite;
+
+public class TestEncodedBytesAndSampleCount extends UsageTestSuite {
+
+ @Test(groups = "fast")
+ public void testGetters() throws Exception {
+ final byte[] encodedBytes = {0xA, 0xB};
+ final int sampleCount = 20;
+ final EncodedBytesAndSampleCount encodedBytesAndSampleCount = new EncodedBytesAndSampleCount(encodedBytes, sampleCount);
+
+ Assert.assertEquals(encodedBytesAndSampleCount.getEncodedBytes(), encodedBytes);
+ Assert.assertEquals(encodedBytesAndSampleCount.getSampleCount(), sampleCount);
+ }
+
+ @Test(groups = "fast")
+ public void testEquals() throws Exception {
+ final byte[] encodedBytes = {0xA, 0xB};
+ final int sampleCount = 20;
+
+ final EncodedBytesAndSampleCount encodedBytesAndSampleCount = new EncodedBytesAndSampleCount(encodedBytes, sampleCount);
+ Assert.assertEquals(encodedBytesAndSampleCount, encodedBytesAndSampleCount);
+
+ final EncodedBytesAndSampleCount sameEncodedBytesAndSampleCount = new EncodedBytesAndSampleCount(encodedBytes, sampleCount);
+ Assert.assertEquals(sameEncodedBytesAndSampleCount, encodedBytesAndSampleCount);
+
+ final EncodedBytesAndSampleCount otherEncodedBytesAndSampleCount = new EncodedBytesAndSampleCount(encodedBytes, sampleCount + 1);
+ Assert.assertNotEquals(otherEncodedBytesAndSampleCount, encodedBytesAndSampleCount);
+ }
+}
diff --git a/usage/src/test/java/com/ning/billing/usage/timeline/codec/TestSampleCoder.java b/usage/src/test/java/com/ning/billing/usage/timeline/codec/TestSampleCoder.java
new file mode 100644
index 0000000..65529a9
--- /dev/null
+++ b/usage/src/test/java/com/ning/billing/usage/timeline/codec/TestSampleCoder.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.codec;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.joda.time.format.DateTimeFormatter;
+import org.joda.time.format.ISODateTimeFormat;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.ning.billing.usage.UsageTestSuite;
+import com.ning.billing.usage.timeline.samples.RepeatSample;
+import com.ning.billing.usage.timeline.samples.SampleOpcode;
+import com.ning.billing.usage.timeline.samples.ScalarSample;
+import com.ning.billing.usage.timeline.times.DefaultTimelineCoder;
+import com.ning.billing.usage.timeline.times.DefaultTimelineCursor;
+import com.ning.billing.usage.timeline.times.TimelineCoder;
+import com.ning.billing.usage.timeline.times.TimelineCursor;
+import com.ning.billing.usage.timeline.util.DateTimeUtils;
+import com.ning.billing.usage.timeline.util.Hex;
+
+import com.google.common.collect.ImmutableList;
+
+public class TestSampleCoder extends UsageTestSuite {
+
+ private static final TimelineCoder timelineCoder = new DefaultTimelineCoder();
+ private static final DateTimeFormatter dateFormatter = ISODateTimeFormat.dateTime().withZone(DateTimeZone.UTC);
+ private static final SampleCoder sampleCoder = new DefaultSampleCoder();
+
+ @Test(groups = "fast")
+ public void testScan() throws Exception {
+ final DateTime startTime = new DateTime(DateTimeZone.UTC);
+ final DateTime endTime = startTime.plusSeconds(5);
+ final List<DateTime> dateTimes = ImmutableList.<DateTime>of(startTime.plusSeconds(1), startTime.plusSeconds(2), startTime.plusSeconds(3), startTime.plusSeconds(4));
+ final byte[] compressedTimes = timelineCoder.compressDateTimes(dateTimes);
+
+
+ final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ final DataOutputStream dataOutputStream = new DataOutputStream(outputStream);
+ final ScalarSample<Short> sample = new ScalarSample<Short>(SampleOpcode.SHORT, (short) 4);
+ sampleCoder.encodeSample(dataOutputStream, sample);
+ sampleCoder.encodeSample(dataOutputStream, new RepeatSample<Short>(3, sample));
+ dataOutputStream.close();
+
+ sampleCoder.scan(outputStream.toByteArray(), compressedTimes, dateTimes.size(), new TimeRangeSampleProcessor(startTime, endTime) {
+ @Override
+ public void processOneSample(final DateTime time, final SampleOpcode opcode, final Object value) {
+ Assert.assertTrue(time.isAfter(startTime));
+ Assert.assertTrue(time.isBefore(endTime));
+ Assert.assertEquals(Short.valueOf(value.toString()), sample.getSampleValue());
+ }
+ });
+ }
+
+ @Test(groups = "fast")
+ public void testTimeRangeSampleProcessor() throws Exception {
+ final DateTime startTime = new DateTime(dateFormatter.parseDateTime("2012-03-23T17:35:11.000Z"));
+ final DateTime endTime = new DateTime(dateFormatter.parseDateTime("2012-03-23T17:35:17.000Z"));
+ final int sampleCount = 2;
+
+ final List<DateTime> dateTimes = ImmutableList.<DateTime>of(startTime, endTime);
+ final byte[] compressedTimes = timelineCoder.compressDateTimes(dateTimes);
+ final TimelineCursor cursor = new DefaultTimelineCursor(compressedTimes, sampleCount);
+ Assert.assertEquals(cursor.getNextTime(), startTime);
+ Assert.assertEquals(cursor.getNextTime(), endTime);
+
+ // 2 x the value 12: REPEAT_BYTE, SHORT, 2, SHORT, 12 (2 bytes)
+ final byte[] samples = new byte[]{(byte) 0xff, 2, 2, 0, 12};
+
+ final AtomicInteger samplesCount = new AtomicInteger(0);
+ sampleCoder.scan(samples, compressedTimes, sampleCount, new TimeRangeSampleProcessor(startTime, endTime) {
+ @Override
+ public void processOneSample(final DateTime time, final SampleOpcode opcode, final Object value) {
+ if (samplesCount.get() == 0) {
+ Assert.assertEquals(DateTimeUtils.unixSeconds(time), DateTimeUtils.unixSeconds(startTime));
+ } else {
+ Assert.assertEquals(DateTimeUtils.unixSeconds(time), DateTimeUtils.unixSeconds(endTime));
+ }
+ samplesCount.incrementAndGet();
+ }
+ });
+ Assert.assertEquals(samplesCount.get(), sampleCount);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test(groups = "fast")
+ public void testCombineSampleBytes() throws Exception {
+ final ScalarSample[] samplesToChoose = new ScalarSample[]{new ScalarSample(SampleOpcode.DOUBLE, 2.0),
+ new ScalarSample(SampleOpcode.DOUBLE, 1.0),
+ new ScalarSample(SampleOpcode.INT_ZERO, 0)};
+ final int[] repetitions = new int[]{1, 2, 3, 4, 5, 240, 250, 300};
+ final Random rand = new Random(0);
+ int count = 0;
+ final TimelineChunkAccumulator accum = new TimelineChunkAccumulator(0, 0, sampleCoder);
+ final List<ScalarSample> samples = new ArrayList<ScalarSample>();
+ for (int i = 0; i < 20; i++) {
+ final ScalarSample sample = samplesToChoose[rand.nextInt(samplesToChoose.length)];
+ final int repetition = repetitions[rand.nextInt(repetitions.length)];
+ for (int r = 0; r < repetition; r++) {
+ samples.add(sample);
+ accum.addSample(sample);
+ count++;
+ }
+ }
+ final byte[] sampleBytes = sampleCoder.compressSamples(samples);
+ final byte[] accumBytes = accum.getEncodedSamples().getEncodedBytes();
+ Assert.assertEquals(accumBytes, sampleBytes);
+ final List<ScalarSample> restoredSamples = sampleCoder.decompressSamples(sampleBytes);
+ Assert.assertEquals(restoredSamples.size(), samples.size());
+ for (int i = 0; i < count; i++) {
+ Assert.assertEquals(restoredSamples.get(i), samples.get(i));
+ }
+ for (int fragmentLength = 2; fragmentLength < count / 2; fragmentLength++) {
+ final List<byte[]> fragments = new ArrayList<byte[]>();
+ final int fragmentCount = (int) Math.ceil((double) count / (double) fragmentLength);
+ for (int fragCounter = 0; fragCounter < fragmentCount; fragCounter++) {
+ final int fragIndex = fragCounter * fragmentLength;
+ final List<ScalarSample> fragment = samples.subList(fragIndex, Math.min(count, fragIndex + fragmentLength));
+ fragments.add(sampleCoder.compressSamples(fragment));
+ }
+ final byte[] combined = sampleCoder.combineSampleBytes(fragments);
+ final List<ScalarSample> restored = sampleCoder.decompressSamples(combined);
+ Assert.assertEquals(restored.size(), samples.size());
+ for (int i = 0; i < count; i++) {
+ Assert.assertEquals(restored.get(i), samples.get(i));
+ }
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test(groups = "fast")
+ public void testCombineMoreThan65KSamples() throws Exception {
+ final int count = 0;
+ final TimelineChunkAccumulator accum = new TimelineChunkAccumulator(0, 0, sampleCoder);
+ final List<ScalarSample> samples = new ArrayList<ScalarSample>();
+ final ScalarSample sample1 = new ScalarSample(SampleOpcode.BYTE, (byte) 1);
+ final ScalarSample sample2 = new ScalarSample(SampleOpcode.BYTE, (byte) 2);
+ for (int i = 0; i < 20; i++) {
+ samples.add(sample1);
+ accum.addSample(sample1);
+ }
+ for (int i = 0; i < 0xFFFF + 100; i++) {
+ samples.add(sample2);
+ accum.addSample(sample2);
+ }
+ final byte[] sampleBytes = sampleCoder.compressSamples(samples);
+ final String hex = new String(Hex.encodeHex(sampleBytes));
+ // Here are the compressed samples: ff140101feffff0102ff640102
+ // Translation:
+ // [ff 14 01 01] means repeat 20 times BYTE value 1
+ // [fe ff ff 01 02] means repeat 65525 times BYTE value 2
+ // [ff 64 01 02] means repeat 100 times BYTE value 2
+ Assert.assertEquals(sampleBytes, Hex.decodeHex("ff140101feffff0102ff640102".toCharArray()));
+ final List<ScalarSample> restoredSamples = sampleCoder.decompressSamples(sampleBytes);
+ Assert.assertEquals(restoredSamples.size(), samples.size());
+ for (int i = 0; i < count; i++) {
+ Assert.assertEquals(restoredSamples.get(i), samples.get(i));
+ }
+ }
+
+ /*
+ * I saw an error in combineSampleBytes:
+ * java.lang.ClassCastException: java.lang.Double cannot be cast to java.lang.Short
+ * These were the inputs:
+ * [11, 44, 74, -1, 2, 15, 11, 40, 68, -1, 2, 15]
+ * meaning half-float-for-double; repeat 2 times double zero; half-float-for-double; repeat 2 time double zero
+ * [11, 44, 68, -1, 3, 15, 11, 40, 68]
+ * meaning meaning half-float-for-double; repeat 3 times double zero; half-float-for-double
+ * [-1, 3, 15, 11, 40, 68, -1, 2, 15, 11, 40, 68]
+ * meaning repeat 3 times double-zero; half-float-for-double; repeat 2 times double zero; half-float-for-double
+ * [-1, 2, 11, 40, 68, -1, 3, 15, 11, 40, 68, 15]
+ * meaning repeat 2 times half-float-for-double; repeat 3 times double-zero; half-float-for-double; double zero
+ */
+ @SuppressWarnings("unchecked")
+ @Test(groups = "fast")
+ public void testCombineError() throws Exception {
+ final byte[] b1 = new byte[]{11, 44, 74, -1, 2, 15, 11, 40, 68, -1, 2, 15};
+ final byte[] b2 = new byte[]{11, 44, 68, -1, 3, 15, 11, 40, 68};
+ final byte[] b3 = new byte[]{-1, 3, 15, 11, 40, 68, -1, 2, 15, 11, 40, 68};
+ final byte[] b4 = new byte[]{-1, 2, 11, 40, 68, -1, 3, 15, 11, 40, 68, 15};
+ final List<byte[]> parts = new ArrayList<byte[]>();
+ parts.add(b1);
+ parts.add(b2);
+ parts.add(b3);
+ parts.add(b4);
+ final byte[] combinedBytes = sampleCoder.combineSampleBytes(parts);
+ final List<ScalarSample> samples = sampleCoder.decompressSamples(combinedBytes);
+ Assert.assertEquals(samples.size(), 25);
+ }
+}
diff --git a/usage/src/test/java/com/ning/billing/usage/timeline/codec/TestSampleCompression.java b/usage/src/test/java/com/ning/billing/usage/timeline/codec/TestSampleCompression.java
new file mode 100644
index 0000000..d572661
--- /dev/null
+++ b/usage/src/test/java/com/ning/billing/usage/timeline/codec/TestSampleCompression.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.codec;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.joda.time.DateTime;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.ning.billing.usage.UsageTestSuite;
+import com.ning.billing.usage.timeline.samples.SampleOpcode;
+import com.ning.billing.usage.timeline.samples.ScalarSample;
+import com.ning.billing.usage.timeline.times.DefaultTimelineCoder;
+import com.ning.billing.usage.timeline.times.TimelineCoder;
+
+public class TestSampleCompression extends UsageTestSuite {
+
+ private static final TimelineCoder timelineCoder = new DefaultTimelineCoder();
+ private static final SampleCoder sampleCoder = new DefaultSampleCoder();
+
+ @Test(groups = "fast")
+ public void testBasicDoubleCompression() throws Exception {
+
+ checkDoubleCodedResult(0.0, SampleOpcode.DOUBLE_ZERO, 1);
+ checkDoubleCodedResult(1.0, SampleOpcode.BYTE_FOR_DOUBLE, 2);
+ checkDoubleCodedResult(1.005, SampleOpcode.BYTE_FOR_DOUBLE, 2);
+ checkDoubleCodedResult(127.2, SampleOpcode.BYTE_FOR_DOUBLE, 2);
+ checkDoubleCodedResult(-128.2, SampleOpcode.BYTE_FOR_DOUBLE, 2);
+
+ checkDoubleCodedResult(65503.0, SampleOpcode.HALF_FLOAT_FOR_DOUBLE, 3);
+ checkDoubleCodedResult(-65503.0, SampleOpcode.HALF_FLOAT_FOR_DOUBLE, 3);
+ checkDoubleCodedResult(6.1e-5, SampleOpcode.HALF_FLOAT_FOR_DOUBLE, 3);
+ checkDoubleCodedResult(-6.1e-5, SampleOpcode.HALF_FLOAT_FOR_DOUBLE, 3);
+
+ checkDoubleCodedResult(200.0, SampleOpcode.SHORT_FOR_DOUBLE, 3);
+ checkDoubleCodedResult(32767.0, SampleOpcode.SHORT_FOR_DOUBLE, 3);
+ checkDoubleCodedResult(-200.0, SampleOpcode.SHORT_FOR_DOUBLE, 3);
+ checkDoubleCodedResult(-32768.0, SampleOpcode.SHORT_FOR_DOUBLE, 3);
+
+ checkDoubleCodedResult((double) Float.MAX_VALUE, SampleOpcode.FLOAT_FOR_DOUBLE, 5);
+ checkDoubleCodedResult((double) Float.MIN_VALUE, SampleOpcode.FLOAT_FOR_DOUBLE, 5);
+
+ checkDoubleCodedResult(((double) Float.MAX_VALUE) * 10.0, SampleOpcode.DOUBLE, 9);
+ }
+
+ @SuppressWarnings("unchecked")
+ private void checkDoubleCodedResult(final double value, final SampleOpcode expectedOpcode, final int expectedSize) {
+ final ScalarSample codedSample = sampleCoder.compressSample(new ScalarSample(SampleOpcode.DOUBLE, value));
+ Assert.assertEquals(codedSample.getOpcode(), expectedOpcode);
+ final double error = value == 0.0 ? 0.0 : Math.abs((value - codedSample.getDoubleValue()) / value);
+ Assert.assertTrue(error <= sampleCoder.getMaxFractionError());
+ final TimelineChunkAccumulator accum = new TimelineChunkAccumulator(123, 456, sampleCoder);
+ accum.addSample(codedSample);
+ final DateTime now = new DateTime();
+ final List<DateTime> dateTimes = new ArrayList<DateTime>();
+ dateTimes.add(now);
+ final byte[] timeBytes = timelineCoder.compressDateTimes(dateTimes);
+ final byte[] encodedSampleBytes = accum.extractTimelineChunkAndReset(now, now, timeBytes).getTimeBytesAndSampleBytes().getSampleBytes();
+ Assert.assertEquals(encodedSampleBytes.length, expectedSize);
+ }
+}
diff --git a/usage/src/test/java/com/ning/billing/usage/timeline/codec/TestTimelineChunkAccumulator.java b/usage/src/test/java/com/ning/billing/usage/timeline/codec/TestTimelineChunkAccumulator.java
new file mode 100644
index 0000000..1afa1ae
--- /dev/null
+++ b/usage/src/test/java/com/ning/billing/usage/timeline/codec/TestTimelineChunkAccumulator.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.codec;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.joda.time.DateTime;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.ning.billing.usage.UsageTestSuite;
+import com.ning.billing.usage.timeline.chunks.TimelineChunk;
+import com.ning.billing.usage.timeline.samples.SampleOpcode;
+import com.ning.billing.usage.timeline.samples.ScalarSample;
+import com.ning.billing.usage.timeline.times.DefaultTimelineCoder;
+import com.ning.billing.usage.timeline.times.TimelineCoder;
+import com.ning.billing.usage.timeline.times.TimelineCursor;
+
+public class TestTimelineChunkAccumulator extends UsageTestSuite {
+
+ private static final TimelineCoder timelineCoder = new DefaultTimelineCoder();
+ private static final SampleCoder sampleCoder = new DefaultSampleCoder();
+
+ @SuppressWarnings("unchecked")
+ @Test(groups = "fast")
+ public void testBasicAccumulator() throws Exception {
+ final int hostId = 123;
+ final int sampleKindId = 456;
+ final TimelineChunkAccumulator accum = new TimelineChunkAccumulator(hostId, sampleKindId, sampleCoder);
+ final List<DateTime> dateTimes = new ArrayList<DateTime>();
+ final DateTime startTime = new DateTime();
+ final DateTime endTime = startTime.plus(1000);
+
+ accum.addSample(new ScalarSample(SampleOpcode.INT, 25));
+ int timesCounter = 0;
+ dateTimes.add(startTime.plusSeconds(30 * timesCounter++));
+ for (int i = 0; i < 5; i++) {
+ accum.addSample(new ScalarSample(SampleOpcode.INT, 10));
+ dateTimes.add(startTime.plusSeconds(30 * timesCounter++));
+ }
+ accum.addSample(new ScalarSample(SampleOpcode.DOUBLE, 100.0));
+ dateTimes.add(startTime.plusSeconds(30 * timesCounter++));
+ accum.addSample(new ScalarSample(SampleOpcode.DOUBLE, 100.0));
+ dateTimes.add(startTime.plusSeconds(30 * timesCounter++));
+
+ accum.addSample(new ScalarSample(SampleOpcode.STRING, "Hiya!"));
+ dateTimes.add(startTime.plusSeconds(30 * timesCounter++));
+
+ final byte[] compressedTimes = timelineCoder.compressDateTimes(dateTimes);
+ final TimelineChunk chunk = accum.extractTimelineChunkAndReset(startTime, endTime, compressedTimes);
+ Assert.assertEquals(chunk.getSampleCount(), 9);
+ // Now play them back
+ sampleCoder.scan(chunk.getTimeBytesAndSampleBytes().getSampleBytes(), compressedTimes, dateTimes.size(), new SampleProcessor() {
+ private int sampleNumber = 0;
+
+ @Override
+ public void processSamples(final TimelineCursor timeCursor, final int sampleCount, final SampleOpcode opcode, final Object value) {
+ if (sampleNumber == 0) {
+ Assert.assertEquals(opcode, SampleOpcode.INT);
+ Assert.assertEquals(value, 25);
+ } else if (sampleNumber >= 1 && sampleNumber < 6) {
+ Assert.assertEquals(opcode, SampleOpcode.INT);
+ Assert.assertEquals(value, 10);
+ } else if (sampleNumber >= 6 && sampleNumber < 8) {
+ Assert.assertEquals(opcode, SampleOpcode.DOUBLE);
+ Assert.assertEquals(value, 100.0);
+ } else if (sampleNumber == 8) {
+ Assert.assertEquals(opcode, SampleOpcode.STRING);
+ Assert.assertEquals(value, "Hiya!");
+ } else {
+ Assert.assertTrue(false);
+ }
+ sampleNumber += sampleCount;
+ }
+ });
+ final TimelineChunkDecoded chunkDecoded = new TimelineChunkDecoded(chunk, sampleCoder);
+ //System.out.printf("%s\n", chunkDecoded.toString());
+ }
+
+
+ @Test(groups = "fast")
+ public void testByteRepeater() throws Exception {
+ final int hostId = 123;
+ final int sampleKindId = 456;
+ final DateTime startTime = new DateTime();
+ final List<DateTime> dateTimes = new ArrayList<DateTime>();
+ final int byteRepeaterCount = 255;
+ final TimelineChunkAccumulator accum = new TimelineChunkAccumulator(hostId, sampleKindId, sampleCoder);
+ for (int i = 0; i < byteRepeaterCount; i++) {
+ dateTimes.add(startTime.plusSeconds(i * 5));
+ accum.addSample(sampleCoder.compressSample(new ScalarSample<Double>(SampleOpcode.DOUBLE, 2.0)));
+ }
+ final DateTime endTime = startTime.plusSeconds(5 * byteRepeaterCount);
+ final byte[] compressedTimes = timelineCoder.compressDateTimes(dateTimes);
+ final TimelineChunk chunk = accum.extractTimelineChunkAndReset(startTime, endTime, compressedTimes);
+ final byte[] samples = chunk.getTimeBytesAndSampleBytes().getSampleBytes();
+ // Should be 0xFF 0xFF 0x12 0x02
+ Assert.assertEquals(samples.length, 4);
+ Assert.assertEquals(((int) samples[0]) & 0xff, SampleOpcode.REPEAT_BYTE.getOpcodeIndex());
+ Assert.assertEquals(((int) samples[1]) & 0xff, byteRepeaterCount);
+ Assert.assertEquals(((int) samples[2]) & 0xff, SampleOpcode.BYTE_FOR_DOUBLE.getOpcodeIndex());
+ Assert.assertEquals(((int) samples[3]) & 0xff, 0x02);
+ Assert.assertEquals(chunk.getSampleCount(), byteRepeaterCount);
+ sampleCoder.scan(chunk.getTimeBytesAndSampleBytes().getSampleBytes(), compressedTimes, dateTimes.size(), new SampleProcessor() {
+
+ @Override
+ public void processSamples(final TimelineCursor timeCursor, final int sampleCount, final SampleOpcode opcode, final Object value) {
+ Assert.assertEquals(sampleCount, byteRepeaterCount);
+ Assert.assertEquals(value, 2.0);
+ }
+ });
+ }
+
+ @Test(groups = "fast")
+ public void testShortRepeater() throws Exception {
+ final int hostId = 123;
+ final int sampleKindId = 456;
+ final DateTime startTime = new DateTime();
+ final List<DateTime> dateTimes = new ArrayList<DateTime>();
+ final int shortRepeaterCount = 256;
+ final TimelineChunkAccumulator accum = new TimelineChunkAccumulator(hostId, sampleKindId, sampleCoder);
+ for (int i = 0; i < shortRepeaterCount; i++) {
+ dateTimes.add(startTime.plusSeconds(i * 5));
+ accum.addSample(sampleCoder.compressSample(new ScalarSample<Double>(SampleOpcode.DOUBLE, 2.0)));
+ }
+ final DateTime endTime = startTime.plusSeconds(5 * shortRepeaterCount);
+ final byte[] compressedTimes = timelineCoder.compressDateTimes(dateTimes);
+ final TimelineChunk chunk = accum.extractTimelineChunkAndReset(startTime, endTime, compressedTimes);
+ final byte[] samples = chunk.getTimeBytesAndSampleBytes().getSampleBytes();
+ Assert.assertEquals(samples.length, 5);
+ Assert.assertEquals(((int) samples[0]) & 0xff, SampleOpcode.REPEAT_SHORT.getOpcodeIndex());
+ final int count = ((samples[1] & 0xff) << 8) | (samples[2] & 0xff);
+ Assert.assertEquals(count, shortRepeaterCount);
+ Assert.assertEquals(((int) samples[3]) & 0xff, SampleOpcode.BYTE_FOR_DOUBLE.getOpcodeIndex());
+ Assert.assertEquals(((int) samples[4]) & 0xff, 0x02);
+ Assert.assertEquals(chunk.getSampleCount(), shortRepeaterCount);
+
+ sampleCoder.scan(chunk.getTimeBytesAndSampleBytes().getSampleBytes(), compressedTimes, dateTimes.size(), new SampleProcessor() {
+
+ @Override
+ public void processSamples(TimelineCursor timeCursor, int sampleCount, SampleOpcode opcode, Object value) {
+ Assert.assertEquals(sampleCount, shortRepeaterCount);
+ Assert.assertEquals(value, 2.0);
+ }
+ });
+ }
+}
diff --git a/usage/src/test/java/com/ning/billing/usage/timeline/codec/TestTimelineChunkToJson.java b/usage/src/test/java/com/ning/billing/usage/timeline/codec/TestTimelineChunkToJson.java
new file mode 100644
index 0000000..9cc56d2
--- /dev/null
+++ b/usage/src/test/java/com/ning/billing/usage/timeline/codec/TestTimelineChunkToJson.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.codec;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.ning.billing.usage.UsageTestSuite;
+import com.ning.billing.usage.timeline.chunks.TimelineChunk;
+import com.ning.billing.usage.timeline.chunks.TimelineChunksViews.Compact;
+import com.ning.billing.usage.timeline.chunks.TimelineChunksViews.Loose;
+import com.ning.billing.usage.timeline.samples.SampleOpcode;
+import com.ning.billing.usage.timeline.samples.ScalarSample;
+import com.ning.billing.usage.timeline.times.DefaultTimelineCoder;
+import com.ning.billing.usage.timeline.times.TimelineCoder;
+
+import com.fasterxml.jackson.databind.MapperFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.TextNode;
+
+public class TestTimelineChunkToJson extends UsageTestSuite {
+
+ private static final ObjectMapper mapper = new ObjectMapper().configure(MapperFeature.DEFAULT_VIEW_INCLUSION, false);
+ private static final TimelineCoder timelineCoder = new DefaultTimelineCoder();
+ private static final SampleCoder sampleCoder = new DefaultSampleCoder();
+
+ private static final long CHUNK_ID = 1242L;
+ private static final int HOST_ID = 1422;
+ private static final int SAMPLE_KIND_ID = 1224;
+ private static final int SAMPLE_COUNT = 2142;
+ private static final DateTime END_TIME = new DateTime(DateTimeZone.UTC);
+ private static final DateTime START_TIME = END_TIME.minusMinutes(SAMPLE_COUNT);
+
+ private byte[] timeBytes;
+ private byte[] samples;
+ private TimelineChunk chunk;
+
+ @BeforeMethod(groups = "fast")
+ public void setUp() throws Exception {
+ final List<DateTime> dateTimes = new ArrayList<DateTime>();
+ final ByteArrayOutputStream out = new ByteArrayOutputStream();
+ final DataOutputStream output = new DataOutputStream(out);
+ for (int i = 0; i < SAMPLE_COUNT; i++) {
+ sampleCoder.encodeSample(output, new ScalarSample<Long>(SampleOpcode.LONG, 10L));
+ dateTimes.add(START_TIME.plusMinutes(i));
+ }
+ output.flush();
+ output.close();
+ samples = out.toByteArray();
+
+ final DateTime endTime = dateTimes.get(dateTimes.size() - 1);
+ timeBytes = timelineCoder.compressDateTimes(dateTimes);
+ chunk = new TimelineChunk(CHUNK_ID, HOST_ID, SAMPLE_KIND_ID, START_TIME, endTime, timeBytes, samples, SAMPLE_COUNT);
+ }
+
+ @Test(groups = "fast")
+ public void testTimelineChunkCompactMapping() throws Exception {
+ final String chunkToString = mapper.writerWithView(Compact.class).writeValueAsString(chunk);
+ final Map chunkFromString = mapper.readValue(chunkToString, Map.class);
+ Assert.assertEquals(chunkFromString.keySet().size(), 10);
+ Assert.assertEquals(chunkFromString.get("sourceId"), HOST_ID);
+ Assert.assertEquals(chunkFromString.get("metricId"), SAMPLE_KIND_ID);
+ final Map<String, String> timeBytesAndSampleBytes = (Map<String, String>) chunkFromString.get("timeBytesAndSampleBytes");
+ Assert.assertEquals(new TextNode(timeBytesAndSampleBytes.get("timeBytes")).binaryValue(), timeBytes);
+ Assert.assertEquals(new TextNode(timeBytesAndSampleBytes.get("sampleBytes")).binaryValue(), samples);
+ Assert.assertEquals(chunkFromString.get("sampleCount"), SAMPLE_COUNT);
+ Assert.assertEquals(chunkFromString.get("aggregationLevel"), 0);
+ Assert.assertEquals(chunkFromString.get("notValid"), false);
+ Assert.assertEquals(chunkFromString.get("dontAggregate"), false);
+ Assert.assertEquals(chunkFromString.get("chunkId"), (int) CHUNK_ID);
+ }
+
+ @Test(groups = "fast")
+ public void testTimelineChunkLooseMapping() throws Exception {
+ final String chunkToString = mapper.writerWithView(Loose.class).writeValueAsString(chunk);
+ final Map chunkFromString = mapper.readValue(chunkToString, Map.class);
+ Assert.assertEquals(chunkFromString.keySet().size(), 3);
+ Assert.assertEquals(chunkFromString.get("sourceId"), HOST_ID);
+ Assert.assertEquals(chunkFromString.get("metricId"), SAMPLE_KIND_ID);
+ Assert.assertEquals(chunkFromString.get("chunkId"), (int) CHUNK_ID);
+ }
+}
diff --git a/usage/src/test/java/com/ning/billing/usage/timeline/filter/TestDecimatingFilter.java b/usage/src/test/java/com/ning/billing/usage/timeline/filter/TestDecimatingFilter.java
new file mode 100644
index 0000000..98fb6eb
--- /dev/null
+++ b/usage/src/test/java/com/ning/billing/usage/timeline/filter/TestDecimatingFilter.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.filter;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.joda.time.DateTime;
+import org.skife.config.TimeSpan;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.ning.billing.usage.UsageTestSuite;
+import com.ning.billing.usage.timeline.consumer.SampleConsumer;
+import com.ning.billing.usage.timeline.samples.SampleOpcode;
+
+public class TestDecimatingFilter extends UsageTestSuite {
+
+ @Test(groups = "fast")
+ public void testBasicFilterOperations() throws Exception {
+ final List<Double> outputs = new ArrayList<Double>();
+ final long millisStart = System.currentTimeMillis() - 2000 * 100;
+
+ final DecimatingSampleFilter filter = new DecimatingSampleFilter(new DateTime(millisStart), new DateTime(millisStart + 2000 * 100), 25, 100, new TimeSpan("2s"), DecimationMode.PEAK_PICK,
+ new SampleConsumer() {
+
+ @Override
+ public void consumeSample(final int sampleNumber, final SampleOpcode opcode, final Object value, final DateTime time) {
+ outputs.add((double) ((Double) value));
+ }
+ });
+ for (int i = 0; i < 100; i++) {
+ // Make the value go up for 4 samples; then down for 4 samples, between 10.0 and 40.0
+ final int index = (i % 8) + 1;
+ double value = 0;
+ if (index <= 4) {
+ value = 10.0 * index;
+ } else {
+ value = (8 - (index - 1)) * 10;
+ }
+ //System.out.printf("For i %d, index %d, adding value %f\n", i, index, value);
+ filter.processOneSample(new DateTime(millisStart + 2000 * i), SampleOpcode.DOUBLE, value);
+ }
+ int index = 0;
+ for (final Double value : outputs) {
+ //System.out.printf("index %d, value %f\n", index++, (double)((Double)value));
+ if ((index & 1) == 0) {
+ Assert.assertEquals(value, 40.0);
+ } else {
+ Assert.assertEquals(value, 10.0);
+ }
+ index++;
+ }
+ }
+
+ /**
+ * This test has sample count of 21, and output count of 6, so there are 5.8 samples per output point
+ *
+ * @throws Exception
+ */
+ @Test(groups = "fast")
+ public void testFilterWithNonAlignedSampleCounts() throws Exception {
+ final List<Double> outputs = new ArrayList<Double>();
+ final long millisStart = System.currentTimeMillis() - 2000 * 21;
+
+ final DecimatingSampleFilter filter = new DecimatingSampleFilter(new DateTime(millisStart), new DateTime(millisStart + 2000 * 21), 6, 21, new TimeSpan("2s"), DecimationMode.PEAK_PICK,
+ new SampleConsumer() {
+
+ @Override
+ public void consumeSample(final int sampleNumber, final SampleOpcode opcode, final Object value, final DateTime time) {
+ outputs.add((double) ((Double) value));
+ }
+ });
+ for (int i = 0; i < 21; i++) {
+ // Make the value go up for 6 samples; then down for 6 samples, between 10.0 and 60.0
+ final int index = (i % 6) + 1;
+ double value = 0;
+ if (index <= 3) {
+ value = 10.0 * index;
+ } else {
+ value = (6 - (index - 1)) * 10;
+ }
+ //System.out.printf("For i %d, index %d, adding value %f\n", i, index, value);
+ filter.processOneSample(new DateTime(millisStart + 2000 * i), SampleOpcode.DOUBLE, value);
+ }
+ Assert.assertEquals(outputs.size(), 5);
+ final double[] expectedValues = new double[]{30.0, 20.0, 30.0, 30.0, 10.0};
+ for (int i = 0; i < 5; i++) {
+ final double value = outputs.get(i);
+ final double expectedValue = expectedValues[i];
+ //System.out.printf("index %d, value returned %f, value expected %f\n", i, value, expectedValue);
+ Assert.assertEquals(value, expectedValue);
+ }
+ }
+}
diff --git a/usage/src/test/java/com/ning/billing/usage/timeline/metrics/TestSamplesForMetricAndSource.java b/usage/src/test/java/com/ning/billing/usage/timeline/metrics/TestSamplesForMetricAndSource.java
new file mode 100644
index 0000000..9bd9d86
--- /dev/null
+++ b/usage/src/test/java/com/ning/billing/usage/timeline/metrics/TestSamplesForMetricAndSource.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.metrics;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.ning.billing.usage.UsageTestSuite;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+public class TestSamplesForMetricAndSource extends UsageTestSuite {
+
+ @Test(groups = "fast")
+ public void testMapping() throws Exception {
+ final SamplesForMetricAndSource samples = new SamplesForMetricAndSource("host.foo.com", "JVM", "GC", "1,2,2,0");
+
+ final ObjectMapper mapper = new ObjectMapper();
+ final String json = mapper.writeValueAsString(samples);
+ Assert.assertEquals("{\"sourceName\":\"host.foo.com\",\"eventCategory\":\"JVM\",\"metric\":\"GC\",\"samples\":\"1,2,2,0\"}", json);
+
+ final SamplesForMetricAndSource samplesFromJson = mapper.readValue(json, SamplesForMetricAndSource.class);
+ Assert.assertEquals(samplesFromJson, samples);
+ }
+}
diff --git a/usage/src/test/java/com/ning/billing/usage/timeline/persistent/TestReplayer.java b/usage/src/test/java/com/ning/billing/usage/timeline/persistent/TestReplayer.java
new file mode 100644
index 0000000..93f7925
--- /dev/null
+++ b/usage/src/test/java/com/ning/billing/usage/timeline/persistent/TestReplayer.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.persistent;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.ning.billing.usage.UsageTestSuite;
+import com.ning.billing.usage.timeline.sources.SourceSamplesForTimestamp;
+
+import com.google.common.base.Function;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+
+public class TestReplayer extends UsageTestSuite {
+
+ private static final File basePath = new File(System.getProperty("java.io.tmpdir"), "TestReplayer-" + System.currentTimeMillis());
+
+ private static final class MockReplayer extends Replayer {
+
+ private final List<File> expectedFiles;
+ private int seen = 0;
+
+ public MockReplayer(final String path, final List<File> expectedFiles) {
+ super(path);
+ this.expectedFiles = expectedFiles;
+ }
+
+ @Override
+ public void read(final File file, final Function<SourceSamplesForTimestamp, Void> fn) throws IOException {
+ Assert.assertEquals(file, expectedFiles.get(seen));
+ seen++;
+ }
+
+ public int getSeen() {
+ return seen;
+ }
+ }
+
+ private final StreamyBytesPersistentOutputStream outputStream = new StreamyBytesPersistentOutputStream(basePath.toString(), "pweet", null, true);
+
+ @Test(groups = "fast")
+ public void testStringOrdering() throws Exception {
+ final File file1 = new File("aaa.bbb.12345.bin");
+ final File file2 = new File("aaa.bbb.12346.bin");
+ final File file3 = new File("aaa.bbb.02345.bin");
+
+ final List<File> sortedCopy = Replayer.FILE_ORDERING.sortedCopy(ImmutableList.<File>of(file2, file1, file3));
+ Assert.assertEquals(sortedCopy.get(0), file3);
+ Assert.assertEquals(sortedCopy.get(1), file1);
+ Assert.assertEquals(sortedCopy.get(2), file2);
+ }
+
+ @Test(groups = "slow")
+ public void testOrdering() throws Exception {
+ Assert.assertTrue(basePath.mkdir());
+
+ final List<String> filePathsCreated = new ArrayList<String>();
+ final List<File> filesCreated = new ArrayList<File>();
+ final int expected = 50;
+
+ for (int i = 0; i < expected; i++) {
+ filePathsCreated.add(outputStream.getFileName());
+ Thread.sleep(17);
+ }
+
+ // Create the files in the opposite ordering to make sure we can re-read them in order
+ for (int i = expected - 1; i >= 0; i--) {
+ final File file = new File(filePathsCreated.get(i));
+ Assert.assertTrue(file.createNewFile());
+ filesCreated.add(file);
+ }
+
+ final MockReplayer replayer = new MockReplayer(basePath.toString(), Lists.reverse(filesCreated));
+ replayer.readAll();
+
+ Assert.assertEquals(replayer.getSeen(), expected);
+ }
+}
diff --git a/usage/src/test/java/com/ning/billing/usage/timeline/samples/TestNullSample.java b/usage/src/test/java/com/ning/billing/usage/timeline/samples/TestNullSample.java
new file mode 100644
index 0000000..dc8f5ef
--- /dev/null
+++ b/usage/src/test/java/com/ning/billing/usage/timeline/samples/TestNullSample.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.samples;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.ning.billing.usage.UsageTestSuite;
+
+public class TestNullSample extends UsageTestSuite {
+
+ @Test(groups = "fast")
+ public void testConstructor() throws Exception {
+ final NullSample sample = new NullSample();
+
+ Assert.assertEquals(sample.getOpcode(), SampleOpcode.NULL);
+ Assert.assertNull(sample.getSampleValue());
+ }
+}
diff --git a/usage/src/test/java/com/ning/billing/usage/timeline/samples/TestRepeatSample.java b/usage/src/test/java/com/ning/billing/usage/timeline/samples/TestRepeatSample.java
new file mode 100644
index 0000000..aac97ec
--- /dev/null
+++ b/usage/src/test/java/com/ning/billing/usage/timeline/samples/TestRepeatSample.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.samples;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.ning.billing.usage.UsageTestSuite;
+
+public class TestRepeatSample extends UsageTestSuite {
+
+ @Test(groups = "fast")
+ public void testGetters() throws Exception {
+ final int repeatCount = 5;
+ final ScalarSample<Short> scalarSample = new ScalarSample<Short>(SampleOpcode.SHORT, (short) 12);
+ final RepeatSample<Short> repeatSample = new RepeatSample<Short>(repeatCount, scalarSample);
+
+ Assert.assertEquals(repeatSample.getRepeatCount(), repeatCount);
+ Assert.assertEquals(repeatSample.getSampleRepeated(), scalarSample);
+ Assert.assertEquals(repeatSample.getOpcode().name(), SampleOpcode.REPEAT_BYTE.name());
+ }
+
+ @Test(groups = "fast")
+ public void testEquals() throws Exception {
+ final int repeatCount = 5;
+ final ScalarSample<Short> scalarSample = new ScalarSample<Short>(SampleOpcode.SHORT, (short) 12);
+
+ final RepeatSample<Short> repeatSample = new RepeatSample<Short>(repeatCount, scalarSample);
+ Assert.assertEquals(repeatSample, repeatSample);
+
+ final RepeatSample<Short> sameRepeatSample = new RepeatSample<Short>(repeatCount, scalarSample);
+ Assert.assertEquals(sameRepeatSample, repeatSample);
+
+ final RepeatSample<Short> otherRepeatSample = new RepeatSample<Short>(repeatCount + 1, scalarSample);
+ Assert.assertNotEquals(otherRepeatSample, repeatSample);
+ }
+}
diff --git a/usage/src/test/java/com/ning/billing/usage/timeline/samples/TestSampleOpcode.java b/usage/src/test/java/com/ning/billing/usage/timeline/samples/TestSampleOpcode.java
new file mode 100644
index 0000000..3243c54
--- /dev/null
+++ b/usage/src/test/java/com/ning/billing/usage/timeline/samples/TestSampleOpcode.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.samples;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.ning.billing.usage.UsageTestSuite;
+
+public class TestSampleOpcode extends UsageTestSuite {
+
+ @Test(groups = "fast")
+ public void testGetKnownOpcodeFromIndex() throws Exception {
+ for (final SampleOpcode opcode : SampleOpcode.values()) {
+ final SampleOpcode opcodeFromIndex = SampleOpcode.getOpcodeFromIndex(opcode.getOpcodeIndex());
+ Assert.assertEquals(opcodeFromIndex, opcode);
+
+ Assert.assertEquals(opcodeFromIndex.getOpcodeIndex(), opcode.getOpcodeIndex());
+ Assert.assertEquals(opcodeFromIndex.getByteSize(), opcode.getByteSize());
+ Assert.assertEquals(opcodeFromIndex.getNoArgs(), opcode.getNoArgs());
+ Assert.assertEquals(opcodeFromIndex.getRepeater(), opcode.getRepeater());
+ Assert.assertEquals(opcodeFromIndex.getReplacement(), opcode.getReplacement());
+ }
+ }
+
+ @Test(groups = "fast", expectedExceptions = IllegalArgumentException.class)
+ public void testgetUnknownOpcodeFromIndex() throws Exception {
+ SampleOpcode.getOpcodeFromIndex(Integer.MAX_VALUE);
+ }
+}
diff --git a/usage/src/test/java/com/ning/billing/usage/timeline/samples/TestScalarSample.java b/usage/src/test/java/com/ning/billing/usage/timeline/samples/TestScalarSample.java
new file mode 100644
index 0000000..0436ed4
--- /dev/null
+++ b/usage/src/test/java/com/ning/billing/usage/timeline/samples/TestScalarSample.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.samples;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.ning.billing.usage.UsageTestSuite;
+
+public class TestScalarSample extends UsageTestSuite {
+
+ @Test(groups = "fast")
+ public void testGetters() throws Exception {
+ final SampleOpcode opcode = SampleOpcode.SHORT;
+ final short value = (short) 5;
+ final ScalarSample<Short> scalarSample = new ScalarSample<Short>(opcode, value);
+
+ Assert.assertEquals(scalarSample.getOpcode(), opcode);
+ Assert.assertEquals((short) scalarSample.getSampleValue(), value);
+ }
+
+ @Test(groups = "fast")
+ public void testEquals() throws Exception {
+ final SampleOpcode opcode = SampleOpcode.SHORT;
+ final short value = (short) 5;
+
+ final ScalarSample<Short> scalarSample = new ScalarSample<Short>(opcode, value);
+ Assert.assertEquals(scalarSample, scalarSample);
+
+ final ScalarSample<Short> sameScalarSample = new ScalarSample<Short>(opcode, value);
+ Assert.assertEquals(sameScalarSample, scalarSample);
+
+ final ScalarSample<Short> otherScalarSample = new ScalarSample<Short>(opcode, (short) (value + 1));
+ Assert.assertNotEquals(otherScalarSample, scalarSample);
+ }
+
+ @Test(groups = "fast")
+ public void testFromObject() throws Exception {
+ verifyFromObject(null, 0.0, null, SampleOpcode.NULL);
+
+ verifyFromObject((byte) 1, (double) 1, (byte) 1, SampleOpcode.BYTE);
+
+ verifyFromObject((short) 128, (double) 128, (short) 128, SampleOpcode.SHORT);
+ verifyFromObject(32767, (double) 32767, (short) 32767, SampleOpcode.SHORT);
+
+ verifyFromObject(32768, (double) 32768, 32768, SampleOpcode.INT);
+ verifyFromObject((long) 32767, (double) 32767, (short) 32767, SampleOpcode.SHORT);
+ verifyFromObject((long) 32768, (double) 32768, 32768, SampleOpcode.INT);
+
+ verifyFromObject(2147483648L, (double) 2147483648L, 2147483648L, SampleOpcode.LONG);
+
+ verifyFromObject((float) 1, 1, (float) 1, SampleOpcode.FLOAT);
+
+ verifyFromObject(12.24, 12.24, 12.24, SampleOpcode.DOUBLE);
+ }
+
+ private void verifyFromObject(final Object value, final double expectedDoubleValue, final Object expectedSampleValue, final SampleOpcode expectedSampleOpcode) {
+ final ScalarSample scalarSample = ScalarSample.fromObject(value);
+ Assert.assertEquals(scalarSample.getOpcode(), expectedSampleOpcode);
+ Assert.assertEquals(scalarSample.getSampleValue(), expectedSampleValue);
+ Assert.assertEquals(scalarSample.getDoubleValue(), expectedDoubleValue);
+ }
+}
diff --git a/usage/src/test/java/com/ning/billing/usage/timeline/TestDateTimeUtils.java b/usage/src/test/java/com/ning/billing/usage/timeline/TestDateTimeUtils.java
new file mode 100644
index 0000000..d5c60c1
--- /dev/null
+++ b/usage/src/test/java/com/ning/billing/usage/timeline/TestDateTimeUtils.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline;
+
+import org.joda.time.DateTime;
+import org.joda.time.Seconds;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.ning.billing.usage.UsageTestSuite;
+import com.ning.billing.usage.timeline.util.DateTimeUtils;
+import com.ning.billing.util.clock.Clock;
+import com.ning.billing.util.clock.ClockMock;
+
+public class TestDateTimeUtils extends UsageTestSuite {
+
+ private final Clock clock = new ClockMock();
+
+ @Test(groups = "fast")
+ public void testRoundTrip() throws Exception {
+ final DateTime utcNow = clock.getUTCNow();
+ final int unixSeconds = DateTimeUtils.unixSeconds(utcNow);
+ final DateTime dateTimeFromUnixSeconds = DateTimeUtils.dateTimeFromUnixSeconds(unixSeconds);
+
+ Assert.assertEquals(Seconds.secondsBetween(dateTimeFromUnixSeconds, utcNow).getSeconds(), 0);
+ }
+}
diff --git a/usage/src/test/java/com/ning/billing/usage/timeline/times/TestDefaultTimelineCoder.java b/usage/src/test/java/com/ning/billing/usage/timeline/times/TestDefaultTimelineCoder.java
new file mode 100644
index 0000000..97931e8
--- /dev/null
+++ b/usage/src/test/java/com/ning/billing/usage/timeline/times/TestDefaultTimelineCoder.java
@@ -0,0 +1,393 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.times;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Random;
+
+import org.joda.time.DateTime;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.ning.billing.usage.UsageTestSuite;
+import com.ning.billing.usage.timeline.util.DateTimeUtils;
+import com.ning.billing.usage.timeline.util.Hex;
+
+public class TestDefaultTimelineCoder extends UsageTestSuite {
+
+ private static final TimelineCoder timelineCoder = new DefaultTimelineCoder();
+
+ @Test(groups = "fast")
+ public void testBasicEncodeDecode() throws Exception {
+ final DateTime firstTime = DateTimeUtils.dateTimeFromUnixSeconds(1000000);
+ final List<DateTime> unencodedTimes = makeSomeTimes(firstTime);
+
+ final byte[] compressedTimes = timelineCoder.compressDateTimes(unencodedTimes);
+ //System.out.printf("Compressed times: %s\n", new String(Hex.encodeHex(compressedTimes)));
+ final List<DateTime> decompressedTimes = timelineCoder.decompressDateTimes(compressedTimes);
+ Assert.assertEquals(decompressedTimes.size(), unencodedTimes.size());
+ for (int i = 0; i < unencodedTimes.size(); i++) {
+ Assert.assertEquals(decompressedTimes.get(i), unencodedTimes.get(i));
+ }
+ }
+
+ private List<DateTime> makeSomeTimes(final DateTime firstTime) {
+ final List<DateTime> times = new ArrayList<DateTime>();
+ Collections.addAll(times, firstTime, firstTime.plusSeconds(5), firstTime.plusSeconds(5), firstTime.plusSeconds(5),
+ firstTime.plusSeconds(1000), firstTime.plusSeconds(1000), firstTime.plusSeconds(2030), firstTime.plusSeconds(2060));
+ return times;
+ }
+
+ @Test(groups = "fast")
+ public void testRepeats() throws Exception {
+ final DateTime firstTime = DateTimeUtils.dateTimeFromUnixSeconds(1293846);
+ final List<DateTime> unencodedTimes = makeSomeTimes(firstTime);
+
+ final byte[] compressedTimes = timelineCoder.compressDateTimes(unencodedTimes);
+ final List<DateTime> decompressedTimes = timelineCoder.decompressDateTimes(compressedTimes);
+ Assert.assertEquals(decompressedTimes.size(), unencodedTimes.size());
+ for (int i = 0; i < unencodedTimes.size(); i++) {
+ Assert.assertEquals(decompressedTimes.get(i), unencodedTimes.get(i));
+ }
+ }
+
+ @Test(groups = "fast")
+ public void testCombiningTimelinesByteRepeats() throws Exception {
+ final int firstTime = 1293846;
+
+ final List<DateTime> unencodedTimes1 = new ArrayList<DateTime>();
+ final List<DateTime> unencodedTimes2 = new ArrayList<DateTime>();
+ final int sampleCount = 10;
+ for (int i = 0; i < sampleCount; i++) {
+ unencodedTimes1.add(DateTimeUtils.dateTimeFromUnixSeconds(firstTime + i * 100));
+ unencodedTimes2.add(DateTimeUtils.dateTimeFromUnixSeconds(firstTime + sampleCount * 100 + i * 100));
+ }
+ final byte[] compressedTimes1 = timelineCoder.compressDateTimes(unencodedTimes1);
+ final byte[] compressedTimes2 = timelineCoder.compressDateTimes(unencodedTimes2);
+ Assert.assertEquals(compressedTimes1.length, 8);
+ Assert.assertEquals(compressedTimes1[0] & 0xff, TimelineOpcode.FULL_TIME.getOpcodeIndex());
+ Assert.assertEquals(compressedTimes1[5] & 0xff, TimelineOpcode.REPEATED_DELTA_TIME_BYTE.getOpcodeIndex());
+ Assert.assertEquals(compressedTimes1[6] & 0xff, 9);
+ Assert.assertEquals(compressedTimes1[7] & 0xff, 100);
+ Assert.assertEquals(compressedTimes2.length, 8);
+ Assert.assertEquals(compressedTimes2[0] & 0xff, TimelineOpcode.FULL_TIME.getOpcodeIndex());
+ Assert.assertEquals(compressedTimes2[5] & 0xff, TimelineOpcode.REPEATED_DELTA_TIME_BYTE.getOpcodeIndex());
+ Assert.assertEquals(compressedTimes2[6] & 0xff, 9);
+ Assert.assertEquals(compressedTimes2[7] & 0xff, 100);
+ final List<byte[]> timesList = new ArrayList<byte[]>();
+ timesList.add(compressedTimes1);
+ timesList.add(compressedTimes2);
+ final byte[] combinedTimes = timelineCoder.combineTimelines(timesList, null);
+ Assert.assertEquals(combinedTimes.length, 8);
+ Assert.assertEquals(combinedTimes[0] & 0xff, TimelineOpcode.FULL_TIME.getOpcodeIndex());
+ Assert.assertEquals(combinedTimes[5] & 0xff, TimelineOpcode.REPEATED_DELTA_TIME_BYTE.getOpcodeIndex());
+ Assert.assertEquals(combinedTimes[6] & 0xff, 19);
+ Assert.assertEquals(combinedTimes[7] & 0xff, 100);
+ // Check for 19, not 20, since the first full time took one
+ Assert.assertEquals(combinedTimes[6], 19);
+ Assert.assertEquals(timelineCoder.countTimeBytesSamples(combinedTimes), 20);
+ }
+
+ @Test(groups = "fast")
+ public void testCombiningTimelinesShortRepeats() throws Exception {
+ final int sampleCount = 240;
+ final int firstTime = 1293846;
+ final List<DateTime> unencodedTimes1 = new ArrayList<DateTime>();
+ final List<DateTime> unencodedTimes2 = new ArrayList<DateTime>();
+ for (int i = 0; i < sampleCount; i++) {
+ unencodedTimes1.add(DateTimeUtils.dateTimeFromUnixSeconds(firstTime + i * 100));
+ unencodedTimes2.add(DateTimeUtils.dateTimeFromUnixSeconds(firstTime + sampleCount * 100 + i * 100));
+ }
+ final byte[] compressedTimes1 = timelineCoder.compressDateTimes(unencodedTimes1);
+ final byte[] compressedTimes2 = timelineCoder.compressDateTimes(unencodedTimes2);
+ Assert.assertEquals(compressedTimes1.length, 8);
+ Assert.assertEquals(compressedTimes1[0] & 0xff, TimelineOpcode.FULL_TIME.getOpcodeIndex());
+ Assert.assertEquals(compressedTimes1[5] & 0xff, TimelineOpcode.REPEATED_DELTA_TIME_BYTE.getOpcodeIndex());
+ Assert.assertEquals(compressedTimes1[6] & 0xff, sampleCount - 1);
+ Assert.assertEquals(compressedTimes1[7] & 0xff, 100);
+ Assert.assertEquals(compressedTimes2.length, 8);
+ Assert.assertEquals(compressedTimes2[0] & 0xff, TimelineOpcode.FULL_TIME.getOpcodeIndex());
+ Assert.assertEquals(compressedTimes2[5] & 0xff, TimelineOpcode.REPEATED_DELTA_TIME_BYTE.getOpcodeIndex());
+ Assert.assertEquals(compressedTimes2[6] & 0xff, sampleCount - 1);
+ Assert.assertEquals(compressedTimes2[7] & 0xff, 100);
+ final List<byte[]> timesList = new ArrayList<byte[]>();
+ timesList.add(compressedTimes1);
+ timesList.add(compressedTimes2);
+ final byte[] combinedTimes = timelineCoder.combineTimelines(timesList, null);
+ Assert.assertEquals(combinedTimes.length, 9);
+ Assert.assertEquals(combinedTimes[0] & 0xff, TimelineOpcode.FULL_TIME.getOpcodeIndex());
+ Assert.assertEquals(combinedTimes[5] & 0xff, TimelineOpcode.REPEATED_DELTA_TIME_SHORT.getOpcodeIndex());
+ Assert.assertEquals(combinedTimes[6] & 0xff, 1);
+ Assert.assertEquals(combinedTimes[7] & 0xff, sampleCount * 2 - 1 - 256);
+ Assert.assertEquals(combinedTimes[8], 100);
+ }
+
+ @Test(groups = "fast")
+ public void testCombiningShortFragments() throws Exception {
+ final byte[] fragment0 = new byte[]{(byte) -1, (byte) 0, (byte) 15, (byte) 66, (byte) 84, (byte) 20};
+ final byte[] fragment1 = new byte[]{(byte) -1, (byte) 0, (byte) 15, (byte) 66, (byte) -122, (byte) 30};
+ final byte[] fragment2 = new byte[]{(byte) -1, (byte) 0, (byte) 15, (byte) 66, (byte) -62, (byte) 30};
+ final byte[] fragment3 = new byte[]{(byte) -1, (byte) 0, (byte) 15, (byte) 66, (byte) -2, (byte) 30};
+ final byte[][] fragmentArray = new byte[][]{fragment0, fragment1, fragment2, fragment3};
+ final byte[] combined = timelineCoder.combineTimelines(Arrays.asList(fragmentArray), null);
+ final List<DateTime> restoredTimes = timelineCoder.decompressDateTimes(combined);
+ final List<List<DateTime>> fragmentIntTimes = new ArrayList<List<DateTime>>();
+ final List<DateTime> allFragmentTimes = new ArrayList<DateTime>();
+ int totalLength = 0;
+ for (final byte[] aFragmentArray : fragmentArray) {
+ final List<DateTime> fragmentTimes = timelineCoder.decompressDateTimes(aFragmentArray);
+ fragmentIntTimes.add(fragmentTimes);
+ totalLength += fragmentTimes.size();
+ for (final DateTime time : fragmentTimes) {
+ allFragmentTimes.add(time);
+ }
+ }
+ Assert.assertEquals(restoredTimes.size(), totalLength);
+ for (int i = 0; i < totalLength; i++) {
+ Assert.assertEquals(restoredTimes.get(i), allFragmentTimes.get(i));
+ }
+ }
+
+ @Test(groups = "fast")
+ public void testCombiningTimelinesRandomRepeats() throws Exception {
+ final int[] increments = new int[]{30, 45, 10, 30, 20};
+ final int[] repetitions = new int[]{1, 2, 3, 4, 5, 240, 250, 300};
+ final int firstTimeInt = 1000000;
+ final DateTime startTime = DateTimeUtils.dateTimeFromUnixSeconds(firstTimeInt);
+ final List<DateTime> dateTimes = new ArrayList<DateTime>();
+ final Random rand = new Random(0);
+ DateTime nextTime = startTime;
+ int count = 0;
+ for (int i = 0; i < 20; i++) {
+ final int increment = increments[rand.nextInt(increments.length)];
+ final int repetition = repetitions[rand.nextInt(repetitions.length)];
+ for (int r = 0; i < repetition; i++) {
+ nextTime = nextTime.plusSeconds(increment);
+ dateTimes.add(nextTime);
+ count++;
+ }
+ }
+ final byte[] allCompressedTime = timelineCoder.compressDateTimes(dateTimes);
+ final List<DateTime> restoredTimes = timelineCoder.decompressDateTimes(allCompressedTime);
+ Assert.assertEquals(restoredTimes.size(), dateTimes.size());
+ for (int i = 0; i < count; i++) {
+ Assert.assertEquals(restoredTimes.get(i), dateTimes.get(i));
+ }
+ for (int fragmentLength = 2; fragmentLength < count / 2; fragmentLength++) {
+ final List<byte[]> fragments = new ArrayList<byte[]>();
+ final int fragmentCount = (int) Math.ceil((double) count / (double) fragmentLength);
+ for (int fragCounter = 0; fragCounter < fragmentCount; fragCounter++) {
+ final int fragIndex = fragCounter * fragmentLength;
+ final List<DateTime> fragment = dateTimes.subList(fragIndex, Math.min(count, fragIndex + fragmentLength));
+ fragments.add(timelineCoder.compressDateTimes(fragment));
+ }
+ final byte[] combined = timelineCoder.combineTimelines(fragments, null);
+ final List<DateTime> restoredDateTimes = timelineCoder.decompressDateTimes(combined);
+ //Assert.assertEquals(intTimes.length, count);
+ for (int i = 0; i < count; i++) {
+ Assert.assertEquals(restoredDateTimes.get(i), dateTimes.get(i));
+ }
+ }
+ }
+
+ @Test(groups = "fast")
+ public void test65KRepeats() throws Exception {
+ final int count = 0;
+ final List<DateTime> dateTimes = new ArrayList<DateTime>();
+ DateTime time = DateTimeUtils.dateTimeFromUnixSeconds(1000000);
+ for (int i = 0; i < 20; i++) {
+ time = time.plusSeconds(200);
+ dateTimes.add(time);
+ }
+ for (int i = 0; i < 0xFFFF + 100; i++) {
+ time = time.plusSeconds(100);
+ dateTimes.add(time);
+ }
+ final byte[] timeBytes = timelineCoder.compressDateTimes(dateTimes);
+ final String hex = new String(Hex.encodeHex(timeBytes));
+ // Here are the compressed samples: ff000f4308fe13c8fdffff64fe6464
+ // Translation:
+ // [ff 00 0f 43 08] means absolution time 1000000
+ // [fe 13 c8] means repeat 19 times delta 200 seconds
+ // [fd ff ff 64] means repeat 65525 times delta 100 seconds
+ // [fe 64 64] means repeat 100 times delta 100 seconds
+ Assert.assertEquals(timeBytes, Hex.decodeHex("ff000f4308fe13c8fdffff64fe6464".toCharArray()));
+ final List<DateTime> restoredSamples = timelineCoder.decompressDateTimes(timeBytes);
+ Assert.assertEquals(restoredSamples.size(), dateTimes.size());
+ for (int i = 0; i < count; i++) {
+ Assert.assertEquals(restoredSamples.get(i), DateTimeUtils.unixSeconds(dateTimes.get(i)));
+ }
+ }
+
+ @Test(groups = "fast")
+ public void testCombiningTimesError() throws Exception {
+ final byte[] times1 = Hex.decodeHex("ff10000001fe0310ff1000011bfe0310".toCharArray());
+ final byte[] times2 = Hex.decodeHex("ff10000160".toCharArray());
+ final List<byte[]> timesList = new ArrayList<byte[]>();
+ timesList.add(times1);
+ timesList.add(times2);
+ final byte[] combinedTimes = timelineCoder.combineTimelines(timesList, null);
+ final String hexCombinedTimes = new String(Hex.encodeHex(combinedTimes));
+ //System.out.printf("Combined times: %s\n", hexCombinedTimes);
+ Assert.assertEquals(hexCombinedTimes, "ff10000001fe0310eafe031015");
+ }
+
+ @Test(groups = "fast")
+ public void testTimeCursorWithZeroDeltaWithNext() throws Exception {
+ // This caused a TimeCursor problem
+ // FF 4F 91 D5 BC FE 02 1E 00 FE 02 1E FF 79 0B 44 22
+ // FF 4F 91 D5 BC FE 02 1E 00 FE 02 1E
+ // FF 4F 91 D5 BC Absolute time
+ // FE 02 1E Repeated delta time: count 2, delta: 30
+ // 00 Delta 0. Why did this happen?
+ // FE 02 1E Repeated delta time: count 2, delta: 30
+ // FF 79 0B 44 22 Absolute time
+ // Total samples: 6
+ final int sampleCount = 7;
+ final byte[] times = Hex.decodeHex("FF4F91D5BCFE021E00FE021EFF790B4422".toCharArray());
+ final DefaultTimelineCursor cursor = new DefaultTimelineCursor(times, sampleCount);
+ for (int i = 0; i < sampleCount; i++) {
+ final DateTime nextTime = cursor.getNextTime();
+ Assert.assertNotNull(nextTime);
+ }
+ try {
+ final DateTime lastTime = cursor.getNextTime();
+ Assert.fail();
+ } catch (Exception e) {
+ Assert.assertTrue(true);
+ }
+ }
+
+ @Test(groups = "fast")
+ public void testTimeCursorWithZeroDeltaWithSampleSkip() throws Exception {
+ // This caused a TimeCursor problem
+ // FF 4F 91 D5 BC FE 02 1E 00 FE 02 1E FF 79 0B 44 22
+ // FF 4F 91 D5 BC FE 02 1E 00 FE 02 1E
+ // FF 4F 91 D5 BC Absolute time
+ // FE 02 1E Repeated delta time: count 2, delta: 30
+ // 00 Delta 0. Why did this happen?
+ // FE 02 1E Repeated delta time: count 2, delta: 30
+ // FF 79 0B 44 22 Absolute time
+ // Total samples: 6
+ final int sampleCount = 7;
+ final byte[] times = Hex.decodeHex("FF4F91D5BCFE021E00FE021EFF790B4422".toCharArray());
+ final DefaultTimelineCursor cursor = new DefaultTimelineCursor(times, sampleCount);
+ for (int i = 0; i < sampleCount; i++) {
+ final DateTime nextTime = cursor.getNextTime();
+ Assert.assertNotNull(nextTime);
+ cursor.skipToSampleNumber(i + 1);
+ }
+ try {
+ final DateTime lastTime = cursor.getNextTime();
+ Assert.fail();
+ } catch (Exception e) {
+ Assert.assertTrue(true);
+ }
+ }
+
+ @Test(groups = "fast")
+ public void testTimeCursorThatShowedError() throws Exception {
+ // 39 bytes are: ff4f90f67afd03ce1e1ffe1a1e1d01fe771e1d01fd01df1e1d1ffe761e1d01fe771e1d01fe571e
+ // 1944 samples; error at 1934
+ final int sampleCount = 1944;
+ //final byte[] times = Hex.decodeHex("ff4f90f67afd03ce1e1ffe1a1e1d01fe771e1d01fd01df1e1d1ffe761e1d01fe771e1d01fe571e".toCharArray());
+ final byte[] times = Hex.decodeHex("00000018FF4F8FE521FD023D1E1FFEF01E1D01FE771E1D01FD03E21EFE07980F".toCharArray());
+ Assert.assertEquals(times.length, 32);
+ final DefaultTimelineCursor cursor = new DefaultTimelineCursor(times, sampleCount);
+ for (int i = 0; i < sampleCount; i++) {
+ final DateTime nextTime = cursor.getNextTime();
+ Assert.assertNotNull(nextTime);
+ cursor.skipToSampleNumber(i + 1);
+ }
+
+ try {
+ final DateTime lastTime = cursor.getNextTime();
+ Assert.fail();
+ } catch (Exception e) {
+ Assert.assertTrue(true);
+ }
+ }
+
+ @Test(groups = "fast")
+ public void testTimeCombineTimesError1() throws Exception {
+ checkCombinedTimelines("ff4f91fb14fe631e00fe151e", "ff4f920942");
+ }
+
+ @Test(groups = "fast")
+ public void testTimeCombineTimesError2() throws Exception {
+ checkCombinedTimelines("ff4f922618fe111e78fe111efe02005a", "ff4f923428");
+ }
+
+ @Test(groups = "fast")
+ public void testTimeCombineTimesError3() throws Exception {
+ checkCombinedTimelines("ff4f9224ecfe091e", "ff4f922618fe071e78fe111e78fe111e78fe111e78fe111efe02005afe121e78fe031e",
+ "ff4f923428fe0d1e7dfe111e78fe111e78fe111e78fe0b1e00fe061e78fe111e", "ff4f92427cfe111e78fe111e78fe111e78fe111e82fe041e1d01fe0c1e78fe0e1e");
+ }
+
+ @Test(groups = "fast")
+ public void testTimeCombineTimesError4() throws Exception {
+ checkCombinedTimelines("ff4f95ba83fe021e", "ff4f95d595", "ff4f95e297fe021e");
+ }
+
+ @Test(groups = "fast")
+ public void testTimeCombineTimesError5() throws Exception {
+ checkCombinedTimelines("ff00000100", "ff00000200");
+ }
+
+ @Test(groups = "fast")
+ public void testTimeCombineTimesError6() throws Exception {
+ checkCombinedTimelines("ff4f95ac73fe471e00fe301e", "ff4f95ba83fe471e00fe311e", "ff4f95d595", "ff4f95e297fe091e");
+ checkCombinedTimelines("ff4f95ac7afe461e1d01fe301e", "ff4f95ba8afe471e00fe041e1ffe2b1e", "ff4f95d59d", "ff4f95e281fe0a1e");
+ checkCombinedTimelines("ff4f95aca4fe461e00fe311e", "ff4f95bab4fe461e00fe261e1f1dfe0a1e", "ff4f95d5a8", "ff4f95e28cfe091e");
+ checkCombinedTimelines("ff4f95ac88fe471e00fe311e", "ff4f95bab6fe461e00fe321e", "ff4f95d5aa", "ff4f95e28efe091eff4f95e4e6fe0a1e");
+ checkCombinedTimelines("ff4f95e394ff4f95e4fcfe0e1e5afe341e00fe221e", "ff4f95f12cfe551e00fe221e", "ff4f95ff3cfe551e00fe231e", "ff4f960d6afe541e00fe231e");
+ checkCombinedTimelines("ff4f95e396ff4f95e4fefe0e1e5afe341e00fe271e", "ff4f95f1c4fe501e00fe281e", "ff4f95fff2fe4f1e00fe281e", "ff4f960e02fe4f1e00fe291e");
+ }
+
+ private void checkCombinedTimelines(final String... timelines) throws Exception {
+ final List<byte[]> timeParts = new ArrayList<byte[]>();
+ for (final String timeline : timelines) {
+ timeParts.add(Hex.decodeHex(timeline.toCharArray()));
+ }
+ int sampleCount = 0;
+ int byteCount = 0;
+ for (final byte[] timePart : timeParts) {
+ byteCount += timePart.length;
+ sampleCount += timelineCoder.countTimeBytesSamples(timePart);
+ }
+ final byte[] concatedTimes = new byte[byteCount];
+ int offset = 0;
+ for (final byte[] timePart : timeParts) {
+ final int length = timePart.length;
+ System.arraycopy(timePart, 0, concatedTimes, offset, length);
+ offset += length;
+ }
+ final byte[] newCombined = timelineCoder.combineTimelines(timeParts, null);
+ final int newCombinedLength = timelineCoder.countTimeBytesSamples(newCombined);
+ final DefaultTimelineCursor concatedCursor = new DefaultTimelineCursor(concatedTimes, sampleCount);
+ final DefaultTimelineCursor combinedCursor = new DefaultTimelineCursor(newCombined, sampleCount);
+ for (int i = 0; i < sampleCount; i++) {
+ final DateTime concatedTime = concatedCursor.getNextTime();
+ final DateTime combinedTime = combinedCursor.getNextTime();
+ Assert.assertEquals(combinedTime, concatedTime);
+ }
+ Assert.assertEquals(newCombinedLength, sampleCount);
+ }
+}
diff --git a/usage/src/test/java/com/ning/billing/usage/timeline/times/TestTimelineOpcode.java b/usage/src/test/java/com/ning/billing/usage/timeline/times/TestTimelineOpcode.java
new file mode 100644
index 0000000..25c27bb
--- /dev/null
+++ b/usage/src/test/java/com/ning/billing/usage/timeline/times/TestTimelineOpcode.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2010-2012 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.usage.timeline.times;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.ning.billing.usage.UsageTestSuite;
+
+public class TestTimelineOpcode extends UsageTestSuite {
+
+ @Test(groups = "fast")
+ public void testMaxDeltaTime() throws Exception {
+ for (final TimelineOpcode opcode : TimelineOpcode.values()) {
+ Assert.assertTrue(opcode.getOpcodeIndex() >= TimelineOpcode.MAX_DELTA_TIME);
+ }
+ }
+}
diff --git a/usage/src/test/java/com/ning/billing/usage/UsageTestSuite.java b/usage/src/test/java/com/ning/billing/usage/UsageTestSuite.java
new file mode 100644
index 0000000..4c4873e
--- /dev/null
+++ b/usage/src/test/java/com/ning/billing/usage/UsageTestSuite.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2010-2012 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.usage;
+
+import com.ning.billing.KillbillTestSuite;
+
+public class UsageTestSuite extends KillbillTestSuite {
+
+}
diff --git a/usage/src/test/java/com/ning/billing/usage/UsageTestSuiteWithEmbeddedDB.java b/usage/src/test/java/com/ning/billing/usage/UsageTestSuiteWithEmbeddedDB.java
new file mode 100644
index 0000000..20e51c6
--- /dev/null
+++ b/usage/src/test/java/com/ning/billing/usage/UsageTestSuiteWithEmbeddedDB.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2010-2012 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.usage;
+
+import com.ning.billing.KillbillTestSuiteWithEmbeddedDB;
+
+public class UsageTestSuiteWithEmbeddedDB extends KillbillTestSuiteWithEmbeddedDB {
+
+}
diff --git a/util/src/main/java/com/ning/billing/util/audit/api/DefaultAuditUserApi.java b/util/src/main/java/com/ning/billing/util/audit/api/DefaultAuditUserApi.java
new file mode 100644
index 0000000..6af61ea
--- /dev/null
+++ b/util/src/main/java/com/ning/billing/util/audit/api/DefaultAuditUserApi.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2010-2012 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.util.audit.api;
+
+import java.util.List;
+import java.util.UUID;
+
+import javax.inject.Inject;
+
+import com.ning.billing.util.api.AuditUserApi;
+import com.ning.billing.util.audit.AuditLog;
+import com.ning.billing.util.audit.dao.AuditDao;
+import com.ning.billing.util.dao.ObjectType;
+import com.ning.billing.util.dao.TableName;
+
+import com.google.common.collect.ImmutableList;
+
+public class DefaultAuditUserApi implements AuditUserApi {
+
+ private final AuditDao auditDao;
+
+ @Inject
+ public DefaultAuditUserApi(final AuditDao auditDao) {
+ this.auditDao = auditDao;
+ }
+
+ @Override
+ public List<AuditLog> getAuditLogs(final UUID objectId, final ObjectType objectType) {
+ final TableName tableName = getTableNameFromObjectType(objectType);
+ if (tableName == null) {
+ return ImmutableList.<AuditLog>of();
+ }
+
+ return auditDao.getAuditLogsForRecordId(tableName, objectId);
+ }
+
+ private TableName getTableNameFromObjectType(final ObjectType objectType) {
+ for (final TableName tableName : TableName.values()) {
+ if (objectType.equals(tableName.getObjectType())) {
+ return tableName;
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/util/src/main/java/com/ning/billing/util/audit/dao/AuditDao.java b/util/src/main/java/com/ning/billing/util/audit/dao/AuditDao.java
new file mode 100644
index 0000000..224b78b
--- /dev/null
+++ b/util/src/main/java/com/ning/billing/util/audit/dao/AuditDao.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2010-2012 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.util.audit.dao;
+
+import java.util.List;
+import java.util.UUID;
+
+import com.ning.billing.util.audit.AuditLog;
+import com.ning.billing.util.dao.TableName;
+
+public interface AuditDao {
+
+ public List<AuditLog> getAuditLogsForRecordId(final TableName tableName, final UUID objectId);
+}
diff --git a/util/src/main/java/com/ning/billing/util/audit/dao/DefaultAuditDao.java b/util/src/main/java/com/ning/billing/util/audit/dao/DefaultAuditDao.java
new file mode 100644
index 0000000..6d4a827
--- /dev/null
+++ b/util/src/main/java/com/ning/billing/util/audit/dao/DefaultAuditDao.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2010-2012 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.util.audit.dao;
+
+import java.util.List;
+import java.util.UUID;
+
+import javax.inject.Inject;
+
+import org.skife.jdbi.v2.IDBI;
+
+import com.ning.billing.util.audit.AuditLog;
+import com.ning.billing.util.dao.AuditSqlDao;
+import com.ning.billing.util.dao.TableName;
+
+import com.google.common.collect.ImmutableList;
+
+public class DefaultAuditDao implements AuditDao {
+
+ private final AuditSqlDao auditSqlDao;
+
+ @Inject
+ public DefaultAuditDao(final IDBI dbi) {
+ this.auditSqlDao = dbi.onDemand(AuditSqlDao.class);
+ }
+
+ @Override
+ public List<AuditLog> getAuditLogsForRecordId(final TableName tableName, final UUID objectId) {
+ final Long recordId = auditSqlDao.getRecordIdForTable(tableName.getTableName().toLowerCase(), objectId.toString());
+ if (recordId == null) {
+ return ImmutableList.<AuditLog>of();
+ } else {
+ return auditSqlDao.getAuditLogsForRecordId(tableName, recordId);
+ }
+ }
+}
diff --git a/util/src/main/java/com/ning/billing/util/audit/DefaultAuditLog.java b/util/src/main/java/com/ning/billing/util/audit/DefaultAuditLog.java
new file mode 100644
index 0000000..513697e
--- /dev/null
+++ b/util/src/main/java/com/ning/billing/util/audit/DefaultAuditLog.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2010-2012 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.util.audit;
+
+import org.joda.time.DateTime;
+
+import com.ning.billing.util.ChangeType;
+import com.ning.billing.util.callcontext.CallContext;
+import com.ning.billing.util.dao.EntityAudit;
+
+public class DefaultAuditLog implements AuditLog {
+
+ private final EntityAudit entityAudit;
+ private final CallContext callContext;
+
+ public DefaultAuditLog(final EntityAudit entityAudit, final CallContext callContext) {
+ this.entityAudit = entityAudit;
+ this.callContext = callContext;
+ }
+
+ @Override
+ public ChangeType getChangeType() {
+ return entityAudit.getChangeType();
+ }
+
+ @Override
+ public String getUserName() {
+ return callContext.getUserName();
+ }
+
+ @Override
+ public DateTime getCreatedDate() {
+ return callContext.getCreatedDate();
+ }
+
+ @Override
+ public String getReasonCode() {
+ return callContext.getReasonCode();
+ }
+
+ @Override
+ public String getUserToken() {
+ if (callContext.getUserToken() == null) {
+ return null;
+ } else {
+ return callContext.getUserToken().toString();
+ }
+ }
+
+ @Override
+ public String getComment() {
+ return callContext.getComment();
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("DefaultAuditLog");
+ sb.append("{entityAudit=").append(entityAudit);
+ sb.append(", callContext=").append(callContext);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final DefaultAuditLog that = (DefaultAuditLog) o;
+
+ if (callContext != null ? !callContext.equals(that.callContext) : that.callContext != null) {
+ return false;
+ }
+ if (entityAudit != null ? !entityAudit.equals(that.entityAudit) : that.entityAudit != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = entityAudit != null ? entityAudit.hashCode() : 0;
+ result = 31 * result + (callContext != null ? callContext.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/util/src/main/java/com/ning/billing/util/callcontext/CallContextBase.java b/util/src/main/java/com/ning/billing/util/callcontext/CallContextBase.java
index 40d7ef5..2c21d05 100644
--- a/util/src/main/java/com/ning/billing/util/callcontext/CallContextBase.java
+++ b/util/src/main/java/com/ning/billing/util/callcontext/CallContextBase.java
@@ -19,12 +19,13 @@ package com.ning.billing.util.callcontext;
import java.util.UUID;
public abstract class CallContextBase implements CallContext {
- private final UUID userToken;
- private final String userName;
- private final CallOrigin callOrigin;
- private final UserType userType;
- private final String reasonCode;
- private final String comment;
+
+ protected final UUID userToken;
+ protected final String userName;
+ protected final CallOrigin callOrigin;
+ protected final UserType userType;
+ protected final String reasonCode;
+ protected final String comment;
public CallContextBase(final String userName, final CallOrigin callOrigin, final UserType userType) {
this(userName, callOrigin, userType, null);
diff --git a/util/src/main/java/com/ning/billing/util/callcontext/DefaultCallContext.java b/util/src/main/java/com/ning/billing/util/callcontext/DefaultCallContext.java
index 081cd7e..e294d30 100644
--- a/util/src/main/java/com/ning/billing/util/callcontext/DefaultCallContext.java
+++ b/util/src/main/java/com/ning/billing/util/callcontext/DefaultCallContext.java
@@ -23,31 +23,102 @@ import org.joda.time.DateTime;
import com.ning.billing.util.clock.Clock;
public class DefaultCallContext extends CallContextBase {
- private final Clock clock;
+
+ private final DateTime createdDate;
public DefaultCallContext(final String userName, final CallOrigin callOrigin, final UserType userType, final UUID userToken, final Clock clock) {
super(userName, callOrigin, userType, userToken);
- this.clock = clock;
+ this.createdDate = clock.getUTCNow();
}
public DefaultCallContext(final String userName, final CallOrigin callOrigin, final UserType userType,
final String reasonCode, final String comment,
final UUID userToken, final Clock clock) {
super(userName, callOrigin, userType, reasonCode, comment, userToken);
- this.clock = clock;
+ this.createdDate = clock.getUTCNow();
}
public DefaultCallContext(final String userName, final CallOrigin callOrigin, final UserType userType, final Clock clock) {
this(userName, callOrigin, userType, null, clock);
}
+ public DefaultCallContext(final String userName, final DateTime createdDate, final String reasonCode,
+ final String comment, final UUID userToken) {
+ super(userName, null, null, reasonCode, comment, userToken);
+ this.createdDate = createdDate;
+ }
+
@Override
public DateTime getCreatedDate() {
- return clock.getUTCNow();
+ return createdDate;
}
@Override
public DateTime getUpdatedDate() {
- return clock.getUTCNow();
+ return createdDate;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("CallContextBase");
+ sb.append("{userToken=").append(userToken);
+ sb.append(", userName='").append(userName).append('\'');
+ sb.append(", callOrigin=").append(callOrigin);
+ sb.append(", userType=").append(userType);
+ sb.append(", reasonCode='").append(reasonCode).append('\'');
+ sb.append(", comment='").append(comment).append('\'');
+ sb.append(", createdDate='").append(createdDate).append('\'');
+ sb.append(", updatedDate='").append(createdDate).append('\'');
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final DefaultCallContext that = (DefaultCallContext) o;
+
+ if (callOrigin != that.callOrigin) {
+ return false;
+ }
+ if (comment != null ? !comment.equals(that.comment) : that.comment != null) {
+ return false;
+ }
+ if (reasonCode != null ? !reasonCode.equals(that.reasonCode) : that.reasonCode != null) {
+ return false;
+ }
+ if (userName != null ? !userName.equals(that.userName) : that.userName != null) {
+ return false;
+ }
+ if (userToken != null ? !userToken.equals(that.userToken) : that.userToken != null) {
+ return false;
+ }
+ if (createdDate != null ? createdDate.compareTo(that.createdDate) != 0 : that.createdDate != null) {
+ return false;
+ }
+ if (userType != that.userType) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = userToken != null ? userToken.hashCode() : 0;
+ result = 31 * result + (userName != null ? userName.hashCode() : 0);
+ result = 31 * result + (callOrigin != null ? callOrigin.hashCode() : 0);
+ result = 31 * result + (userType != null ? userType.hashCode() : 0);
+ result = 31 * result + (reasonCode != null ? reasonCode.hashCode() : 0);
+ result = 31 * result + (comment != null ? comment.hashCode() : 0);
+ result = 31 * result + (createdDate != null ? createdDate.hashCode() : 0);
+ return result;
}
}
diff --git a/util/src/main/java/com/ning/billing/util/dao/AuditLogMapper.java b/util/src/main/java/com/ning/billing/util/dao/AuditLogMapper.java
new file mode 100644
index 0000000..188ea71
--- /dev/null
+++ b/util/src/main/java/com/ning/billing/util/dao/AuditLogMapper.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2010-2012 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.util.dao;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.skife.jdbi.v2.StatementContext;
+import org.skife.jdbi.v2.tweak.ResultSetMapper;
+
+import com.ning.billing.util.ChangeType;
+import com.ning.billing.util.audit.AuditLog;
+import com.ning.billing.util.audit.DefaultAuditLog;
+import com.ning.billing.util.callcontext.CallContext;
+import com.ning.billing.util.callcontext.DefaultCallContext;
+
+public class AuditLogMapper extends MapperBase implements ResultSetMapper<AuditLog> {
+
+ @Override
+ public AuditLog map(final int index, final ResultSet r, final StatementContext ctx) throws SQLException {
+ final String tableName = r.getString("table_name");
+ final long recordId = r.getLong("record_id");
+ final String changeType = r.getString("change_type");
+ final DateTime changeDate = getDateTime(r, "change_date");
+ final String changedBy = r.getString("changed_by");
+ final String reasonCode = r.getString("reason_code");
+ final String comments = r.getString("comments");
+ final UUID userToken = getUUID(r, "user_token");
+
+ final EntityAudit entityAudit = new EntityAudit(TableName.valueOf(tableName), recordId, ChangeType.valueOf(changeType));
+ final CallContext callContext = new DefaultCallContext(changedBy, changeDate, reasonCode, comments, userToken);
+ return new DefaultAuditLog(entityAudit, callContext);
+ }
+}
diff --git a/util/src/main/java/com/ning/billing/util/dao/AuditSqlDao.java b/util/src/main/java/com/ning/billing/util/dao/AuditSqlDao.java
index 4f9c256..58a08c6 100644
--- a/util/src/main/java/com/ning/billing/util/dao/AuditSqlDao.java
+++ b/util/src/main/java/com/ning/billing/util/dao/AuditSqlDao.java
@@ -22,13 +22,18 @@ import org.skife.jdbi.v2.sqlobject.Bind;
import org.skife.jdbi.v2.sqlobject.SqlBatch;
import org.skife.jdbi.v2.sqlobject.SqlQuery;
import org.skife.jdbi.v2.sqlobject.SqlUpdate;
+import org.skife.jdbi.v2.sqlobject.customizers.Define;
+import org.skife.jdbi.v2.sqlobject.customizers.RegisterMapper;
import org.skife.jdbi.v2.sqlobject.stringtemplate.ExternalizedSqlViaStringTemplate3;
+import com.ning.billing.util.audit.AuditLog;
import com.ning.billing.util.callcontext.CallContext;
import com.ning.billing.util.callcontext.CallContextBinder;
@ExternalizedSqlViaStringTemplate3
+@RegisterMapper(AuditLogMapper.class)
public interface AuditSqlDao {
+
@SqlUpdate
public void insertAuditFromTransaction(@AuditBinder final EntityAudit audit,
@CallContextBinder final CallContext context);
@@ -38,9 +43,17 @@ public interface AuditSqlDao {
@CallContextBinder final CallContext context);
@SqlQuery
+ public List<AuditLog> getAuditLogsForRecordId(@TableNameBinder final TableName tableName,
+ @Bind("recordId") final long recordId);
+
+ @SqlQuery
public Long getRecordId(@Bind("id") final String id);
@SqlQuery
+ public Long getRecordIdForTable(@Define("tableName") final String tableName,
+ @Bind("id") final String id);
+
+ @SqlQuery
public Long getHistoryRecordId(@Bind("recordId") final Long recordId);
}
diff --git a/util/src/main/java/com/ning/billing/util/dao/TableName.java b/util/src/main/java/com/ning/billing/util/dao/TableName.java
index a5038d5..4dedcd0 100644
--- a/util/src/main/java/com/ning/billing/util/dao/TableName.java
+++ b/util/src/main/java/com/ning/billing/util/dao/TableName.java
@@ -16,31 +16,40 @@
package com.ning.billing.util.dao;
+import javax.annotation.Nullable;
+
public enum TableName {
- ACCOUNT("accounts"),
- ACCOUNT_HISTORY("account_history"),
- ACCOUNT_EMAIL_HISTORY("account_email_history"),
- BUNDLES("bundles"),
- CUSTOM_FIELD_HISTORY("custom_field_history"),
- INVOICE_ITEMS("invoice_items"),
- INVOICE_PAYMENTS("invoice_payments"),
- INVOICES("invoices"),
- PAYMENT_ATTEMPTS("payment_attempts"),
- PAYMENT_HISTORY("payment_history"),
- PAYMENTS("payments"),
- PAYMENT_METHODS("payment_methods"),
- SUBSCRIPTIONS("subscriptions"),
- SUBSCRIPTION_EVENTS("subscription_events"),
- REFUNDS("refunds"),
- TAG_HISTORY("tag_history");
+ ACCOUNT("accounts", ObjectType.ACCOUNT),
+ ACCOUNT_HISTORY("account_history", null),
+ ACCOUNT_EMAIL_HISTORY("account_email_history", ObjectType.ACCOUNT_EMAIL),
+ BUNDLES("bundles", ObjectType.BUNDLE),
+ CUSTOM_FIELD_HISTORY("custom_field_history", null),
+ INVOICE_ITEMS("invoice_items", ObjectType.INVOICE_ITEM),
+ INVOICE_PAYMENTS("invoice_payments", ObjectType.INVOICE_PAYMENT),
+ INVOICES("invoices", ObjectType.INVOICE),
+ PAYMENT_ATTEMPTS("payment_attempts", null),
+ PAYMENT_HISTORY("payment_history", null),
+ PAYMENTS("payments", ObjectType.PAYMENT),
+ PAYMENT_METHODS("payment_methods", ObjectType.PAYMENT_METHOD),
+ SUBSCRIPTIONS("subscriptions", ObjectType.SUBSCRIPTION),
+ SUBSCRIPTION_EVENTS("subscription_events", null),
+ REFUNDS("refunds", ObjectType.REFUND),
+ TAG_DEFINITIONS("tag_definitions", ObjectType.TAG_DEFINITION),
+ TAG_HISTORY("tag_history", null);
private final String tableName;
+ private final ObjectType objectType;
- TableName(final String tableName) {
+ TableName(final String tableName, @Nullable final ObjectType objectType) {
this.tableName = tableName;
+ this.objectType = objectType;
}
public String getTableName() {
return tableName;
}
+
+ public ObjectType getObjectType() {
+ return objectType;
+ }
}
diff --git a/util/src/main/java/com/ning/billing/util/dao/TableNameBinder.java b/util/src/main/java/com/ning/billing/util/dao/TableNameBinder.java
index 545186c..9cacf92 100644
--- a/util/src/main/java/com/ning/billing/util/dao/TableNameBinder.java
+++ b/util/src/main/java/com/ning/billing/util/dao/TableNameBinder.java
@@ -31,7 +31,9 @@ import org.skife.jdbi.v2.sqlobject.BindingAnnotation;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER})
public @interface TableNameBinder {
+
public static class TableNameBinderFactory implements BinderFactory {
+
public Binder build(final Annotation annotation) {
return new Binder<TableNameBinder, TableName>() {
public void bind(final SQLStatement q, final TableNameBinder bind, final TableName tableName) {
diff --git a/util/src/main/java/com/ning/billing/util/glue/AuditModule.java b/util/src/main/java/com/ning/billing/util/glue/AuditModule.java
new file mode 100644
index 0000000..2fa9310
--- /dev/null
+++ b/util/src/main/java/com/ning/billing/util/glue/AuditModule.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2010-2012 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.util.glue;
+
+import com.ning.billing.util.api.AuditUserApi;
+import com.ning.billing.util.audit.api.DefaultAuditUserApi;
+import com.ning.billing.util.audit.dao.AuditDao;
+import com.ning.billing.util.audit.dao.DefaultAuditDao;
+
+import com.google.inject.AbstractModule;
+
+public class AuditModule extends AbstractModule {
+
+ protected void installDaos() {
+ bind(AuditDao.class).to(DefaultAuditDao.class).asEagerSingleton();
+ }
+
+ protected void installUserApi() {
+ bind(AuditUserApi.class).to(DefaultAuditUserApi.class).asEagerSingleton();
+ }
+
+ @Override
+ protected void configure() {
+ installDaos();
+ installUserApi();
+ }
+}
diff --git a/util/src/test/java/com/ning/billing/util/audit/dao/TestDefaultAuditDao.java b/util/src/test/java/com/ning/billing/util/audit/dao/TestDefaultAuditDao.java
new file mode 100644
index 0000000..8ce34d0
--- /dev/null
+++ b/util/src/test/java/com/ning/billing/util/audit/dao/TestDefaultAuditDao.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2010-2012 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.util.audit.dao;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.UUID;
+
+import org.skife.jdbi.v2.Handle;
+import org.skife.jdbi.v2.IDBI;
+import org.testng.Assert;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Guice;
+import org.testng.annotations.Test;
+
+import com.ning.billing.util.ChangeType;
+import com.ning.billing.util.UtilTestSuiteWithEmbeddedDB;
+import com.ning.billing.util.audit.AuditLog;
+import com.ning.billing.util.bus.Bus;
+import com.ning.billing.util.callcontext.CallContext;
+import com.ning.billing.util.callcontext.CallOrigin;
+import com.ning.billing.util.callcontext.DefaultCallContextFactory;
+import com.ning.billing.util.callcontext.UserType;
+import com.ning.billing.util.clock.Clock;
+import com.ning.billing.util.dao.ObjectType;
+import com.ning.billing.util.dao.TableName;
+import com.ning.billing.util.glue.AuditModule;
+import com.ning.billing.util.tag.MockTagStoreModuleSql;
+import com.ning.billing.util.tag.TagDefinition;
+import com.ning.billing.util.tag.dao.AuditedTagDao;
+import com.ning.billing.util.tag.dao.TagDefinitionDao;
+
+import com.google.inject.Inject;
+
+@Guice(modules = {MockTagStoreModuleSql.class, AuditModule.class})
+public class TestDefaultAuditDao extends UtilTestSuiteWithEmbeddedDB {
+
+ @Inject
+ private TagDefinitionDao tagDefinitionDao;
+
+ @Inject
+ private AuditedTagDao tagDao;
+
+ @Inject
+ private AuditDao auditDao;
+
+ @Inject
+ private Clock clock;
+
+ @Inject
+ private Bus bus;
+
+ @Inject
+ private IDBI dbi;
+
+ private CallContext context;
+
+ @BeforeClass(groups = "slow")
+ public void setup() throws IOException {
+ context = new DefaultCallContextFactory(clock).createCallContext("Audit DAO test", CallOrigin.TEST, UserType.TEST, UUID.randomUUID());
+ bus.start();
+ }
+
+ @AfterClass(groups = "slow")
+ public void tearDown() {
+ bus.stop();
+ }
+
+ @Test(groups = "slow")
+ public void testRetrieveAudits() throws Exception {
+ final TagDefinition defYo = tagDefinitionDao.create("yo", "defintion yo", context);
+
+ // Create a tag
+ tagDao.insertTag(UUID.randomUUID(), ObjectType.ACCOUNT, defYo.getId(), context);
+
+ // Verify we get an audit entry for the tag_history table
+ final Handle handle = dbi.open();
+ final String tagHistoryString = (String) handle.select("select id from tag_history limit 1").get(0).get("id");
+ handle.close();
+
+ final List<AuditLog> auditLogs = auditDao.getAuditLogsForRecordId(TableName.TAG_HISTORY, UUID.fromString(tagHistoryString));
+ Assert.assertEquals(auditLogs.size(), 1);
+ Assert.assertEquals(auditLogs.get(0).getUserToken(), context.getUserToken().toString());
+ Assert.assertEquals(auditLogs.get(0).getChangeType(), ChangeType.INSERT);
+ Assert.assertNull(auditLogs.get(0).getComment());
+ Assert.assertNull(auditLogs.get(0).getReasonCode());
+ Assert.assertEquals(auditLogs.get(0).getUserName(), context.getUserName());
+ Assert.assertNotNull(auditLogs.get(0).getCreatedDate());
+ }
+}
diff --git a/util/src/test/java/com/ning/billing/util/audit/TestDefaultAuditLog.java b/util/src/test/java/com/ning/billing/util/audit/TestDefaultAuditLog.java
new file mode 100644
index 0000000..709708f
--- /dev/null
+++ b/util/src/test/java/com/ning/billing/util/audit/TestDefaultAuditLog.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2010-2012 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.util.audit;
+
+import java.util.UUID;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.ning.billing.util.ChangeType;
+import com.ning.billing.util.UtilTestSuite;
+import com.ning.billing.util.callcontext.CallContext;
+import com.ning.billing.util.callcontext.CallOrigin;
+import com.ning.billing.util.callcontext.DefaultCallContext;
+import com.ning.billing.util.callcontext.UserType;
+import com.ning.billing.util.clock.ClockMock;
+import com.ning.billing.util.dao.EntityAudit;
+import com.ning.billing.util.dao.TableName;
+
+public class TestDefaultAuditLog extends UtilTestSuite {
+
+ @Test(groups = "fast")
+ public void testGetters() throws Exception {
+ final TableName tableName = TableName.ACCOUNT_EMAIL_HISTORY;
+ final long recordId = Long.MAX_VALUE;
+ final ChangeType changeType = ChangeType.DELETE;
+ final EntityAudit entityAudit = new EntityAudit(tableName, recordId, changeType);
+
+ final String userName = UUID.randomUUID().toString();
+ final CallOrigin callOrigin = CallOrigin.EXTERNAL;
+ final UserType userType = UserType.CUSTOMER;
+ final UUID userToken = UUID.randomUUID();
+ final ClockMock clock = new ClockMock();
+ final CallContext callContext = new DefaultCallContext(userName, callOrigin, userType, userToken, clock);
+
+ final AuditLog auditLog = new DefaultAuditLog(entityAudit, callContext);
+ Assert.assertEquals(auditLog.getChangeType(), changeType);
+ Assert.assertNull(auditLog.getComment());
+ Assert.assertNotNull(auditLog.getCreatedDate());
+ Assert.assertNull(auditLog.getReasonCode());
+ Assert.assertEquals(auditLog.getUserName(), userName);
+ Assert.assertEquals(auditLog.getUserToken(), userToken.toString());
+ }
+
+ @Test(groups = "fast")
+ public void testEquals() throws Exception {
+ final TableName tableName = TableName.ACCOUNT_EMAIL_HISTORY;
+ final long recordId = Long.MAX_VALUE;
+ final ChangeType changeType = ChangeType.DELETE;
+ final EntityAudit entityAudit = new EntityAudit(tableName, recordId, changeType);
+
+ final String userName = UUID.randomUUID().toString();
+ final CallOrigin callOrigin = CallOrigin.EXTERNAL;
+ final UserType userType = UserType.CUSTOMER;
+ final UUID userToken = UUID.randomUUID();
+ final ClockMock clock = new ClockMock();
+ final CallContext callContext = new DefaultCallContext(userName, callOrigin, userType, userToken, clock);
+
+ final AuditLog auditLog = new DefaultAuditLog(entityAudit, callContext);
+ Assert.assertEquals(auditLog, auditLog);
+
+ final AuditLog sameAuditLog = new DefaultAuditLog(entityAudit, callContext);
+ Assert.assertEquals(sameAuditLog, auditLog);
+
+ clock.addMonths(1);
+ final CallContext otherCallContext = new DefaultCallContext(userName, callOrigin, userType, userToken, clock);
+ final AuditLog otherAuditLog = new DefaultAuditLog(entityAudit, otherCallContext);
+ Assert.assertNotEquals(otherAuditLog, auditLog);
+ }
+}
diff --git a/util/src/test/java/com/ning/billing/util/callcontext/TestDefaultCallContext.java b/util/src/test/java/com/ning/billing/util/callcontext/TestDefaultCallContext.java
new file mode 100644
index 0000000..c56b5f3
--- /dev/null
+++ b/util/src/test/java/com/ning/billing/util/callcontext/TestDefaultCallContext.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2010-2012 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.util.callcontext;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.ning.billing.util.UtilTestSuite;
+import com.ning.billing.util.clock.Clock;
+import com.ning.billing.util.clock.ClockMock;
+
+public class TestDefaultCallContext extends UtilTestSuite {
+
+ private final Clock clock = new ClockMock();
+
+ @Test(groups = "fast")
+ public void testGetters() throws Exception {
+ final String userName = UUID.randomUUID().toString();
+ final DateTime createdDate = clock.getUTCNow();
+ final String reasonCode = UUID.randomUUID().toString();
+ final String comment = UUID.randomUUID().toString();
+ final UUID userToken = UUID.randomUUID();
+ final DefaultCallContext callContext = new DefaultCallContext(userName, createdDate, reasonCode, comment, userToken);
+
+ Assert.assertEquals(callContext.getCreatedDate(), createdDate);
+ Assert.assertNull(callContext.getCallOrigin());
+ Assert.assertEquals(callContext.getComment(), comment);
+ Assert.assertEquals(callContext.getReasonCode(), reasonCode);
+ Assert.assertEquals(callContext.getUserName(), userName);
+ Assert.assertEquals(callContext.getUpdatedDate(), createdDate);
+ Assert.assertEquals(callContext.getUserToken(), userToken);
+ Assert.assertNull(callContext.getUserType());
+ }
+
+ @Test(groups = "fast")
+ public void testEquals() throws Exception {
+ final String userName = UUID.randomUUID().toString();
+ final DateTime createdDate = clock.getUTCNow();
+ final String reasonCode = UUID.randomUUID().toString();
+ final String comment = UUID.randomUUID().toString();
+ final UUID userToken = UUID.randomUUID();
+
+ final DefaultCallContext callContext = new DefaultCallContext(userName, createdDate, reasonCode, comment, userToken);
+ Assert.assertEquals(callContext, callContext);
+
+ final DefaultCallContext sameCallContext = new DefaultCallContext(userName, createdDate, reasonCode, comment, userToken);
+ Assert.assertEquals(sameCallContext, callContext);
+
+ final DefaultCallContext otherCallContext = new DefaultCallContext(UUID.randomUUID().toString(), createdDate, reasonCode, comment, userToken);
+ Assert.assertNotEquals(otherCallContext, callContext);
+ }
+}