it.newfammulfin.api.EntryResource.java Source code

Java tutorial

Introduction

Here is the source code for it.newfammulfin.api.EntryResource.java

Source

/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package it.newfammulfin.api;

import com.google.common.base.Joiner;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.Work;
import com.googlecode.objectify.cmd.Query;
import it.newfammulfin.api.util.GroupRetrieverRequestFilter;
import it.newfammulfin.api.util.OfyService;
import it.newfammulfin.api.util.RetrieveGroup;
import it.newfammulfin.api.util.Util;
import it.newfammulfin.model.Chapter;
import it.newfammulfin.model.Entry;
import it.newfammulfin.model.EntryOperation;
import it.newfammulfin.model.Group;
import it.newfammulfin.model.RegisteredUser;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
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;
import javax.ws.rs.QueryParam;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.SecurityContext;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVParser;
import org.apache.commons.csv.CSVRecord;
import org.joda.money.CurrencyUnit;
import org.joda.money.Money;
import org.joda.time.LocalDate;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;

/**
 *
 * @author eric
 */
@Path("groups/{groupId}/entries")
@Produces(MediaType.APPLICATION_JSON)
@RetrieveGroup
public class EntryResource {

    @Context
    private SecurityContext securityContext;
    @Context
    private ContainerRequestContext requestContext;
    private static final Logger LOG = Logger.getLogger(EntryResource.class.getName());
    private static final String CSV_CHAPTERS_SEPARATOR = " > ";
    private static final String CSV_TAGS_SEPARATOR = " > ";
    public static final int DEFAULT_SHARE_SCALE = 4;

    @GET
    public Response readAll(@PathParam("groupId") @NotNull Long groupId, @QueryParam("year") Integer year,
            @QueryParam("month") Integer month, @QueryParam("chapterId") Long chapterId,
            @QueryParam("payee") String payee) {
        Group group = (Group) requestContext.getProperty(GroupRetrieverRequestFilter.GROUP);
        Query<Entry> query = OfyService.ofy().load().type(Entry.class).ancestor(group);
        if (year != null && month == null) {
            query = query.filter("date >=", new LocalDate(year, 1, 1)).filter("date <",
                    new LocalDate(year + 1, 1, 1));
        }
        if (year != null && month != null) {
            if (month < 12) {
                query = query.filter("date >=", new LocalDate(year, month, 1)).filter("date <",
                        new LocalDate(year, month + 1, 1));
            } else {
                query = query.filter("date >=", new LocalDate(year, 12, 1)).filter("date <",
                        new LocalDate(year + 1, 1, 1));
            }
        }
        if (chapterId != null) {
            Set<Key<Chapter>> chapterKeys = new LinkedHashSet<>();
            Key<Chapter> chapterKey = Key.create(Key.create(group), Chapter.class, chapterId);
            chapterKeys.addAll(getChildChapterKeys(chapterKey, group));
            query = query.filter("chapterKey in", chapterKeys);
        }
        if (payee != null && !payee.isEmpty()) {
            query = query.filter("payee", payee);
        }
        List<Entry> entries = query.list();
        return Response.ok(entries).build();
    }

    private Set<Key<Chapter>> getChildChapterKeys(Key<Chapter> parentChapterKey, Group group) {
        Set<Key<Chapter>> chapterKeys = new LinkedHashSet<>();
        chapterKeys.add(parentChapterKey);
        List<Chapter> chapters = OfyService.ofy().load().type(Chapter.class).ancestor(group)
                .filter("parentChapterKey", parentChapterKey).list();
        for (Chapter chapter : chapters) {
            chapterKeys.addAll(getChildChapterKeys(Key.create(chapter), group));
        }
        return chapterKeys;
    }

