/* * Copyright 2010 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package com.google.gwt.sample.expenses.server.domain; import com.google.appengine.api.datastore.Cursor; import org.datanucleus.store.appengine.query.JPACursorHelper; import java.util.Collections; import java.util.Date; import java.util.List; import java.util.logging.Logger; import javax.cache.Cache; import javax.cache.CacheException; import javax.cache.CacheFactory; import javax.cache.CacheManager; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.EntityManager; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.Query; import javax.persistence.Version; /** * Models an expense report. */ @Entity public class ReportBack { private static Cache cache; private static final Logger log = Logger.getLogger(ReportBack.class.getName()); /** * The total number of reports in the database. */ private static long REPORT_COUNT = 2; public static long countReports() { EntityManager em = entityManager(); try { return ((Number) em.createQuery("select count(o) from Report o").getSingleResult()).longValue(); } finally { em.close(); } } public static long countReportsBySearch(Long employeeId, String department, String startsWith) { EntityManager em = entityManager(); try { Query query = queryReportsBySearch(em, employeeId, department, startsWith, null, true); if (query == null) { return REPORT_COUNT; } long count = ((Number) query.getSingleResult()).longValue(); return count; } finally { em.close(); } } public static final EntityManager entityManager() { return EMF.get().createEntityManager(); } @SuppressWarnings("unchecked") public static List<ReportBack> findAllReports() { EntityManager em = entityManager(); try { List<ReportBack> reportList = em.createQuery("select o from Report o").getResultList(); // force it to materialize reportList.size(); return reportList; } finally { em.close(); } } public static ReportBack findReport(Long id) { if (id == null) { return null; } EntityManager em = entityManager(); try { return em.find(ReportBack.class, id); } finally { em.close(); } } @SuppressWarnings("unchecked") public static List<ReportBack> findReportEntries(int firstResult, int maxResults) { EntityManager em = entityManager(); try { List<ReportBack> reportList = em.createQuery("select o from Report o").setFirstResult( firstResult).setMaxResults(maxResults).getResultList(); // force it to materialize reportList.size(); return reportList; } finally { em.close(); } } @SuppressWarnings("unchecked") public static List<ReportBack> findReportEntriesBySearch(Long employeeId, String department, String startsWith, String orderBy, int firstResult, int maxResults) { EntityManager em = entityManager(); try { Query query = queryReportsBySearch(em, employeeId, department, startsWith, orderBy, false); // Try to get the memcache if (cache == null) { try { CacheFactory cacheFactory = CacheManager.getInstance().getCacheFactory(); cache = cacheFactory.createCache(Collections.emptyMap()); } catch (CacheException e) { log.warning("Exception retrieving memcache instance: " + e); } } // Try to get a cursor for the current query String encodedCursor = null; if (cache != null) { String key = createKey(employeeId, department, startsWith, orderBy, firstResult); encodedCursor = (String) cache.get(key); } if (encodedCursor != null) { // Got a cursor, use it Cursor cursor = Cursor.fromWebSafeString(encodedCursor); query.setHint(JPACursorHelper.CURSOR_HINT, cursor); query.setFirstResult(0); } else if (firstResult + maxResults < 1000) { // Results may be retrieved directly using "OFFSET" query.setHint(JPACursorHelper.CURSOR_HINT, null); query.setFirstResult(firstResult); } else { // Skip results int pos = (firstResult / 1000) * 1000; Cursor trialCursor = null; while (pos > 0) { String trialKey = createKey(employeeId, department, startsWith, orderBy, pos); String trialEncodedCursor = (String) cache.get(trialKey); if (trialEncodedCursor != null) { trialCursor = Cursor.fromWebSafeString(trialEncodedCursor); break; } pos -= 1000; } // If trialCursor is null, we'll start searching from result 0 query.setHint(JPACursorHelper.CURSOR_HINT, trialCursor); while (firstResult > pos) { int min = Math.min(firstResult - pos, 1000); // If we need to skip more than 1000 records, ensure the // breaks occur at multiples of 1000 in order to increase the // chances of reusing cursors from the memcache if (pos + min < firstResult) { int mod = (pos + min) % 1000; min -= mod; } query.setMaxResults(min); List<ReportBack> results = query.getResultList(); int count = results.size(); if (count == 0) { break; } pos += count; // Save the cursor for later Cursor cursor = JPACursorHelper.getCursor(results); if (cache != null) { String key = createKey(employeeId, department, startsWith, orderBy, pos); cache.put(key, cursor.toWebSafeString()); } query.setHint(JPACursorHelper.CURSOR_HINT, cursor); } } query.setMaxResults(maxResults); List<ReportBack> reportList = query.getResultList(); // force it to materialize reportList.size(); Cursor cursor = JPACursorHelper.getCursor(reportList); if (cache != null) { int pos = firstResult + reportList.size(); String key = createKey(employeeId, department, startsWith, orderBy, pos); cache.put(key, cursor.toWebSafeString()); } return reportList; } finally { em.close(); } } @SuppressWarnings("unchecked") public static List<ReportBack> findReportsByEmployee(Long employeeId) { EntityManager em = entityManager(); try { Query query = em.createQuery("select o from Report o where o.reporterKey =:reporterKey"); query.setParameter("reporterKey", employeeId); List<ReportBack> reportList = query.getResultList(); // force it to materialize reportList.size(); return reportList; } finally { em.close(); } } private static String createKey(Long employeeId, String department, String startsWith, String orderBy, int firstResult) { return "" + employeeId + "+" + encode(department) + "+" + encode(startsWith) + "+" + encode(orderBy) + "+" + firstResult; } /** * Returns a String based on an input String that provides the following * guarantees. * * <ol> * <li>The result contains no '+' characters * <li>Distinct inputs always produce distinct results * </ol> * * <p> * Note that the transformation is not required to be reversible. * * @param s the input String * @return a String suitable for use as part of a a memcache key */ private static String encode(String s) { if (s == null) { return ""; } s = s.replace("@", "@@"); s = s.replace("+", "@"); return s; } /** * Query for reports based on the search parameters. If startsWith is * specified, the results will not be ordered. * * @param em the {@link EntityManager} to use * @param employeeId the employee id * @param department the department to search * @param startsWith the starting string * @param orderBy the order of the results * @param isCount true to query on the count only * @return the query, or null to return full report count. */ private static Query queryReportsBySearch(EntityManager em, Long employeeId, String department, String startsWith, String orderBy, boolean isCount) { // Determine which parameters to include. boolean isFirstStatement = true; boolean hasEmployee = employeeId != null && employeeId >= 0; boolean hasDepartment = !hasEmployee && department != null && department.length() > 0; boolean hasStartsWith = startsWith != null && startsWith.length() > 0; // If we are counting and we don't have any query parameters, return null // to force #countReportsBySearch to return the full Report count. if (isCount && !hasEmployee && !hasDepartment && !hasStartsWith) { return null; } // Construct the query string. String retValue = isCount ? "count(o)" : "o"; String queryString = "select " + retValue + " from Report o"; if (hasEmployee) { queryString += isFirstStatement ? " WHERE" : " AND"; isFirstStatement = false; queryString += " o.reporterKey =:reporterKey"; } if (hasDepartment) { queryString += isFirstStatement ? " WHERE" : " AND"; isFirstStatement = false; queryString += " o.department =:department"; } if (hasStartsWith) { queryString += isFirstStatement ? " WHERE" : " AND"; isFirstStatement = false; queryString += " o.purposeLowerCase >=:startsWith"; queryString += " AND o.purposeLowerCase <=:startsWithZ"; } if (!hasStartsWith && orderBy != null && orderBy.length() >= 0) { queryString += " ORDER BY " + orderBy; } // Construct the query; Query query = em.createQuery(queryString); if (hasEmployee) { query.setParameter("reporterKey", employeeId); } if (hasDepartment) { query.setParameter("department", department); } if (hasStartsWith) { String startsWithLc = startsWith.toLowerCase(); query.setParameter("startsWith", startsWithLc); query.setParameter("startsWithZ", startsWithLc + "zzzzzz"); } return query; } // @JoinColumn private Long approvedSupervisorKey; private Date created; private String department; @Id @Column(name = "id") @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String notes; private String purpose; /** * Store a lower case version of the purpose for searching. */ @SuppressWarnings("unused") private String purposeLowerCase; /** * Store reporter's key instead of reporter. See: * http://code.google.com/appengine * /docs/java/datastore/relationships.html#Unowned_Relationships */ // @JoinColumn private Long reporterKey; @Version @Column(name = "version") private Integer version; public Long getApprovedSupervisorKey() { return approvedSupervisorKey; } public Date getCreated() { return this.created; } public String getDepartment() { return this.department; } public Long getId() { return this.id; } public String getNotes() { return this.notes; } public String getPurpose() { return this.purpose; } public Long getReporterKey() { return this.reporterKey; } public Integer getVersion() { return this.version; } public void persist() { EntityManager em = entityManager(); try { em.persist(this); } finally { em.close(); } } public void remove() { EntityManager em = entityManager(); try { ReportBack attached = em.find(ReportBack.class, this.id); em.remove(attached); } finally { em.close(); } } public void setApprovedSupervisorKey(Long approvedSupervisorKey) { this.approvedSupervisorKey = approvedSupervisorKey; } public void setCreated(Date created) { this.created = created; } public void setDepartment(String department) { this.department = department; } public void setId(Long id) { this.id = id; } public void setNotes(String notes) { this.notes = notes; } public void setPurpose(String purpose) { this.purpose = purpose; this.purposeLowerCase = purpose == null ? "" : purpose.toLowerCase(); } public void setReporterKey(Long reporter) { this.reporterKey = reporter; } public void setVersion(Integer version) { this.version = version; } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("Id: ").append(getId()).append(", "); sb.append("Version: ").append(getVersion()).append(", "); sb.append("Created: ").append(getCreated()).append(", "); sb.append("Department: ").append(getDepartment()).append(", "); sb.append("Notes: ").append(getNotes()).append(", "); sb.append("Purpose: ").append(getPurpose()).append(", "); sb.append("Reporter: ").append(getReporterKey()).append(", "); sb.append("ApprovedSupervisor: ").append(getApprovedSupervisorKey()); return sb.toString(); } }