diff --git a/zanata-war/src/main/java/org/zanata/dao/DatabaseSpecific.java b/zanata-war/src/main/java/org/zanata/dao/DatabaseSpecific.java deleted file mode 100644 index 8d0bb249da..0000000000 --- a/zanata-war/src/main/java/org/zanata/dao/DatabaseSpecific.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.zanata.dao; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Marker annotation to mark methods that will only work in certain database. - */ -@Target({ElementType.METHOD, ElementType.LOCAL_VARIABLE, ElementType.TYPE, ElementType.PARAMETER}) -@Retention(RetentionPolicy.SOURCE) -public @interface DatabaseSpecific { - /** - * Reason why this is database specific - */ - String value() default ""; -} diff --git a/zanata-war/src/main/java/org/zanata/dao/NativeQuery.java b/zanata-war/src/main/java/org/zanata/dao/NativeQuery.java index d4041fa495..b733617d80 100644 --- a/zanata-war/src/main/java/org/zanata/dao/NativeQuery.java +++ b/zanata-war/src/main/java/org/zanata/dao/NativeQuery.java @@ -15,4 +15,9 @@ * Reason why it has to be native query. */ String value() default ""; + + /** + * If the query is specific to certain database due to built-in function etc. + */ + String specificTo() default ""; } diff --git a/zanata-war/src/main/java/org/zanata/dao/TextFlowTargetHistoryDAO.java b/zanata-war/src/main/java/org/zanata/dao/TextFlowTargetHistoryDAO.java index bc5349d11f..d8f1b7ba89 100644 --- a/zanata-war/src/main/java/org/zanata/dao/TextFlowTargetHistoryDAO.java +++ b/zanata-war/src/main/java/org/zanata/dao/TextFlowTargetHistoryDAO.java @@ -20,7 +20,6 @@ */ package org.zanata.dao; -import java.math.BigDecimal; import java.math.BigInteger; import java.util.Date; import java.util.List; @@ -28,24 +27,19 @@ import org.hibernate.Query; import org.hibernate.Session; +import org.hibernate.transform.ResultTransformer; import org.jboss.seam.ScopeType; import org.jboss.seam.annotations.AutoCreate; import org.jboss.seam.annotations.Name; import org.jboss.seam.annotations.Scope; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; -import org.zanata.common.ContentState; -import org.zanata.model.HLocale; import org.zanata.model.HPerson; -import org.zanata.model.HProjectIteration; import org.zanata.model.HTextFlowTarget; import org.zanata.model.HTextFlowTargetHistory; + import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Optional; -import com.google.common.collect.ImmutableList; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; @Name("textFlowTargetHistoryDAO") @AutoCreate @@ -198,13 +192,15 @@ public boolean findConflictInHistory(HTextFlowTarget target, * different from system time zone * @param systemZone * current system time zone - * @return a list of UserTranslationMatrix object + * @param resultTransformer + * result transformer to transform query results + * @return a list of transformed object */ - @NativeQuery("need to use union") - @DatabaseSpecific("uses mysql date() function. In test we can override stripTimeFromDateTimeFunction(String) below to workaround it.") - public List getUserTranslationMatrix( + @NativeQuery(value = "need to use union", specificTo = "mysql due to usage of date() and convert_tz() functions.") + public List getUserTranslationMatrix( HPerson user, DateTime fromDate, DateTime toDate, - Optional userZoneOpt, DateTimeZone systemZone) { + Optional userZoneOpt, DateTimeZone systemZone, + ResultTransformer resultTransformer) { // @formatter:off String queryHistory = "select history.id, iter.id as iteration, tft.locale as locale, tf.wordCount as wordCount, history.state as state, history.lastChanged as lastChanged " + " from HTextFlowTargetHistory history " + @@ -239,29 +235,12 @@ public List getUserTranslationMatrix( Query query = getSession().createSQLQuery(queryString) .setParameter("user", user.getId()) .setTimestamp("fromDate", fromDate.toDate()) - .setTimestamp("toDate", toDate.toDate()); - @SuppressWarnings("unchecked") - List result = query.list(); - ImmutableList.Builder builder = - ImmutableList.builder(); - for (Object[] objects : result) { - Date savedDate = new DateTime(objects[0]).toDate(); - HProjectIteration iteration = - loadById(objects[1], HProjectIteration.class); - HLocale locale = loadById(objects[2], HLocale.class); - ContentState savedState = ContentState.values()[(int) objects[3]]; - long wordCount = - ((BigDecimal) objects[4]).toBigInteger().longValue(); - UserTranslationMatrix matrix = - new UserTranslationMatrix(savedDate, iteration, locale, - savedState, wordCount); - builder.add(matrix); - } - return builder.build(); + .setTimestamp("toDate", toDate.toDate()) + .setResultTransformer(resultTransformer); + return query.list(); } @VisibleForTesting - @DatabaseSpecific("uses mysql function") protected String convertTimeZoneFunction(String columnName, Optional userZoneOpt, DateTimeZone systemZone) { if (userZoneOpt.isPresent()) { @@ -275,7 +254,6 @@ protected String convertTimeZoneFunction(String columnName, // This is so we can override it in test and be able to test it against h2 @VisibleForTesting - @DatabaseSpecific("uses mysql function") protected String stripTimeFromDateTimeFunction(String columnName) { return "date(" + columnName + ")"; } @@ -297,13 +275,4 @@ private static String getOffsetAsString(DateTimeZone zone) { TimeUnit.MILLISECONDS.toHours(standardOffset)); } - @Getter - @RequiredArgsConstructor - public static class UserTranslationMatrix { - private final Date savedDate; - private final HProjectIteration projectIteration; - private final HLocale locale; - private final ContentState savedState; - private final long wordCount; - } } diff --git a/zanata-war/src/main/java/org/zanata/rest/dto/matrix/DetailMatrix.java b/zanata-war/src/main/java/org/zanata/rest/dto/TranslationMatrix.java similarity index 72% rename from zanata-war/src/main/java/org/zanata/rest/dto/matrix/DetailMatrix.java rename to zanata-war/src/main/java/org/zanata/rest/dto/TranslationMatrix.java index f8ab08e522..d9d487a46e 100644 --- a/zanata-war/src/main/java/org/zanata/rest/dto/matrix/DetailMatrix.java +++ b/zanata-war/src/main/java/org/zanata/rest/dto/TranslationMatrix.java @@ -1,20 +1,24 @@ -package org.zanata.rest.dto.matrix; +package org.zanata.rest.dto; -import org.zanata.common.ContentState; -import org.zanata.common.LocaleId; import lombok.AllArgsConstructor; import lombok.Data; +import org.zanata.common.ContentState; +import org.zanata.common.LocaleId; + /** * @author Patrick Huang * pahuang@redhat.com */ @Data @AllArgsConstructor -public class DetailMatrix { +public class TranslationMatrix { + private String savedDate; private String projectSlug; + private String projectName; private String versionSlug; private LocaleId localeId; + private String localeDisplayName; private ContentState savedState; private long wordCount; } diff --git a/zanata-war/src/main/java/org/zanata/rest/dto/matrix/UserWorkMatrix.java b/zanata-war/src/main/java/org/zanata/rest/dto/matrix/UserWorkMatrix.java deleted file mode 100644 index 30f828b3b8..0000000000 --- a/zanata-war/src/main/java/org/zanata/rest/dto/matrix/UserWorkMatrix.java +++ /dev/null @@ -1,48 +0,0 @@ -package org.zanata.rest.dto.matrix; - -import java.util.List; -import java.util.Map; - -import javax.xml.bind.annotation.XmlRootElement; - -import lombok.Delegate; - -import org.codehaus.jackson.annotate.JsonIgnore; -import org.codehaus.jackson.annotate.JsonIgnoreProperties; -import org.codehaus.jackson.map.annotate.JsonSerialize; -import org.zanata.dao.TextFlowTargetHistoryDAO; - -import com.google.common.collect.Lists; -import com.google.common.collect.Maps; - -/** - * This will help us to build a nested map structure. - * - * @see org.zanata.rest.dto.matrix.DetailMatrix - * - * @author Patrick Huang pahuang@redhat.com - */ -@JsonIgnoreProperties(ignoreUnknown = true) -@JsonSerialize(include = JsonSerialize.Inclusion.NON_NULL) -@XmlRootElement -public class UserWorkMatrix implements Map> { - @Delegate - private Map> dateToProjects = Maps.newHashMap(); - - @JsonIgnore - public void putOrCreateIfAbsent(String date, - TextFlowTargetHistoryDAO.UserTranslationMatrix matrixRecord) { - DetailMatrix detailMatrix = new DetailMatrix( - matrixRecord.getProjectIteration().getProject().getSlug(), - matrixRecord.getProjectIteration().getSlug(), - matrixRecord.getLocale().getLocaleId(), - matrixRecord.getSavedState(), matrixRecord.getWordCount()); - if (containsKey(date)) { - List matrixList = get(date); - matrixList.add(detailMatrix); - } else { - put(date, Lists.newArrayList(detailMatrix)); - } - } -} diff --git a/zanata-war/src/main/java/org/zanata/rest/service/StatisticsServiceImpl.java b/zanata-war/src/main/java/org/zanata/rest/service/StatisticsServiceImpl.java index b423b7700e..182a08e70e 100644 --- a/zanata-war/src/main/java/org/zanata/rest/service/StatisticsServiceImpl.java +++ b/zanata-war/src/main/java/org/zanata/rest/service/StatisticsServiceImpl.java @@ -21,21 +21,25 @@ package org.zanata.rest.service; import java.math.BigDecimal; +import java.math.BigInteger; import java.net.URI; import java.util.Date; import java.util.List; import java.util.Map; -import java.util.TimeZone; +import javax.persistence.EntityManager; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; +import lombok.Getter; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang.StringUtils; +import org.hibernate.transform.ResultTransformer; import org.jboss.seam.ScopeType; import org.jboss.seam.annotations.In; import org.jboss.seam.annotations.Name; @@ -53,7 +57,6 @@ import org.zanata.dao.PersonDAO; import org.zanata.dao.ProjectIterationDAO; import org.zanata.dao.TextFlowTargetHistoryDAO; -import org.zanata.dao.TextFlowTargetHistoryDAO.UserTranslationMatrix; import org.zanata.model.HDocument; import org.zanata.model.HLocale; import org.zanata.model.HPerson; @@ -61,7 +64,7 @@ import org.zanata.model.HTextFlowTarget; import org.zanata.rest.NoSuchEntityException; import org.zanata.rest.dto.Link; -import org.zanata.rest.dto.matrix.UserWorkMatrix; +import org.zanata.rest.dto.TranslationMatrix; import org.zanata.rest.dto.stats.ContainerTranslationStatistics; import org.zanata.rest.dto.stats.TranslationStatistics; import org.zanata.rest.dto.stats.TranslationStatistics.StatUnit; @@ -106,6 +109,9 @@ public class StatisticsServiceImpl implements StatisticsResource { @In private PersonDAO personDAO; + @In + private EntityManager entityManager; + @In private TranslationStateCache translationStateCacheImpl; @@ -299,12 +305,10 @@ public ContainerTranslationStatistics getStatistics(String projectSlug, * Get contribution statistic (translations) from project-version within * given date range. * - * Throws NoSuchEntityException if: - * - project/version not found or is obsolete, - * - user not found + * Throws NoSuchEntityException if: - project/version not found or is + * obsolete, - user not found * - * Throws InvalidDateParamException if: - * - dateRangeParam is in wrong format, + * Throws InvalidDateParamException if: - dateRangeParam is in wrong format, * - date range is over MAX_STATS_DAYS * * @param projectSlug @@ -314,8 +318,8 @@ public ContainerTranslationStatistics getStatistics(String projectSlug, * @param username * username of contributor * @param dateRangeParam - * from..to (yyyy-mm-dd..yyyy-mm-dd), - * date range maximum: 365 days + * from..to (yyyy-mm-dd..yyyy-mm-dd), date range maximum: 365 + * days */ @Override public ContributionStatistics getContributionStatistics(String projectSlug, @@ -443,7 +447,7 @@ public ContainerTranslationStatistics getDocStatistics(Long documentId, @Path("user/{username}/{dateRangeParam}") @GET @Produces({"application/json"}) - public UserWorkMatrix getUserWorkMatrix( + public List getUserWorkMatrix( @PathParam("username") final String username, @PathParam("dateRangeParam") String dateRangeParam, @QueryParam("userTimeZone") String userTimeZoneID) { @@ -468,22 +472,60 @@ public UserWorkMatrix getUserWorkMatrix( userZoneOpt = Optional.absent(); } - // TODO pahuang restrict toDate to yesterday (with timezone) -// if (toDate.isAfter(new DateTime().withTimeAtStartOfDay())) { -// toDate = new DateTime().withTimeAtStartOfDay(); -// } - List databaseRecords = + List translationMatrixList = textFlowTargetHistoryDAO.getUserTranslationMatrix(person, - fromDate, toDate, userZoneOpt, systemZone); + fromDate, toDate, userZoneOpt, systemZone, + new UserMatrixResultTransformer(entityManager, dateFormatter)); - UserWorkMatrix result = new UserWorkMatrix(); + return translationMatrixList; + } - for (UserTranslationMatrix matrixRecord : databaseRecords) { - String dateString = dateFormatter.print( - matrixRecord.getSavedDate().getTime()); - result.putOrCreateIfAbsent(dateString, matrixRecord); + @RequiredArgsConstructor + public static class UserMatrixResultTransformer implements + ResultTransformer { + private static final long serialVersionUID = 1L; + private final EntityManager entityManager; + private final DateTimeFormatter dateFormater; + + @Override + public Object transformTuple(Object[] tuple, String[] aliases) { + String savedDate = dateFormater.print( + new DateTime(tuple[0]).toDate().getTime()); + HProjectIteration iteration = + entityManager.find(HProjectIteration.class, + ((BigInteger) tuple[1]).longValue()); + String projectSlug = iteration.getProject().getSlug(); + String projectName = iteration.getProject().getName(); + String versionSlug = iteration.getSlug(); + + HLocale locale = + entityManager.find(HLocale.class, + ((BigInteger) tuple[2]).longValue()); + String localeDisplayName = locale.retrieveDisplayName(); + LocaleId localeId = locale.getLocaleId(); + + ContentState savedState = ContentState.values()[(int) tuple[3]]; + long wordCount = + ((BigDecimal) tuple[4]).toBigInteger().longValue(); + + return new TranslationMatrix(savedDate, projectSlug, projectName, + versionSlug, localeId, localeDisplayName, + savedState, wordCount); } - return result; + @Override + public List transformList(List collection) { + return collection; + } + } + + @Getter + @RequiredArgsConstructor + public static class UserTranslationMatrix { + private final Date savedDate; + private final HProjectIteration projectIteration; + private final HLocale locale; + private final ContentState savedState; + private final long wordCount; } } diff --git a/zanata-war/src/test/java/org/zanata/dao/TextFlowTargetHistoryDAOTest.java b/zanata-war/src/test/java/org/zanata/dao/TextFlowTargetHistoryDAOTest.java index 6697a58f68..839cf7b31b 100644 --- a/zanata-war/src/test/java/org/zanata/dao/TextFlowTargetHistoryDAOTest.java +++ b/zanata-war/src/test/java/org/zanata/dao/TextFlowTargetHistoryDAOTest.java @@ -2,14 +2,18 @@ import java.util.List; +import org.hibernate.transform.ResultTransformer; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; +import org.joda.time.format.DateTimeFormat; +import org.joda.time.format.DateTimeFormatter; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; import org.zanata.ZanataJpaTest; import org.zanata.common.ContentState; import org.zanata.common.LocaleId; -import org.zanata.dao.TextFlowTargetHistoryDAO.UserTranslationMatrix; +import org.zanata.rest.dto.TranslationMatrix; +import org.zanata.rest.service.StatisticsServiceImpl; import org.zanata.model.HAccount; import org.zanata.model.HDocument; import org.zanata.model.HLocale; @@ -30,7 +34,6 @@ import lombok.RequiredArgsConstructor; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; @Test(groups = { "jpa-tests" }) public class TextFlowTargetHistoryDAOTest extends ZanataJpaTest { @@ -41,9 +44,13 @@ public class TextFlowTargetHistoryDAOTest extends ZanataJpaTest { private DateTime yesterday = new DateTime().minusDays(1); private DateTime twoDaysAgo = new DateTime().minusDays(2); private HDocument hDocument; + private ResultTransformer resultTransformer; + private static final DateTimeFormatter dateFormatter = DateTimeFormat.mediumDate(); @BeforeMethod public void setUp() throws Exception { + resultTransformer = new StatisticsServiceImpl.UserMatrixResultTransformer(getEm(), + dateFormatter); historyDAO = new TextFlowTargetHistoryDAO(getSession()) { @Override protected String stripTimeFromDateTimeFunction(String columnName) { @@ -125,19 +132,19 @@ public void canGetUserTranslationMatrix() { .withTargetState(ContentState.Translated).build(); getEm().flush(); - List result = + List result = historyDAO .getUserTranslationMatrix(user, twoDaysAgo.withTimeAtStartOfDay(), today.withTimeAtStartOfDay(), Optional. absent(), - DateTimeZone.getDefault()); + DateTimeZone.getDefault(), resultTransformer); assertThat(result).hasSize(4); final SavedDatePredicate yesterdayPredicate = new SavedDatePredicate(yesterday); - Iterable yesterdayFuzzy = + Iterable yesterdayFuzzy = Iterables .filter(result, Predicates.and(yesterdayPredicate, new ContentStatePredicate( @@ -150,7 +157,7 @@ Optional. absent(), .describedAs("total words saved as fuzzy yesterday") .isEqualTo(4); - Iterable yesterdayApproved = + Iterable yesterdayApproved = Iterables.filter(result, Predicates .and(yesterdayPredicate, @@ -163,7 +170,7 @@ Optional. absent(), .describedAs("total words saved as approved yesterday") .isEqualTo(2); - Iterable yesterdayTranslated = + Iterable yesterdayTranslated = Iterables.filter(result, Predicates.and(yesterdayPredicate, new ContentStatePredicate( @@ -175,7 +182,7 @@ Optional. absent(), .describedAs("total words saved as translated yesterday") .isEqualTo(2); - Iterable twoDaysAgoFuzzy = + Iterable twoDaysAgoFuzzy = Iterables.filter(result, Predicates.and(new SavedDatePredicate(twoDaysAgo), new ContentStatePredicate( @@ -205,11 +212,11 @@ public void whenSettingParameterIt() { .withTargetState(ContentState.Approved).build(); getEm().flush(); - List result = historyDAO + List result = historyDAO .getUserTranslationMatrix(user, new DateTime(2015, 2, 1, 1, 1, zone), new DateTime(zone), Optional. absent(), - DateTimeZone.getDefault()); + DateTimeZone.getDefault(), resultTransformer); assertThat(result).isEmpty(); } @@ -225,24 +232,26 @@ public void canConvertTimeZoneIfUSerSuppliedDifferentZone() { @RequiredArgsConstructor private static class ContentStatePredicate - implements Predicate { + implements Predicate { private final ContentState state; @Override - public boolean apply(UserTranslationMatrix input) { + public boolean apply(TranslationMatrix input) { return input.getSavedState() == state; } } - @RequiredArgsConstructor private static class SavedDatePredicate - implements Predicate { - private final DateTime theDate; + implements Predicate { + private final String theDate; + + private SavedDatePredicate(DateTime theDate) { + this.theDate = dateFormatter.print(theDate.withTimeAtStartOfDay()); + } @Override - public boolean apply(UserTranslationMatrix input) { - return theDate.withTimeAtStartOfDay() - .toDate().equals(input.getSavedDate()); + public boolean apply(TranslationMatrix input) { + return theDate.equals(input.getSavedDate()); } } }