    @GET
    @Path("template")
    public Response template(@PathParam("groupId") @NotNull Long groupId) {
        Group group = (Group) requestContext.getProperty(GroupRetrieverRequestFilter.GROUP);
        Key<RegisteredUser> userKey = Key.create(RegisteredUser.class,
                securityContext.getUserPrincipal().getName());
        Entry entry = new Entry();
        entry.setAmount(Money.of(group.getDefaultCurrencyUnit(), 2.20));
        entry.setDate(LocalDate.now());
        entry.setPayee("Pub");
        entry.setDescription("Beer");
        entry.getTags().add("Fun");
        entry.getByShares().put(userKey, entry.getAmount().getAmount());
        for (Key<RegisteredUser> otherSserKey : group.getUsersMap().keySet()) {
            entry.getForShares().put(otherSserKey, BigDecimal.ZERO);
        }
        checkAndBalanceZeroShares(entry.getForShares(), entry.getAmount().getAmount());
        return Response.ok(entry).build();
    }

    @GET
    @Path("{id:[0-9]+}")
    public Response read(@PathParam("id") @NotNull Long id) {
        Group group = (Group) requestContext.getProperty(GroupRetrieverRequestFilter.GROUP);
        Entry entry = OfyService.ofy().load().type(Entry.class).parent(group).id(id).now();
        if ((entry == null) || (!entry.getGroupKey().equals(Key.create(group)))) {
            return Response.status(Response.Status.NOT_FOUND)
                    .entity(String.format("Entry with id %d does not exist.", id)).type(MediaType.TEXT_PLAIN)
                    .build();
        }
        return Response.ok(entry).build();
    }

    //debug method, should remove in production
    @DELETE
    @Produces(MediaType.TEXT_PLAIN)
    public Response deleteAll() {
        Group group = (Group) requestContext.getProperty(GroupRetrieverRequestFilter.GROUP);
        List<Key<?>> keys = new ArrayList<>();
        keys.addAll(OfyService.ofy().load().type(Entry.class).ancestor(group).keys().list());
        keys.addAll(OfyService.ofy().load().type(EntryOperation.class).ancestor(group).keys().list());
        keys.addAll(OfyService.ofy().load().type(Chapter.class).ancestor(group).keys().list());
        OfyService.ofy().delete().keys(keys).now();
        return Response.ok(String.format("Removed %d entities.", keys.size())).build();
    }

    @DELETE
    @Path("{id:[0-9]+}")
    public Response delete(@PathParam("id") @NotNull Long id) {
        final Group group = (Group) requestContext.getProperty(GroupRetrieverRequestFilter.GROUP);
        final Entry entry = OfyService.ofy().load().type(Entry.class).parent(group).id(id).now();
        if ((entry == null) || (!entry.getGroupKey().equals(Key.create(group)))) {
            return Response.status(Response.Status.NOT_FOUND)
                    .entity(String.format("Entry with id %d does not exist.", id)).type(MediaType.TEXT_PLAIN)
                    .build();
        }
        OfyService.ofy().transact(new Work<Entry>() {
            @Override
            public Entry run() {
                List<EntryOperation> operations = OfyService.ofy().load().type(EntryOperation.class).ancestor(group)
                        .filter("entryKey", Key.create(entry)).list();
                OfyService.ofy().delete().entity(entry).now();
                OfyService.ofy().delete().entities(operations).now();
                LOG.info(String.format("%s deleted.", entry));
                return entry;
            }
        });
        return Response.ok().build();
    }

    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    public Response create(final @Valid @NotNull Entry entry) {
        final Group group = (Group) requestContext.getProperty(GroupRetrieverRequestFilter.GROUP);
        entry.setGroupKey(Key.create(group));
        //validate users
        if (!group.getUsersMap().keySet().containsAll(entry.getByShares().keySet())
                || !group.getUsersMap().keySet().containsAll(entry.getForShares().keySet())) {
            return Response.status(Response.Status.BAD_REQUEST)
                    .entity(String.format("Entry for/by contains unknown users.")).type(MediaType.TEXT_PLAIN)
                    .build();
        }
        OfyService.ofy().transact(new Work<Entry>() {
            @Override
            public Entry run() {
                OfyService.ofy().save().entity(entry).now();
                EntryOperation operation = new EntryOperation(Key.create(group), Key.create(entry), new Date(),
                        Key.create(RegisteredUser.class, securityContext.getUserPrincipal().getName()),
                        EntryOperation.Type.CREATE);
                OfyService.ofy().save().entity(operation).now();
                LOG.info(String.format("%s created.", entry));
                return entry;
            }
        });
        return Response.ok(entry).build();
    }

