org.fracturedatlas.athena.apa.impl.jpa.JpaApaAdapter.java Source code

Java tutorial

Introduction

Here is the source code for org.fracturedatlas.athena.apa.impl.jpa.JpaApaAdapter.java

Source

/*
ATHENA Project: Management Tools for the Cultural Sector
Copyright (C) 2010, Fractured Atlas
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/
 */
package org.fracturedatlas.athena.apa.impl.jpa;

import java.util.*;
import java.util.List;
import java.util.Map.Entry;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.NoResultException;
import javax.persistence.PersistenceUnit;
import javax.persistence.Query;
import org.apache.commons.collections.CollectionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.fracturedatlas.athena.apa.ApaAdapter;
import org.fracturedatlas.athena.apa.IndexingApaAdapter;
import org.fracturedatlas.athena.apa.exception.ApaException;
import org.fracturedatlas.athena.apa.exception.ImmutableObjectException;
import org.fracturedatlas.athena.apa.exception.InvalidFieldException;
import org.fracturedatlas.athena.apa.exception.InvalidPropException;
import org.fracturedatlas.athena.apa.exception.InvalidValueException;
import org.fracturedatlas.athena.apa.impl.LongUserType;
import org.springframework.beans.factory.annotation.Autowired;
import org.fracturedatlas.athena.search.AthenaSearch;
import org.fracturedatlas.athena.search.AthenaSearchConstraint;
import org.fracturedatlas.athena.client.PTicket;
import org.fracturedatlas.athena.search.Operator;

public class JpaApaAdapter extends IndexingApaAdapter implements ApaAdapter {

    @Autowired
    private EntityManagerFactory emf;
    Logger logger = LoggerFactory.getLogger(this.getClass().getName());

    public JpaApaAdapter() {
        initializeIndex();
    }

    @PersistenceUnit
    public void setEntityManagerFactory(EntityManagerFactory emf) {
        this.emf = emf;
    }

    @Override
    public PTicket getRecord(String type, Object id) {
        JpaRecord r = getTicket(type, id);
        if (r != null) {
            return r.toClientTicket();
        } else {
            return null;
        }
    }

    @Override
    public Set<String> getTypes() {
        EntityManager em = this.emf.createEntityManager();
        Set<String> types = new HashSet<String>();
        try {
            Query query = em.createQuery("SELECT DISTINCT t.type FROM JpaRecord t");
            List<String> values = query.getResultList();
            logger.debug("PropValues are " + values.toString());
            types.addAll(values);
            return types;
        } finally {
            cleanup(em);
        }
    }

    private JpaRecord getTicket(String type, Object id) {
        return getTicket(null, type, id);
    }

