killbill-aplcache

util: avoid inserting duplicate Tags This patch makes the

6/28/2013 12:15:34 AM

Details

diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/TagJson.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/TagJson.java
index fa62e86..12581cb 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/TagJson.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/TagJson.java
@@ -13,26 +13,28 @@
  * 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.fasterxml.jackson.annotation.JsonCreator;
-import com.fasterxml.jackson.annotation.JsonProperty;
 import com.ning.billing.util.audit.AuditLog;
 import com.ning.billing.util.tag.TagDefinition;
 
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
 public class TagJson extends JsonBase {
 
     private final String tagDefinitionId;
     private final String tagDefinitionName;
 
     @JsonCreator
-    public TagJson(@JsonProperty("tagDefinitionId")  final String tagDefinitionId,
-            @JsonProperty("tagDefinitionName") final String tagDefinitionName,
-            @JsonProperty("auditLogs") @Nullable List<AuditLogJson> auditLogs) {
+    public TagJson(@JsonProperty("tagDefinitionId") final String tagDefinitionId,
+                   @JsonProperty("tagDefinitionName") final String tagDefinitionName,
+                   @JsonProperty("auditLogs") @Nullable final List<AuditLogJson> auditLogs) {
         super(auditLogs);
         this.tagDefinitionId = tagDefinitionId;
         this.tagDefinitionName = tagDefinitionName;
@@ -49,4 +51,41 @@ public class TagJson extends JsonBase {
     public String getTagDefinitionName() {
         return tagDefinitionName;
     }
+
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder("TagJson{");
+        sb.append("tagDefinitionId='").append(tagDefinitionId).append('\'');
+        sb.append(", tagDefinitionName='").append(tagDefinitionName).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 TagJson tagJson = (TagJson) o;
+
+        if (tagDefinitionId != null ? !tagDefinitionId.equals(tagJson.tagDefinitionId) : tagJson.tagDefinitionId != null) {
+            return false;
+        }
+        if (tagDefinitionName != null ? !tagDefinitionName.equals(tagJson.tagDefinitionName) : tagJson.tagDefinitionName != null) {
+            return false;
+        }
+
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = tagDefinitionId != null ? tagDefinitionId.hashCode() : 0;
+        result = 31 * result + (tagDefinitionName != null ? tagDefinitionName.hashCode() : 0);
+        return result;
+    }
 }
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 40092a2..7af0b3d 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
@@ -130,6 +130,7 @@ public abstract class JaxRsResourceBase implements JaxrsResource {
                                   final CallContext context) throws TagApiException {
         final Collection<UUID> input = getTagDefinitionUUIDs(tagList);
         tagUserApi.addTags(id, getObjectType(), input, context);
+        // TODO This will always return 201, even if some (or all) tags already existed (in which case we don't do anything)
         return uriBuilder.buildResponse(this.getClass(), "getTags", id, uriInfo.getBaseUri().toString());
     }
 
diff --git a/server/src/test/java/com/ning/billing/jaxrs/TestAccount.java b/server/src/test/java/com/ning/billing/jaxrs/TestAccount.java
index f469837..4c90eab 100644
--- a/server/src/test/java/com/ning/billing/jaxrs/TestAccount.java
+++ b/server/src/test/java/com/ning/billing/jaxrs/TestAccount.java
@@ -34,13 +34,11 @@ import com.ning.billing.jaxrs.json.CustomFieldJson;
 import com.ning.billing.jaxrs.json.PaymentJsonSimple;
 import com.ning.billing.jaxrs.json.PaymentMethodJson;
 import com.ning.billing.jaxrs.json.RefundJson;
-import com.ning.billing.jaxrs.json.TagDefinitionJson;
 import com.ning.billing.jaxrs.json.TagJson;
 import com.ning.billing.jaxrs.resources.JaxrsResource;
 import com.ning.http.client.Response;
 
 import com.fasterxml.jackson.core.type.TypeReference;
-import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 
 import static org.testng.Assert.assertEquals;
@@ -252,21 +250,32 @@ public class TestAccount extends TestJaxrsBase {
 
     @Test(groups = "slow")
     public void testTags() throws Exception {
-
+        final String accountId = UUID.randomUUID().toString();
+        final String uri = JaxrsResource.ACCOUNTS_PATH + "/" + accountId + "/" + JaxrsResource.TAGS;
+        final String accountTagsUrl = getUrlFromUri(uri);
         // Use tag definition for AUTO_PAY_OFF
-        final TagDefinitionJson input = new TagDefinitionJson(new UUID(0, 1).toString(), false, "AUTO_PAY_OFF",
-                                                              "nothing more to say", ImmutableList.<String>of());
+        final String autoPayOffId = new UUID(0, 1).toString();
 
-        final Map<String, String> queryParams = new HashMap<String, String>();
-        queryParams.put(JaxrsResource.QUERY_TAGS, input.getId());
-        final String uri = JaxrsResource.ACCOUNTS_PATH + "/" + UUID.randomUUID().toString() + "/" + JaxrsResource.TAGS;
-        Response response = doPost(uri, null, queryParams, DEFAULT_HTTP_TIMEOUT_SEC);
+        // Add a tag
+        Response response = doPost(uri, null, ImmutableMap.<String, String>of(JaxrsResource.QUERY_TAGS, autoPayOffId), DEFAULT_HTTP_TIMEOUT_SEC);
         assertEquals(response.getStatusCode(), Status.CREATED.getStatusCode());
 
-        // Retrieves by Id based on Location returned
-        final String url = getUrlFromUri(uri);
-        response = doGetWithUrl(url, DEFAULT_EMPTY_QUERY, DEFAULT_HTTP_TIMEOUT_SEC);
+        // Retrieves all tags
+        response = doGetWithUrl(accountTagsUrl, DEFAULT_EMPTY_QUERY, DEFAULT_HTTP_TIMEOUT_SEC);
+        Assert.assertEquals(response.getStatusCode(), Status.OK.getStatusCode());
+        final List<TagJson> tags1 = mapper.readValue(response.getResponseBody(), new TypeReference<List<TagJson>>() {});
+        Assert.assertEquals(tags1.size(), 1);
+        Assert.assertEquals(tags1.get(0).getTagDefinitionId(), autoPayOffId);
+
+        // Verify adding the same tag a second time doesn't do anything
+        response = doPost(uri, null, ImmutableMap.<String, String>of(JaxrsResource.QUERY_TAGS, autoPayOffId), DEFAULT_HTTP_TIMEOUT_SEC);
+        assertEquals(response.getStatusCode(), Status.CREATED.getStatusCode());
+
+        // Retrieves all tags again
+        response = doGetWithUrl(accountTagsUrl, DEFAULT_EMPTY_QUERY, DEFAULT_HTTP_TIMEOUT_SEC);
         Assert.assertEquals(response.getStatusCode(), Status.OK.getStatusCode());
+        final List<TagJson> tags2 = mapper.readValue(response.getResponseBody(), new TypeReference<List<TagJson>>() {});
+        Assert.assertEquals(tags2, tags1);
     }
 
     @Test(groups = "slow")
diff --git a/util/src/main/java/com/ning/billing/util/entity/dao/EntityDaoBase.java b/util/src/main/java/com/ning/billing/util/entity/dao/EntityDaoBase.java
index e4718c3..e69c388 100644
--- a/util/src/main/java/com/ning/billing/util/entity/dao/EntityDaoBase.java
+++ b/util/src/main/java/com/ning/billing/util/entity/dao/EntityDaoBase.java
@@ -38,12 +38,16 @@ public abstract class EntityDaoBase<M extends EntityModelDao<E>, E extends Entit
 
     @Override
     public void create(final M entity, final InternalCallContext context) throws U {
-        transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<Void>() {
+        transactionalSqlDao.execute(getCreateEntitySqlDaoTransactionWrapper(entity, context));
+    }
+
+    protected EntitySqlDaoTransactionWrapper<Void> getCreateEntitySqlDaoTransactionWrapper(final M entity, final InternalCallContext context) {
+        return new EntitySqlDaoTransactionWrapper<Void>() {
             @Override
             public Void inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
                 final EntitySqlDao<M, E> transactional = entitySqlDaoWrapperFactory.become(realSqlDao);
 
-                if (transactional.getById(entity.getId().toString(), context) != null) {
+                if (checkEntityAlreadyExists(transactional, entity, context)) {
                     throw generateAlreadyExistsException(entity, context);
                 }
                 transactional.create(entity, context);
@@ -53,7 +57,11 @@ public abstract class EntityDaoBase<M extends EntityModelDao<E>, E extends Entit
                 postBusEventFromTransaction(entity, refreshedEntity, ChangeType.INSERT, entitySqlDaoWrapperFactory, context);
                 return null;
             }
-        });
+        };
+    }
+
+    protected boolean checkEntityAlreadyExists(final EntitySqlDao<M, E> transactional, final M entity, final InternalCallContext context) {
+        return transactional.getById(entity.getId().toString(), context) != null;
     }
 
     protected void postBusEventFromTransaction(final M entity, final M savedEntity, final ChangeType changeType,
diff --git a/util/src/main/java/com/ning/billing/util/entity/dao/EntitySqlDaoTransactionalJdbiWrapper.java b/util/src/main/java/com/ning/billing/util/entity/dao/EntitySqlDaoTransactionalJdbiWrapper.java
index 9b6933b..0f31ecd 100644
--- a/util/src/main/java/com/ning/billing/util/entity/dao/EntitySqlDaoTransactionalJdbiWrapper.java
+++ b/util/src/main/java/com/ning/billing/util/entity/dao/EntitySqlDaoTransactionalJdbiWrapper.java
@@ -70,4 +70,24 @@ public class EntitySqlDaoTransactionalJdbiWrapper {
         final EntitySqlDao<EntityModelDao<Entity>, Entity> entitySqlDao = dbi.onDemand(InitialEntitySqlDao.class);
         return entitySqlDao.inTransaction(TransactionIsolationLevel.READ_COMMITTED, new JdbiTransaction<ReturnType, EntityModelDao<Entity>, Entity>(entitySqlDaoTransactionWrapper));
     }
+
+    /**
+     * @param entitySqlDaoTransactionWrapper transaction to execute
+     * @param <ReturnType>                   object type to return from the transaction
+     * @return result from the transaction fo type ReturnType
+     */
+    public <ReturnType, E extends Exception> ReturnType execute(final Class<E> exception, final EntitySqlDaoTransactionWrapper<ReturnType> entitySqlDaoTransactionWrapper) throws E {
+        final EntitySqlDao<EntityModelDao<Entity>, Entity> entitySqlDao = dbi.onDemand(InitialEntitySqlDao.class);
+        try {
+            return execute(entitySqlDaoTransactionWrapper);
+        } catch (RuntimeException e) {
+            if (e.getCause() != null && e.getCause().getClass().isAssignableFrom(exception)) {
+                throw (E) e.getCause();
+            } else if (e.getCause() != null && e.getCause() instanceof RuntimeException) {
+                throw (RuntimeException) e.getCause();
+            } else {
+                throw e;
+            }
+        }
+    }
 }
diff --git a/util/src/main/java/com/ning/billing/util/tag/api/DefaultTagUserApi.java b/util/src/main/java/com/ning/billing/util/tag/api/DefaultTagUserApi.java
index de54c35..6c5f150 100644
--- a/util/src/main/java/com/ning/billing/util/tag/api/DefaultTagUserApi.java
+++ b/util/src/main/java/com/ning/billing/util/tag/api/DefaultTagUserApi.java
@@ -20,6 +20,7 @@ import java.util.Collection;
 import java.util.List;
 import java.util.UUID;
 
+import com.ning.billing.ErrorCode;
 import com.ning.billing.ObjectType;
 import com.ning.billing.util.api.TagApiException;
 import com.ning.billing.util.api.TagDefinitionApiException;
@@ -110,7 +111,14 @@ public class DefaultTagUserApi implements TagUserApi {
     public void addTag(final UUID objectId, final ObjectType objectType, final UUID tagDefinitionId, final CallContext context) throws TagApiException {
         final InternalCallContext internalContext = internalCallContextFactory.createInternalCallContext(objectId, objectType, context);
         final TagModelDao tag = new TagModelDao(context.getCreatedDate(), tagDefinitionId, objectId, objectType);
-        tagDao.create(tag, internalContext);
+        try {
+            tagDao.create(tag, internalContext);
+        } catch (TagApiException e) {
+            // Be lenient here and make the addTag method idempotent
+            if (ErrorCode.TAG_ALREADY_EXISTS.getCode() != e.getCode()) {
+                throw e;
+            }
+        }
     }
 
     @Override
diff --git a/util/src/main/java/com/ning/billing/util/tag/dao/DefaultTagDao.java b/util/src/main/java/com/ning/billing/util/tag/dao/DefaultTagDao.java
index 6ae85a9..206fe2b 100644
--- a/util/src/main/java/com/ning/billing/util/tag/dao/DefaultTagDao.java
+++ b/util/src/main/java/com/ning/billing/util/tag/dao/DefaultTagDao.java
@@ -30,7 +30,6 @@ import com.ning.billing.ErrorCode;
 import com.ning.billing.ObjectType;
 import com.ning.billing.util.api.TagApiException;
 import com.ning.billing.util.audit.ChangeType;
-import com.ning.billing.util.cache.Cachable.CacheType;
 import com.ning.billing.util.cache.CacheControllerDispatcher;
 import com.ning.billing.util.callcontext.InternalCallContext;
 import com.ning.billing.util.callcontext.InternalTenantContext;
@@ -50,6 +49,7 @@ import com.ning.billing.util.tag.api.user.TagEventBuilder;
 import com.google.common.base.Predicate;
 import com.google.common.collect.Collections2;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
 import com.google.inject.Inject;
 
 public class DefaultTagDao extends EntityDaoBase<TagModelDao, Tag, TagApiException> implements TagDao {
@@ -132,8 +132,22 @@ public class DefaultTagDao extends EntityDaoBase<TagModelDao, Tag, TagApiExcepti
     }
 
     @Override
+    protected boolean checkEntityAlreadyExists(final EntitySqlDao<TagModelDao, Tag> transactional, final TagModelDao entity, final InternalCallContext context) {
+        return Iterables.find(transactional.getByAccountRecordId(context),
+                              new Predicate<TagModelDao>() {
+                                  @Override
+                                  public boolean apply(final TagModelDao existingTag) {
+                                      return entity.equals(existingTag) || entity.isSame(existingTag);
+                                  }
+                              },
+                              null) != null;
+    }
+
+    @Override
     protected TagApiException generateAlreadyExistsException(final TagModelDao entity, final InternalCallContext context) {
-        return new TagApiException(ErrorCode.TAG_ALREADY_EXISTS, entity.getId());
+        // Print the tag details, not the id here, as we throw this exception when checking if a tag already exists
+        // by using the isSame(TagModelDao) method (see above)
+        return new TagApiException(ErrorCode.TAG_ALREADY_EXISTS, entity.toString());
     }
 
     private TagDefinitionModelDao getTagDefinitionFromTransaction(final UUID tagDefinitionId, final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory, final InternalTenantContext context) throws TagApiException {
@@ -156,6 +170,11 @@ public class DefaultTagDao extends EntityDaoBase<TagModelDao, Tag, TagApiExcepti
     }
 
     @Override
+    public void create(final TagModelDao entity, final InternalCallContext context) throws TagApiException {
+        transactionalSqlDao.execute(TagApiException.class, getCreateEntitySqlDaoTransactionWrapper(entity, context));
+    }
+
+    @Override
     public void deleteTag(final UUID objectId, final ObjectType objectType, final UUID tagDefinitionId, final InternalCallContext context) throws TagApiException {
 
         transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<Void>() {
diff --git a/util/src/main/java/com/ning/billing/util/tag/dao/TagModelDao.java b/util/src/main/java/com/ning/billing/util/tag/dao/TagModelDao.java
index ea6ce3f..d88163f 100644
--- a/util/src/main/java/com/ning/billing/util/tag/dao/TagModelDao.java
+++ b/util/src/main/java/com/ning/billing/util/tag/dao/TagModelDao.java
@@ -95,6 +95,10 @@ public class TagModelDao extends EntityBase implements EntityModelDao<Tag> {
 
         final TagModelDao that = (TagModelDao) o;
 
+        return isSame(that);
+    }
+
+    public boolean isSame(final TagModelDao that) {
         if (isActive != null ? !isActive.equals(that.isActive) : that.isActive != null) {
             return false;
         }
diff --git a/util/src/main/resources/com/ning/billing/util/entity/dao/EntitySqlDao.sql.stg b/util/src/main/resources/com/ning/billing/util/entity/dao/EntitySqlDao.sql.stg
index af975cd..5ee87d6 100644
--- a/util/src/main/resources/com/ning/billing/util/entity/dao/EntitySqlDao.sql.stg
+++ b/util/src/main/resources/com/ning/billing/util/entity/dao/EntitySqlDao.sql.stg
@@ -162,11 +162,12 @@ where <recordIdField("t.")> = :recordId
 ;
 >>
 
+/** Use NULL-safe equal to operator in case account_record_id is NULL **/
 getByAccountRecordId(accountRecordId) ::= <<
 select
 <allTableFields("t.")>
 from <tableName()> t
-where <accountRecordIdField("t.")> = :accountRecordId
+where <accountRecordIdField("t.")> \<=\> :accountRecordId
 <AND_CHECK_TENANT("t.")>
 ;
 >>