    @PUT
    @Path("{id:[0-9]+}")
    @Consumes(MediaType.APPLICATION_JSON)
    public Response update(@PathParam("id") @NotNull Long id, final @Valid @NotNull Entry entry) {
        final Group group = (Group) requestContext.getProperty(GroupRetrieverRequestFilter.GROUP);
        Entry existingEntry = OfyService.ofy().load().type(Entry.class).parent(group).id(id).now();
        if ((existingEntry == null) || (!existingEntry.getGroupKey().equals(Key.create(group)))) {
            return Response.status(Response.Status.NOT_FOUND)
                    .entity(String.format("Entry with id %d does not exist.", id)).type(MediaType.TEXT_PLAIN)
                    .build();
        }
        if (!existingEntry.getId().equals(id) || !existingEntry.getId().equals(entry.getId())) {
            LOG.warning(String.format("User %s attempted to change entry id: %d in path, %d in payload.",
                    securityContext.getUserPrincipal().getName(), existingEntry.getId(), entry.getId()));
            return Response.status(Response.Status.CONFLICT).entity("Cannot change entry id.")
                    .type(MediaType.TEXT_PLAIN).build();
        }
        //validate users
        if (!group.getUsersMap().keySet().containsAll(entry.getByShares().keySet())
                || !group.getUsersMap().keySet().containsAll(entry.getForShares().keySet())) {
            return Response.status(Response.Status.BAD_REQUEST)
                    .entity(String.format("Entry for/by contains unknown users.")).type(MediaType.TEXT_PLAIN)
                    .build();
        }
        entry.setGroupKey(Key.create(group));
        OfyService.ofy().transact(new Work<Entry>() {
            @Override
            public Entry run() {
                OfyService.ofy().save().entity(entry).now();
                EntryOperation operation = new EntryOperation(Key.create(group), Key.create(entry), new Date(),
                        Key.create(RegisteredUser.class, securityContext.getUserPrincipal().getName()),
                        EntryOperation.Type.UPDATE);
                OfyService.ofy().save().entity(operation).now();
                LOG.info(String.format("%s updated.", entry));
                return entry;
            }
        });
        return Response.ok(entry).build();
    }

    @GET
    @Path("{id:[0-9]+}/operations")
    public Response readOperations(@PathParam("id") @NotNull Long id) {
        Group group = (Group) requestContext.getProperty(GroupRetrieverRequestFilter.GROUP);
        Entry entry = OfyService.ofy().load().type(Entry.class).parent(group).id(id).now();
        if ((entry == null) || (!entry.getGroupKey().equals(Key.create(group)))) {
            return Response.status(Response.Status.NOT_FOUND)
                    .entity(String.format("Entry with id %d does not exist.", id)).type(MediaType.TEXT_PLAIN)
                    .build();
        }
        List<EntryOperation> operations = OfyService.ofy().load().type(EntryOperation.class).ancestor(group)
                .filter("entryKey", Key.create(entry)).list();
        return Response.ok(operations).build();
    }