    private JpaRecord getTicket(EntityManager em, String type, Object id) {
        boolean closeTransaction = (em == null);
        if (em == null) {
            em = this.emf.createEntityManager();
        }
        try {
            Long longId = LongUserType.massageToLong(id);
            if (longId == null) {
                return null;
            } else {
                try {
                    JpaRecord t = (JpaRecord) em
                            .createQuery("from JpaRecord as ticket where id=:id AND type=:ticketType")
                            .setParameter("id", longId).setParameter("ticketType", type).getSingleResult();
                    return t;
                } catch (NoResultException nre) {
                    return null;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        } finally {
            if (closeTransaction) {
                cleanup(em);
            }
        }
    }

    private JpaRecord saveRecord(JpaRecord t, EntityManager em) {
        boolean useTransaction = (em == null);

        if (em == null) {
            em = this.emf.createEntityManager();
        }
        try {
            if (useTransaction) {
                em.getTransaction().begin();
            }
            t.setId(LongUserType.massageToLong(t.getId()));
            // Make sure the TicketProps are set to this existingTicket
            // If the caller forgets to do it, and we don't do it here, then JPA
            // will bonk
            if (t.getTicketProps() != null) {
                for (TicketProp prop : t.getTicketProps()) {
                    enforceStrict(prop.getPropField(), prop.getValueAsString());
                    enforceCorrectValueType(prop.getPropField(), prop, em);
                    prop.setTicket(t);
                }
            }
            t = (JpaRecord) em.merge(t);
            if (useTransaction) {

                em.getTransaction().commit();
            }
            return t;
        } catch (ApaException e) {
            if (useTransaction) {
                em.getTransaction().rollback();
            }
            throw e;
        } finally {
            if (useTransaction) {
                cleanup(em);
            }
        }
    }

    private JpaRecord saveRecord(JpaRecord t) {
        return saveRecord(t, null);
    }

    @Override
    public PTicket saveRecord(PTicket record) {
        return saveRecord(record.getType(), record);
    }

    @Override
    public PTicket saveRecord(String type, PTicket record) {
        if (type == null) {
            throw new ApaException("Cannot save a record without a type");
        }

        //if this existingTicket has an id
        if (record.getId() != null) {
            return updateTicketFromClientTicket(type, record, record.getId()).toClientTicket();
        } else {
            return createAndSaveTicketFromClientTicket(type, record).toClientTicket();
        }
    }

    @Override
    public PTicket patchRecord(Object idToPatch, String type, PTicket patchRecord) {

        EntityManager em = this.emf.createEntityManager();

        try {
            JpaRecord existingRecord = getTicket(type, idToPatch);
            if (existingRecord == null) {
                throw new ApaException("Record with id [" + idToPatch + "] was not found to patch");
            }
            if (patchRecord == null) {
                return existingRecord.toClientTicket();
            }

            em.getTransaction().begin();

            //delete properties on the patch
            for (Entry<String, List<String>> entry : patchRecord.getProps().entrySet()) {
                TicketProp prop = getTicketProp(entry.getKey(), type, idToPatch);
                if (prop != null) {
                    prop = em.merge(prop);
                    existingRecord.getTicketProps().remove(prop);
                    em.remove(prop);
                    existingRecord = em.merge(existingRecord);
                }
            }

            //Now patch the records
            List<TicketProp> propsToSave = buildProps(type, existingRecord, patchRecord, em);
            for (TicketProp prop : propsToSave) {

                //TODO: This should be done when we're building the prop
                enforceStrict(prop.getPropField(), prop.getValueAsString());
                prop = (TicketProp) em.merge(prop);
                existingRecord.addTicketProp(prop);
            }
            existingRecord = saveRecord(existingRecord, em);
            em.getTransaction().commit();
            return existingRecord.toClientTicket();
        } finally {
            cleanup(em);
        }
    }

    /*
     * updateTicketFromClientTicket assumes that PTicket has been sent with an ID.
     * updateTicketFromClientTicket will load a existingTicket with that ID.
     *
     * Any system props will be updated by the props on this PTicket.
     * No system props will be deleted
     * All regular props will be deleted
     * New props will be created from this pTicket
     *
     * basic algorithm:
     * - get all props on this existingTicket
     * - delete all that are not system props
     * - create and dave new props on this pTicket
     * - for all system props on pTicket
     * - if prop exists, update it
     * - else, save a new one
     *
     * It would be smarter to bulk delete all props on the existingTicket, then just create new props
     * But, JPA/Hibernate chokes on the query to delete all props, prob something to do with the
     * STI on TicketProp
     */
    public JpaRecord updateTicketFromClientTicket(String type, PTicket clientTicket, Object idToUpdate)
            throws InvalidPropException, InvalidValueException {

        EntityManager em = this.emf.createEntityManager();

        try {
            em.getTransaction().begin();
            JpaRecord existingTicket = getTicket(em, type, idToUpdate);
            deletePropsFromRecord(existingTicket, clientTicket, em);
            List<TicketProp> propsToSave = buildProps(type, existingTicket, clientTicket, em);

            for (TicketProp prop : propsToSave) {

                //TODO: This should be done when we're building the prop
                enforceStrict(prop.getPropField(), prop.getValueAsString());
                prop = (TicketProp) em.merge(prop);
                existingTicket.addTicketProp(prop);
            }
            existingTicket = saveRecord(existingTicket, em);
            em.getTransaction().commit();
            addToIndex(existingTicket.toClientTicket());
            return existingTicket;
        } catch (ApaException e) {
            em.getTransaction().rollback();
            throw e;
        } finally {
            cleanup(em);
        }
    }

    /*
     * createAndSaveTicketFromClientTicket assumes that PTicket has been sent WITHOUT an ID.
     * createAndSaveTicketFromClientTicket will create a new existingTicket using magic and wizardry
     */
    private JpaRecord createAndSaveTicketFromClientTicket(String type, PTicket clientTicket)
            throws InvalidPropException, InvalidValueException {

        JpaRecord ticket = new JpaRecord();

        //for all props on this pTicket, create new props with apa
        Set<String> keys = clientTicket.getProps().keySet();
        for (String key : keys) {
            for (String val : clientTicket.getProps().get(key)) {
                buildPropOntoTicket(ticket, key, val);
            }
        }
        keys = clientTicket.getSystemProps().keySet();
        for (String key : keys) {
            for (String val : clientTicket.getSystemProps().get(key)) {
                buildPropOntoTicket(ticket, key, val);
            }
        }
        ticket.setType(type);
        ticket = saveRecord(ticket);
        addToIndex(ticket.toClientTicket());
        return ticket;
    }

    private List<TicketProp> buildProps(String type, JpaRecord existingRecord, PTicket newRecord,
            EntityManager em) {
        List<TicketProp> propsToSave = new ArrayList<TicketProp>();
        Set<String> keys = newRecord.getProps().keySet();
        for (String key : keys) {
            for (String val : newRecord.getProps().get(key)) {
                propsToSave.add(buildNewProp(em, existingRecord, type, key, val));
            }
        }

        /*
         * Save all the new system props
         */
        keys = newRecord.getSystemProps().keySet();
        for (String key : keys) {
            String val = newRecord.getSystemProps().getFirst(key);
            propsToSave.add(buildNewProp(em, existingRecord, type, key, val));
        }

        return propsToSave;
    }

    /*
     * Delete anything that isn't a system prop
     * Also delete system props that exist on the incoming record
     * This method DOES NOT manage transactions
     */
    private void deletePropsFromRecord(JpaRecord existingRecord, PTicket newRecord, EntityManager em) {
        Iterator<TicketProp> iter = existingRecord.getTicketProps().iterator();
        List<TicketProp> propsToDelete = new ArrayList<TicketProp>();
        while (iter.hasNext()) {
            TicketProp prop = (TicketProp) iter.next();
            if (!prop.isSystemProp()) {
                propsToDelete.add(prop);
            } else {
                List<String> values = newRecord.getSystemProps().get(prop.getPropField().getName());
                if (values != null) {
                    propsToDelete.add(prop);
                }
            }
        }

        for (TicketProp prop : propsToDelete) {
            deleteTicketProp(prop, em);
        }
    }

    private TicketProp buildNewProp(EntityManager em, JpaRecord ticket, String type, String key, String val) {
        PropField propField = getPropField(key, em);
        validatePropField(propField, key, val);
        TicketProp ticketProp = propField.getValueType().newTicketProp();
        ticketProp.setPropField(propField);
        ticketProp.setTicket(ticket);
        ticketProp.setValue(val);
        return ticketProp;
    }

    private TicketProp buildExistingProp(JpaRecord ticket, String type, String key, String val) {

        TicketProp ticketProp = getTicketProp(key, type, ticket.getId());

        if (ticketProp == null) {
            PropField propField = getPropField(key);
            validatePropField(propField, key, val);

            ticketProp = propField.getValueType().newTicketProp();
            ticketProp.setPropField(propField);
            ticketProp.setTicket(ticket);
        }

        ticketProp.setValue(val);

        return ticketProp;
    }

    private JpaRecord buildPropOntoTicket(JpaRecord ticket, String key, String val) {
        logger.debug("Creating property: {}={}", key, val);
        PropField propField = getPropField(key);
        logger.debug("Found PropField: {}", propField);
        validatePropField(propField, key, val);
        TicketProp ticketProp = propField.getValueType().newTicketProp();
        ticketProp.setPropField(propField);
        ticketProp.setValue(val);
        ticketProp.setTicket(ticket);
        logger.debug("Creating TicketProp: [{}]", ticketProp.getClass().getName());
        logger.debug("{}={}", ticketProp.getPropField().getName(), ticketProp.getValueAsString());
        ticket.addTicketProp(ticketProp);
        return ticket;
    }

    /**
     * This method will throw ObjectNotFoundException if propField is null.
     *
     * @param propField the prop field to validate
     * @param key the name of the propField.  Used to validate that propField exists and is correct.
     * @param value the value that will be validated if propField is strict
     * @throws PropFieldNotFoundException
     */
    private void validatePropField(PropField propField, String key, String value) throws InvalidPropException {
        if (propField == null) {
            throw new InvalidPropException("Field with name [" + key + "] does not exist");
        }
    }

    private void enforceStrict(PropField propField, String value) throws InvalidValueException {

        if (propField.getStrict()) {

            //Reload the propField because we <3 Hibernate
            propField = getPropField(propField.getId());
            Collection<PropValue> propValues = propField.getPropValues();
            PropValue targetValue = new PropValue(propField, value);

            //TODO: Should be using a .contains method here or something
            for (PropValue propValue : propValues) {
                if (propValue.getPropValue().equals(value)) {
                    return;
                }
            }
            throw new InvalidValueException("Value [" + value + "] is not a valid value for the strict field ["
                    + propField.getName() + "]");
        }
    }

    @Override
    public Boolean deleteRecord(String type, Object id) {
        logger.debug("Deleting ticket: " + id);
        if (id == null) {
            return false;
        }
        EntityManager em = this.emf.createEntityManager();
        try {
            Long longId = LongUserType.massageToLong(id);
            em.getTransaction().begin();
            JpaRecord t = em.find(JpaRecord.class, longId);
            logger.trace("Deleting ticket: " + t);
            em.remove(t);
            logger.trace("Deleted ticket: " + longId);
            em.getTransaction().commit();
            deleteFromIndex(id);
            return true;
        } finally {
            cleanup(em);
        }
    }

    /**
     * Find tickets according to the AtheaSearch.
     * Type must be specified, but search constraints may be empty.  This method honors start, end, and limit modifiers.
     *
     * If one of the fields is ValueType.TEXT, this method will throw an ApaException
     *
     * @param athenaSearch a set of search constraints and search modifiers
     * @return Set of tickets whose Props match the athenaSearch
     */
    @Override
    public Set<PTicket> findTickets(AthenaSearch athenaSearch) {
        logger.debug("Searching for tickets:");
        logger.debug("{}", athenaSearch);

        checkValueTypes(athenaSearch);

        EntityManager em = this.emf.createEntityManager();
        Query query = null;
        Collection<JpaRecord> finishedTicketsList = null;
        Set<JpaRecord> finishedTicketsSet = null;
        Collection<JpaRecord> ticketsList = null;

        if (athenaSearch.isQuerySearch()) {
            Set<PTicket> tickets = new HashSet<PTicket>();
            Set<Object> ids = searchIndex(athenaSearch);
            for (Object id : ids) {
                PTicket ticket = getRecord(athenaSearch.getType(), id);
                if (ticket != null) {
                    tickets.add(ticket);
                } else {
                    logger.error("Found an id [{}] in the index that wasn't persisted in the DB", id);
                }
            }
            return tickets;
        }

        try {
            //if there are no modifiers, grab all records of (type)
            if (CollectionUtils.isEmpty(athenaSearch.getConstraints())) {
                logger.debug("No modifiers, getting all records of specified type");
                finishedTicketsSet = getRecordsByType(athenaSearch, em);
            } else {
                //else, search with the modifiers
                //TODO: This block runs independent searches for each constraint, then just smashes those lists together
                //Smarter way would be to run the first constraint, then search THAT list for the next constraint
                //Even smarter: be clever about which constraint we search for first.

                for (AthenaSearchConstraint apc : athenaSearch.getConstraints()) {
                    logger.debug("Searching on modifier: {}", apc);

                    if (apc.getOper().equals(Operator.MATCHES)) {
                        if (apc.getValue().equals(AthenaSearch.ANY_VALUE)) {
                            ticketsList = getRecordsWithFieldDefined(athenaSearch.getType(), apc, em);
                        } else {
                            throw new UnsupportedOperationException("Regex searching is not supported");
                        }
                    } else {
                        ticketsList = getRecordsForConstraint(athenaSearch.getType(), apc, em);
                    }

                    logger.debug("Found {} tickets", ticketsList.size());
                    if (finishedTicketsList == null) {
                        finishedTicketsList = ticketsList;
                    } else {
                        logger.debug("Smashing together ticket lists");
                        finishedTicketsList = CollectionUtils.intersection(finishedTicketsList, ticketsList);
                    }
                    logger.debug("{} tickets remain", finishedTicketsList.size());
                }
                if (finishedTicketsList == null) {
                    finishedTicketsList = new ArrayList<JpaRecord>();
                }
                Integer limit = athenaSearch.getLimit();
                Integer start = athenaSearch.getStart();

                finishedTicketsSet = new HashSet<JpaRecord>();
                finishedTicketsSet.addAll(finishedTicketsList);
                finishedTicketsSet = enforceStartAndLimit(finishedTicketsSet, start, limit);
            }

            logger.debug("Returning {} tickets", finishedTicketsSet.size());
            return convert(finishedTicketsSet);
        } catch (ApaException ex) {

            throw ex;
        } finally {
            cleanup(em);
        }
    }

    private void checkValueTypes(AthenaSearch athenaSearch) {
        for (AthenaSearchConstraint apc : athenaSearch.getConstraints()) {
            String fieldName = apc.getParameter();

            //TODO: This is done twice (see checkValueTypes), a bit of a waste of time
            PropField pf = getPropField(fieldName);
            if (pf != null) {
                ValueType vt = pf.getValueType();
                if (vt.equals(ValueType.TEXT)) {
                    throw new ApaException("You cannot search on TEXT fields");
                }
            } else {
                throw new InvalidFieldException("No Property Field called " + fieldName + " exists.");
            }
        }
    }

    private Set<PTicket> convert(Set<JpaRecord> jpaRecords) {
        Set<PTicket> out = new HashSet<PTicket>();
        for (JpaRecord r : jpaRecords) {
            out.add(r.toClientTicket());
        }
        return out;
    }

    private Set<JpaRecord> enforceStartAndLimit(Set<JpaRecord> ticketSet, Integer start, Integer limit) {

        Integer from = 0;
        Integer to = 0;

        from = (start == null) ? 0 : start;

        //I'm not sure this statement could be any more unclear.  It looks like Haskell
        //If limit isn't set, or it is set higher than the number of tickets + start_offset, then set the "to" to the number of tickets we have
        //otherwise, set "to" to limit+start
        to = (limit == null || limit + from > ticketSet.size()) ? ticketSet.size() : limit + from;

        logger.debug("Enforcing limit:");
        logger.debug("FROM: {}", from);
        logger.debug("TO:   {}", to);

        //short circuit all of this if we can.  If they've asked for more tickets than we found, punch out
        if (from == 0 && to >= ticketSet.size()) {
            return ticketSet;
        }

        //if the start is greater than the number of tickets we've found, return nothing
        if (from > to) {
            return new HashSet<JpaRecord>();
        }

        JpaRecord[] ticketArray = new JpaRecord[ticketSet.size()];
        ticketArray = ticketSet.toArray(ticketArray);
        JpaRecord[] outTickets = Arrays.copyOfRange(ticketArray, from, to);
        ticketSet = new HashSet(Arrays.asList(outTickets));
        return ticketSet;
    }

    private Set<JpaRecord> getRecordsByType(AthenaSearch athenaSearch, EntityManager em) {
        Set<JpaRecord> finishedTicketsSet = null;

        Query query = em.createQuery("from JpaRecord as ticket where type=:ticketType").setParameter("ticketType",
                athenaSearch.getType());

        if (athenaSearch.getLimit() != null) {
            query.setMaxResults(athenaSearch.getLimit());
        }

        if (athenaSearch.getStart() != null) {
            query.setFirstResult(athenaSearch.getStart());
        }

        finishedTicketsSet = new HashSet<JpaRecord>(query.getResultList());
        return finishedTicketsSet;
    }

    private Collection<JpaRecord> getRecordsWithFieldDefined(String type, AthenaSearchConstraint apc,
            EntityManager em) {
        Set<JpaRecord> tickets = new HashSet<JpaRecord>();
        Collection<TicketProp> props = getTicketPropsForType(type, apc.getParameter());
        for (TicketProp prop : props) {
            tickets.add(prop.getTicket());
        }
        return tickets;
    }

    private Collection<TicketProp> getTicketPropsForType(String type, String fieldName) {
        EntityManager em = this.emf.createEntityManager();

        try {
            //TODO: Would this be faster to first select propFields with fieldName, then use that id to
            //search ticketProp?
            Query query = em.createQuery(
                    "FROM TicketProp ticketProp WHERE ticketProp.propField.name=:fieldName AND ticketProp.ticket.type=:type");
            query.setParameter("type", type);
            query.setParameter("fieldName", fieldName);

            //TODO: There must be a better way of getting asingle result in JPA.
            //Using exceptions as flow control is kinda lame
            try {
                List ticketProp = query.getResultList();
                return ticketProp;
            } catch (javax.persistence.NoResultException nre) {
                return null;
            }
        } finally {
            cleanup(em);
        }
    }

    private Collection<JpaRecord> getRecordsForConstraint(String type, AthenaSearchConstraint apc,
            EntityManager em) {

        PropField pf = null;
        ValueType vt = null;
        String fieldName = null;
        List<TicketProp> props = null;
        JpaRecord tempTicket = null;
        Collection<JpaRecord> ticketsList = null;

        fieldName = apc.getParameter();

        //TODO: This is done twice (see checkValueTypes), a bit of a waste of time
        pf = getPropField(fieldName);
        if (pf != null) {
            vt = pf.getValueType();
        } else {
            throw new InvalidFieldException("No Property Field called " + fieldName + " exists.");
        }

        logger.debug("{}", apc);

        TicketProp prop = vt.newTicketProp();
        prop.setPropField(pf);
        Query query = buildQuery(type, apc, pf, vt, em);
        ticketsList = new ArrayList<JpaRecord>();

        if (query != null) {
            props = query.getResultList();
            for (TicketProp tp : props) {
                tempTicket = tp.getTicket();
                ticketsList.add(tempTicket);
            }
        }

        return ticketsList;
    }

    private Query buildQuery(String type, AthenaSearchConstraint apc, PropField pf, ValueType vt,
            EntityManager em) {

        Query query = null;
        String queryString = null;
        String singleValue = null;
        Iterator<String> it = null;
        Set<Object> valuesAsObjects = null;

        TicketProp prop = vt.newTicketProp();
        prop.setPropField(pf);
        Set<String> values = apc.getValueSet();

        if (values.size() > 1) {
            it = values.iterator();
            valuesAsObjects = new HashSet<Object>();
            while (it.hasNext()) {
                singleValue = it.next();
                try {
                    prop.setValue(singleValue);
                    valuesAsObjects.add(prop.getValue());
                } catch (Exception ex) {
                    //TODO: This is bad. We should blow up here

                }
            }
            queryString = "FROM " + prop.getClass().getName()
                    + " ticketProp WHERE ticketProp.propField.name=:fieldName AND ticketProp.value "
                    + apc.getOper().getOperatorString();

            if (type != null) {
                queryString += " AND ticketProp.ticket.type=:ticketType ";
            }

            query = em.createQuery(queryString);
            query.setParameter("value", valuesAsObjects);
            query.setParameter("fieldName", apc.getParameter());

            if (type != null) {
                query.setParameter("ticketType", type);
            }

        } else {
            try {
                prop.setValue(values.iterator().next());
                queryString = "FROM " + prop.getClass().getName()
                        + " ticketProp WHERE ticketProp.propField.name=:fieldName AND ticketProp.value "
                        + apc.getOper().getOperatorString();

                if (type != null) {
                    queryString += " AND ticketProp.ticket.type=:ticketType ";
                }

                query = em.createQuery(queryString);
                query.setParameter("value", prop.getValue());
                query.setParameter("fieldName", apc.getParameter());

                if (type != null) {
                    query.setParameter("ticketType", type);
                }
            } catch (InvalidValueException e) {
                //this is cool, continue
            }
        }

        return query;
    }

    private List<TicketProp> saveTicketProps(List<TicketProp> props) {
        List<TicketProp> outProps = new ArrayList<TicketProp>();
        EntityManager em = this.emf.createEntityManager();
        try {
            em.getTransaction().begin();

            for (TicketProp prop : props) {
                enforceStrict(prop.getPropField(), prop.getValueAsString());
                enforceCorrectValueType(prop.getPropField(), prop, em);
                prop.setId(LongUserType.massageToLong(prop.getId()));
                prop = (TicketProp) em.merge(prop);
            }
            em.getTransaction().commit();
            return outProps;
        } catch (ApaException e) {

            em.getTransaction().rollback();
            throw e;
        } finally {
            cleanup(em);
        }

    }

    private TicketProp saveTicketProp(TicketProp prop) throws InvalidValueException {
        EntityManager em = this.emf.createEntityManager();
        try {
            em.getTransaction().begin();
            enforceStrict(prop.getPropField(), prop.getValueAsString());
            enforceCorrectValueType(prop.getPropField(), prop, em);
            prop.setId(LongUserType.massageToLong(prop.getId()));
            prop = (TicketProp) em.merge(prop);
            em.getTransaction().commit();
            return prop;
        } finally {
            cleanup(em);
        }
    }

    /**
     * This method will not hydrate TicketProp.geTTicket because no type information is available
     * @param id
     * @return the ticketProp, null if not found
     */
    @Override
    public TicketProp getTicketProp(Object id) {
        EntityManager em = this.emf.createEntityManager();
        try {
            TicketProp prop = em.find(TicketProp.class, LongUserType.massageToLong(id));
            return prop;
        } finally {
            cleanup(em);
        }
    }

    /**
     * Return the first ticketprop where name=fieldName and ticket_id = ticketId.
     *
     * Callers of this method are assuming that there is only one prop that meets the above conditions
     *
     * If ticketId is null, this method will return null.  If no existingTicket prop
     * is found for the given conditions, this method will return null;
     *
     * This method WILL hydrate TicketProp.getTicket().  
     *
     * @param fieldName
     * @param type
     * @param ticketId
     * @return the ticketprop, null if not found.
     */
    @Override
    public TicketProp getTicketProp(String fieldName, String type, Object ticketId) {

        //This is to get around a bug in Derby that prevents us selecting on a null
        if (ticketId == null) {
            return null;
        }

        EntityManager em = this.emf.createEntityManager();

        try {
            Long longTicketId = LongUserType.massageToLong(ticketId);

            //TODO: Would this be faster to first select propFields with fieldName, then use that id to
            //search ticketProp?
            Query query = em.createQuery(
                    "FROM TicketProp ticketProp WHERE ticketProp.propField.name=:fieldName AND ticketProp.ticket.id=:ticketId");
            query.setParameter("fieldName", fieldName);
            query.setParameter("ticketId", longTicketId);

            //TODO: There must be a better way of getting asingle result in JPA.
            //Using exceptions as flow control is kinda lame
            try {
                TicketProp ticketProp = (TicketProp) query.getSingleResult();
                //ticketProp.setTicket(getTicket(type, ticketId));
                return ticketProp;
            } catch (javax.persistence.NoResultException nre) {
                return null;
            }
        } finally {
            cleanup(em);
        }
    }

    @Override
    public List getTicketProps(String fieldName, String type, Object ticketId) {
        EntityManager em = this.emf.createEntityManager();

        //This is to get around a bug in Derby that prevents us selecting on a null
        if (ticketId == null) {
            return null;
        }

        try {
            Long longTicketId = LongUserType.massageToLong(ticketId);

            //TODO: Would this be faster to first select propFields with fieldName, then use that id to
            //search ticketProp?
            Query query = em.createQuery(
                    "FROM TicketProp ticketProp WHERE ticketProp.propField.name=:fieldName AND ticketProp.ticket.id=:ticketId");
            query.setParameter("fieldName", fieldName);
            query.setParameter("ticketId", longTicketId);

            //TODO: There must be a better way of getting asingle result in JPA.
            //Using exceptions as flow control is kinda lame
            try {
                List ticketProp = query.getResultList();
                return ticketProp;
            } catch (javax.persistence.NoResultException nre) {
                return null;
            }
        } finally {
            cleanup(em);
        }
    }

    @Override
    public List getTicketProps(String fieldName) {
        EntityManager em = this.emf.createEntityManager();

        try {
            //TODO: Would this be faster to first select propFields with fieldName, then use that id to
            //search ticketProp?
            Query query = em.createQuery("FROM TicketProp ticketProp WHERE ticketProp.propField.name=:fieldName");
            query.setParameter("fieldName", fieldName);

            //TODO: There must be a better way of getting asingle result in JPA.
            //Using exceptions as flow control is kinda lame
            try {
                List ticketProp = query.getResultList();
                return ticketProp;
            } catch (javax.persistence.NoResultException nre) {
                return null;
            }
        } finally {
            cleanup(em);
        }
    }

    @Override
    public PropField getPropField(Object id) {
        EntityManager em = this.emf.createEntityManager();
        try {
            Long longId = LongUserType.massageToLong(id);
            PropField pf = em.find(PropField.class, longId);
            return pf;
        } finally {
            cleanup(em);
        }
    }

    public PropField getPropField(String name, EntityManager em) {
        boolean closeEm = false;
        if (em == null) {
            closeEm = true;
            em = this.emf.createEntityManager();
        }
        try {
            Query query = em.createQuery("FROM PropField pf where pf.name=:name");
            query.setParameter("name", name);

            try {
                PropField propField = (PropField) query.getSingleResult();
                return propField;
            } catch (javax.persistence.NoResultException nre) {
                return null;
            }
        } finally {
            if (closeEm) {
                cleanup(em);
            }
        }
    }

    @Override
    public PropField getPropField(String name) {
        return getPropField(name, null);
    }

    @Override
    public PropField savePropField(PropField propField) throws ImmutableObjectException {

        //strict must be set
        if (propField.getValueType() == null) {
            throw new ApaException("Please specify a value type");
        }

        //strict must be set
        if (propField.getStrict() == null) {
            throw new ApaException("Please specify strict as true or false");
        }

        if (propField.getStrict() && propField.getValueType().equals(ValueType.BOOLEAN)) {
            throw new ApaException("Boolean fields cannot be marked as strict");
        }

        if (propField.getStrict() && propField.getValueType().equals(ValueType.TEXT)) {
            throw new ApaException("Text fields cannot be marked as strict");
        }

        //check for immutability
        if (propField.getId() != null) {
            PropField oldPropField = getPropField(propField.getId());
            checkExists(oldPropField);
            checkImmutability(propField, oldPropField);
        } else {
            checkForDuplicatePropField(propField.getName());
        }
        checkForDuplicatePropValue(propField);

        EntityManager em = this.emf.createEntityManager();
        try {
            em.getTransaction().begin();
            propField.setId(LongUserType.massageToLong(propField.getId()));
            // loop through propValue ids to massage to long
            Collection<PropValue> propValues = propField.getPropValues();
            if (propValues != null) {
                for (PropValue propValue : propValues) {
                    propValue.setId(LongUserType.massageToLong(propValue.getId()));
                    propValue.setPropField(propField);
                }
            }
            propField = (PropField) em.merge(propField);

            if (propField.getPropValues() == null) {
                propField.setPropValues(new ArrayList<PropValue>());
            }

            em.getTransaction().commit();
            return propField;
        } finally {
            cleanup(em);
        }
    }

    @Override
    public PropValue savePropValue(PropValue propValue) {
        EntityManager em = this.emf.createEntityManager();
        try {
            PropField tmpPropField = getPropField(propValue.getPropField().getId());
            for (PropValue value : tmpPropField.getPropValues()) {
                if (propValue.getPropValue().equals(value.getPropValue())) {
                    throw new ApaException("Field [" + tmpPropField.getId() + "] already has a value set of ["
                            + value.getPropValue() + "]");
                }
            }

            em.getTransaction().begin();
            if (tmpPropField != null) {
                tmpPropField.setId(LongUserType.massageToLong(tmpPropField.getId()));
                propValue.setPropField(tmpPropField);
            }
            propValue.setId(LongUserType.massageToLong(propValue.getId()));
            propValue = (PropValue) em.merge(propValue);
            em.getTransaction().commit();
            return propValue;
        } finally {
            cleanup(em);
        }
    }

    private void checkForDuplicatePropValue(PropField propField) throws ApaException {
        if (propField.getPropValues() == null) {
            return;
        }

        Set<PropValue> duplicates = new TreeSet<PropValue>(new PropValue.PropValueComparator());
        for (PropValue value : propField.getPropValues()) {
            if (!duplicates.add(value)) {
                throw new ApaException("Cannot save Field [" + propField.getId()
                        + "] because it contains duplicate values of [" + value.getPropValue() + "]");
            }
        }
    }

    private void checkForDuplicatePropField(String name) throws ApaException {
        PropField duplicate = getPropField(name);
        if (duplicate != null) {
            throw new ApaException("Field [" + name + "] already exists.");
        }
    }

    private void checkExists(PropField propField) throws ApaException {
        if (propField == null) {
            throw new ApaException(
                    "Cannot update field with Id [" + propField.getId() + "] because the propField was not found");
        }
    }

    private void checkImmutability(PropField newPropField, PropField oldPropField) throws ImmutableObjectException {
        if (!newPropField.getStrict().equals(oldPropField.getStrict())) {
            throw new ImmutableObjectException(
                    "You cannot change the strictness of a field after is has been saved");
        } else if (!newPropField.getValueType().equals(oldPropField.getValueType())) {
            throw new ImmutableObjectException("You cannot change the type of a field after is has been saved");
        }
    }

    /*
     * So nuts that you can't do a em.remove(prop)
     */
    @Override
    public void deleteTicketProp(TicketProp prop) {
        deleteTicketProp(prop, null);

    }

    public void deleteTicketProp(TicketProp prop, EntityManager em) {
        boolean useTransaction = (em == null);
        if (em == null) {
            em = this.emf.createEntityManager();
        }
        try {
            if (useTransaction) {
                em.getTransaction().begin();
            }
            prop = em.merge(prop);

            if (prop == null) {
                throw new ApaException("Cannot delete prop.  Prop was not found.");
            }

            JpaRecord t = prop.getTicket();

            if (t == null) {
                throw new ApaException("Cannot delete prop.  This prop has not been assigned to a ticket.");
            }
            t.getTicketProps().remove(prop);
            em.remove(prop);
            t = em.merge(t);
            if (useTransaction) {
                em.getTransaction().commit();
            }
        } finally {
            if (useTransaction) {
                cleanup(em);
            }
        }
    }

    @Override
    public Boolean deletePropField(Object id) {
        EntityManager em = this.emf.createEntityManager();
        try {
            Long longId = LongUserType.massageToLong(id);
            PropField pf = em.find(PropField.class, longId);
            if (pf != null) {
                em.getTransaction().begin();
                em.remove(pf);
                em.getTransaction().commit();
                return true;
            } else {
                return false;
            }
        } finally {
            cleanup(em);
        }
    }

    @Override
    public Boolean deletePropField(PropField propField) {
        return deletePropField(propField.getId());
    }

    @Override
    public Collection<PropField> getPropFields() {
        EntityManager em = this.emf.createEntityManager();
        try {
            Query query = em.createQuery("FROM PropField pf");
            return query.getResultList();
        } finally {
            cleanup(em);
        }
    }

    @Override
    public Collection<PropValue> getPropValues(Object propFieldId) {
        EntityManager em = this.emf.createEntityManager();
        try {
            PropField pf = em.find(PropField.class, LongUserType.massageToLong(propFieldId));
            logger.debug("PropField is " + pf.toString());
            Query query = em.createQuery("FROM PropValue propValue WHERE propField IN (:pf)");
            query.setParameter("pf", pf);
            List<PropValue> values = query.getResultList();
            logger.debug("PropValues are " + values.toString());
            return values;
        } finally {
            cleanup(em);
        }
    }

    @Override
    public void deletePropValue(Object propFieldId, Object propValueId) {
        EntityManager em = this.emf.createEntityManager();
        try {
            Long longId = LongUserType.massageToLong(propValueId);
            PropValue pv = em.find(PropValue.class, longId);
            if (pv != null) {
                PropField propField = getPropField(pv.getPropField().getId());
                propField.getPropValues().remove(pv);
                em.getTransaction().begin();
                em.remove(pv);
                propField = em.merge(propField);
                em.getTransaction().commit();
            }
        } finally {
            cleanup(em);
        }
    }

    @Override
    public void deletePropValue(PropValue propValue) {
        if (propValue != null) {
            deletePropValue(propValue.getPropField(), propValue.getId());
        }
    }

    private void enforceCorrectValueType(PropField propField, TicketProp prop, EntityManager em)
            throws InvalidValueException {
        Long longId = LongUserType.massageToLong(propField.getId());
        propField = em.find(PropField.class, longId);
        if (!propField.getValueType().newTicketProp().getClass().getName().equals(prop.getClass().getName())) {
            String err = "Value [" + prop.getValueAsString() + "] is not a valid value for the field ["
                    + propField.getName() + "].  ";
            err += "Field is of type [" + propField.getValueType().name() + "].";
            throw new InvalidValueException(err);
        }
    }

    private void cleanup(EntityManager em) {
        if (em.getTransaction().isActive()) {
            em.getTransaction().rollback();
        }
        em.close();
    }
}