Project: ATHENA
/*
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.indexing.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) { 
         
        if(athenaSearch.getType() == null) { 
            throw new ApaException("You must specify a record type when doing a search"); 
        } 
         
        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()); 
            Set<PTicket> results = convert(finishedTicketsSet); 
            return loadRelationships(results, athenaSearch.getIncludes()); 
        } 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(); 
    } 
}