    private <K> boolean checkAndBalanceZeroShares(final Map<K, BigDecimal> shares, BigDecimal expectedSum) {
        if (shares.isEmpty()) {
            return false;
        }
        boolean equalShares = false;
        if (!Util.containsNotZero(shares.values())) {
            equalShares = true;
            expectedSum = expectedSum.setScale(Math.max(DEFAULT_SHARE_SCALE, expectedSum.scale()));
            for (Map.Entry<K, BigDecimal> shareEntry : shares.entrySet()) {
                shareEntry.setValue(expectedSum.divide(BigDecimal.valueOf(shares.size()), RoundingMode.DOWN));
            }
        }
        K largestKey = shares.keySet().iterator().next();
        for (Map.Entry<K, BigDecimal> share : shares.entrySet()) {
            if (share.getValue().abs().compareTo(shares.get(largestKey).abs()) > 0) {
                largestKey = share.getKey();
            }
        }
        BigDecimal remainder = Util.remainder(shares.values(), expectedSum);
        if (remainder.compareTo(BigDecimal.ZERO) != 0) {
            shares.put(largestKey, shares.get(largestKey).add(remainder));
        }
        return equalShares;
    }

    @POST
    @Consumes("text/csv")
    @Produces(MediaType.TEXT_PLAIN)
    public Response importFromCsv(String csvData,
            @DefaultValue("false") @QueryParam("invertSign") final boolean invertSign) {
        final Group group = (Group) requestContext.getProperty(GroupRetrieverRequestFilter.GROUP);
        final Map<String, Key<Chapter>> chapterStringsMap = new HashMap<>();
        final List<CSVRecord> records;
        try {
            records = CSVParser.parse(csvData, CSVFormat.DEFAULT.withHeader()).getRecords();
        } catch (IOException e) {
            return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
                    .entity(String.format("Unexpected %s: %s.", e.getClass().getSimpleName(), e.getMessage()))
                    .build();
        }
        //check users
        final Set<String> userIds = new HashSet<>();
        for (String columnName : records.get(0).toMap().keySet()) {
            if (columnName.startsWith("by:")) {
                String userId = columnName.replaceFirst("by:", "");
                if (!group.getUsersMap().keySet().contains(Key.create(RegisteredUser.class, userId))) {
                    return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
                            .entity(String.format("User %s not found in this group.", userId)).build();
                }
                userIds.add(userId);
            }
        }
        //build chapters
        final Set<String> chapterStringsSet = new HashSet<>();
        for (CSVRecord record : records) {
            chapterStringsSet.add(record.get("chapters"));
        }
        final List<Key<?>> createdKeys = new ArrayList<>();
        try {
            OfyService.ofy().transact(new Work<List<Key<?>>>() {
                @Override
                public List<Key<?>> run() {
                    for (String chapterStrings : chapterStringsSet) {
                        List<String> pieces = Arrays.asList(chapterStrings.split(CSV_CHAPTERS_SEPARATOR));
                        Key<Chapter> parentChapterKey = null;
                        for (int i = 0; i < pieces.size(); i++) {
                            String partialChapterString = Joiner.on(CSV_CHAPTERS_SEPARATOR)
                                    .join(pieces.subList(0, i + 1));
                            Key<Chapter> chapterKey = chapterStringsMap.get(partialChapterString);
                            if (chapterKey == null) {
                                chapterKey = OfyService.ofy().load().type(Chapter.class).ancestor(group)
                                        .filter("name", pieces.get(i)).filter("parentChapterKey", parentChapterKey)
                                        .keys().first().now();
                                chapterStringsMap.put(partialChapterString, chapterKey);
                            }
                            if (chapterKey == null) {
                                Chapter chapter = new Chapter(pieces.get(i), Key.create(group), parentChapterKey);
                                OfyService.ofy().save().entity(chapter).now();
                                chapterKey = Key.create(chapter);
                                createdKeys.add(chapterKey);
                                LOG.info(String.format("%s created.", chapter));
                            }
                            chapterStringsMap.put(partialChapterString, chapterKey);
                            parentChapterKey = chapterKey;
                        }
                    }
                    //build entries
                    DateTimeFormatter formatter = DateTimeFormat.forPattern("dd/MM/YY");
                    Key<Group> groupKey = Key.create(group);
                    for (CSVRecord record : records) {
                        Entry entry = new Entry();
                        entry.setGroupKey(groupKey);
                        entry.setDate(LocalDate.parse(record.get("date"), formatter));
                        entry.setAmount(Money.of(CurrencyUnit.of(record.get("currency").toUpperCase()),
                                (invertSign ? -1 : 1) * Double.parseDouble(record.get("value"))));
                        if (!record.get("chapters").isEmpty()) {
                            entry.setChapterKey(chapterStringsMap.get(record.get("chapters")));
                        }
                        entry.setPayee(record.get("payee"));
                        for (String tag : record.get("tags").split(CSV_TAGS_SEPARATOR)) {
                            if (!tag.trim().isEmpty()) {
                                entry.getTags().add(tag);
                            }
                        }
                        entry.setDescription(record.get("description"));
                        entry.setNote(record.get("notes"));
                        int scale = Math.max(DEFAULT_SHARE_SCALE, entry.getAmount().getScale());
                        //by shares
                        for (String userId : userIds) {
                            String share = record.get("by:" + userId);
                            double value;
                            if (share.contains("%")) {
                                entry.setByPercentage(true);
                                value = Double.parseDouble(share.replace("%", ""));
                                value = entry.getAmount().getAmount().doubleValue() * value / 100d;
                            } else {
                                value = (invertSign ? -1 : 1) * Double.parseDouble(share);
                            }
                            entry.getByShares().put(Key.create(RegisteredUser.class, userId),
                                    BigDecimal.valueOf(value).setScale(scale, RoundingMode.DOWN));
                        }
                        boolean equalByShares = checkAndBalanceZeroShares(entry.getByShares(),
                                entry.getAmount().getAmount());
                        entry.setByPercentage(entry.isByPercentage() || equalByShares);
                        //for shares
                        for (String userId : userIds) {
                            String share = record.get("for:" + userId);
                            double value;
                            if (share.contains("%")) {
                                entry.setForPercentage(true);
                                value = Double.parseDouble(share.replace("%", ""));
                                value = entry.getAmount().getAmount().doubleValue() * value / 100d;
                            } else {
                                value = (invertSign ? -1 : 1) * Double.parseDouble(share);
                            }
                            entry.getForShares().put(Key.create(RegisteredUser.class, userId),
                                    BigDecimal.valueOf(value).setScale(scale, RoundingMode.DOWN));
                        }
                        boolean equalForShares = checkAndBalanceZeroShares(entry.getForShares(),
                                entry.getAmount().getAmount());
                        entry.setForPercentage(entry.isForPercentage() || equalForShares);
                        OfyService.ofy().save().entity(entry).now();
                        createdKeys.add(Key.create(entry));
                        EntryOperation operation = new EntryOperation(Key.create(group), Key.create(entry),
                                new Date(),
                                Key.create(RegisteredUser.class, securityContext.getUserPrincipal().getName()),
                                EntryOperation.Type.IMPORT);
                        OfyService.ofy().save().entity(operation).now();
                        LOG.info(String.format("%s created.", entry));
                    }
                    return createdKeys;
                }
            });
            //count keys
            int numberOfCreatedChapters = 0;
            int numberOfCreatedEntries = 0;
            for (Key<?> key : createdKeys) {
                if (key.getKind().equals(Entry.class.getSimpleName())) {
                    numberOfCreatedEntries = numberOfCreatedEntries + 1;
                } else if (key.getKind().equals(Chapter.class.getSimpleName())) {
                    numberOfCreatedChapters = numberOfCreatedChapters + 1;
                }
            }
            return Response.ok(String.format("Done: %d chapters and %d entries created.", numberOfCreatedChapters,
                    numberOfCreatedEntries)).build();
        } catch (RuntimeException e) {
            LOG.warning(String.format("Unexpected %s: %s.", e.getClass().getSimpleName(), e.getMessage()));
            return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
                    .entity(String.format("Unexpected %s: %s.", e.getClass().getSimpleName(), e.getMessage()))
                    .build();
        }
    }

}