/*
* 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 openbook.server;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.PersistenceContextType;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Expression;
import javax.persistence.criteria.ParameterExpression;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import javax.persistence.metamodel.EntityType;
import openbook.domain.Author;
import openbook.domain.Author_;
import openbook.domain.Book;
import openbook.domain.Book_;
import openbook.domain.Customer;
import openbook.domain.Customer_;
import openbook.domain.Inventory;
import openbook.domain.Inventory_;
import openbook.domain.LineItem;
import openbook.domain.PurchaseOrder;
import openbook.domain.PurchaseOrder_;
import openbook.domain.Range;
import openbook.domain.ShoppingCart;
import openbook.util.PropertyHelper;
import openbook.util.Randomizer;
import org.apache.openjpa.persistence.criteria.OpenJPACriteriaBuilder;
/**
* A demonstrative example of a transaction service with persistent entity using Java Persistence API.
* <br>
* This example service operates on a persistent domain model to browse {@linkplain Book books},
* occasionally {@linkplain #placeOrder(ShoppingCart) placing} {@linkplain PurchaseOrder purchase orders},
* while {@linkplain Inventory inventory} gets updated either by {@linkplain #deliver() delivery} or
* by {@linkplain #supply() supply}.
* <br>
* The operational model as well as the persistent domain model is influenced by the fact that
* a JPA based application can benefit from
* <LI>Mostly Immutable Persistent Data Model
* <LI>Optimistic Transaction Model
* <br>for better scalability and throughput.
* <br>
*
* @author Pinaki Poddar
*
*/
@SuppressWarnings("serial")
class OpenBookServiceImpl extends PersistenceService implements OpenBookService {
public static final int CUSTOMER_COUNT = 10;
public static final int BOOK_COUNT = 100;
public static final int AUTHOR_COUNT = 40;
public static final int AUTHOR_PER_BOOK = 3;
/**
* Range of number of queries executed for a {@linkplain #shop() shopping} trip.
*/
public static final Range<Double> PRICE_RANGE = new Range<Double>(4.99, 120.99);
public static final Range<Integer> STOCK_RANGE = new Range<Integer>(5, 50);
public static final int REORDER_LEVEL = 10;
OpenBookServiceImpl(String unit, EntityManagerFactory emf, boolean managed,
PersistenceContextType scope) {
super(unit, emf, managed, scope);
}
/**
* Initialize service by populating inventory of Books and Customers.
* If the inventory exists, then returns immediately without creating any new inventory.
*
* @return true if new inventory is created. false otherwise.
*/
public boolean initialize(Map<String,Object> config) {
if (isInitialized()) {
return false;
}
EntityManager em = begin();
if (config == null) {
config = Collections.EMPTY_MAP;
}
int nCustomer = PropertyHelper.getInteger(config, "openbook.Customer.Count", CUSTOMER_COUNT);
int nBook = PropertyHelper.getInteger(config, "openbook.Book.Count", BOOK_COUNT);
int nAuthor = PropertyHelper.getInteger(config, "openbook.Author.Count", AUTHOR_COUNT);
int nAuthorPerBook = PropertyHelper.getInteger(config, "openbook.Book.Author.Count", AUTHOR_PER_BOOK);
Double priceMax = PropertyHelper.getDouble(config, "openbook.Book.Price.Max", PRICE_RANGE.getMaximum());
Double priceMin = PropertyHelper.getDouble(config, "openbook.Book.Price.Min", PRICE_RANGE.getMinimum());
Integer stockMax = PropertyHelper.getInteger(config, "openbook.Inventory.Max", STOCK_RANGE.getMaximum());
Integer stockMin = PropertyHelper.getInteger(config, "openbook.Inventory.Min", STOCK_RANGE.getMinimum());
System.err.println("Creating " + nCustomer + " new Customer");
for (int i = 1; i < nCustomer; i++) {
Customer customer = new Customer();
customer.setName("Customer-"+i);
em.persist(customer);
}
List<Author> allAuthors = new ArrayList<Author>();
System.err.println("Creating " + nAuthor + " new Authors");
for (int i = 1; i <= nAuthor; i++) {
Author author = new Author();
author.setName("Author-"+i);
allAuthors.add(author);
em.persist(author);
}
System.err.println("Creating " + nBook + " new Books");
System.err.println("Linking at most " + nAuthorPerBook + " Authors per Book");
for (int i = 1; i <= nBook; i++) {
Book book = new Book(Randomizer.randomString(4,2),
"Book-" + i,
Randomizer.random(priceMin, priceMax),
Randomizer.random(stockMin, stockMax));
List<Author> authors = Randomizer.selectRandom(allAuthors,
Math.max(1, Randomizer.random(nAuthorPerBook)));
for (Author author : authors) {
author.addBook(book);
book.addAuthor(author);
}
em.persist(book);
}
commit();
return true;
}
/**
* Affirms whether the database is loaded with some records.
*/
public boolean isInitialized() {
return count(Book.class) > 0;
}
/**
* <A name="login">
* Provide a name to login a Customer.
* If such a customer exists, return it. Otherwise creates a new one.
*
* @param name name of an existing or a new Customer
*
* @return a Customer
*/
public Customer login(String name) {
EntityManager em = begin();
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Customer> q = cb.createQuery(Customer.class);
Customer customer = null;
Root<Customer> root = q.from(Customer.class);
ParameterExpression<String> pName = cb.parameter(String.class);
q.where(cb.equal(root.get(Customer_.name), pName));
List<Customer> customers = em.createQuery(q)
.setParameter(pName, name)
.getResultList();
if (customers.isEmpty()) {
Customer newCustomer = new Customer();
newCustomer.setName(name);
em.persist(newCustomer);
customer = newCustomer;
} else {
customer = customers.get(0);
}
commit();
return customer;
}
/**
* Find books that match title and price range.
*/
public List<Book> select(String title, Double minPrice, Double maxPrice, String author,
QueryDecorator...decorators) {
CriteriaQuery<Book> q = buildQuery(title, minPrice, maxPrice, author);
EntityManager em = begin();
TypedQuery<Book> query = em.createQuery(q);
List<Book> result = query.getResultList();
commit();
return result;
}
/**
* <A name="getQuery">
* Gets the string representation of a Criteria Query.
* The string form of a Criteria Query is not specified in JPA specification.
* But OpenJPA produces a readable form that is quite <em>similar</em> to
* equivalent JPQL.
*/
public String getQuery(String title, Double minPrice, Double maxPrice, String author) {
CriteriaQuery<Book> q = buildQuery(title, minPrice, maxPrice, author);
return q.toString();
}
/**
* <A name="buildQuery">
* Creates a Query based on the values of the user input form.
* The user may or may not have filled a value for each form field
* and accordingly the query will be different.<br>
* This is typical of a form-based query. To account for all possible
* combinations of field values to build a String-based JPQL can be
* a daunting exercise. This method demonstrates how such dynamic,
* conditional be alternatively developed using {@link CriteriaQuery}
* introduced in JPA version 2.0.
* <br>
*
* @return a typed query
*/
private CriteriaQuery<Book> buildQuery(String title, Double minPrice, Double maxPrice, String author) {
// builder generates the Criteria Query as well as all the expressions
CriteriaBuilder cb = getUnit().getCriteriaBuilder();
// The query declares what type of result it will produce
CriteriaQuery<Book> q = cb.createQuery(Book.class);
// Which type will be searched
Root<Book> book = q.from(Book.class);
// of course, the projection term must match the result type declared earlier
q.select(book);
// Builds the predicates conditionally for the filled-in input fields
List<Predicate> predicates = new ArrayList<Predicate>();
if (!isEmpty(title)) {
Predicate matchTitle = cb.like(book.get(Book_.title), title);
predicates.add(matchTitle);
}
if (!isEmpty(author)) {
Predicate matchAuthor = cb.like(book.join(Book_.authors).get(Author_.name), "%"+author+"%");
predicates.add(matchAuthor);
}
// for price fields, also the comparison operation changes based on whether
// minimum or maximum price or both have been filled.
if (minPrice != null && maxPrice != null) {
Predicate matchPrice = cb.between(book.get(Book_.price), minPrice, maxPrice);
predicates.add(matchPrice);
} else if (minPrice != null && maxPrice == null) {
Predicate matchPrice = cb.ge(book.get(Book_.price), minPrice);
predicates.add(matchPrice);
} else if (minPrice == null && maxPrice != null) {
Predicate matchPrice = cb.le(book.get(Book_.price), maxPrice);
predicates.add(matchPrice);
}
// Sets the evaluation criteria
if (!predicates.isEmpty())
q.where(predicates.toArray(new Predicate[predicates.size()]));
return q;
}
boolean isEmpty(String s) {
return s == null || s.trim().isEmpty();
}
/**
* <A name="deliver"/>
* Delivers the given order, if it is pending.
* Delivery of an order amounts to decrementing inventory for each line item
* and eventually nullify the line items to demonstrate orphan delete feature.
* <br>
* The transactions may fail because of either insufficient inventory or
* concurrent modification of the same inventory by {@link #supply(Book, int) the supplier}.
*/
public PurchaseOrder deliver(PurchaseOrder o) {
if (o.isDelivered())
return o;
EntityManager em = begin();
o = em.merge(o);
for (LineItem item : o.getItems()) {
item.getBook().getInventory().decrement(item.getQuantity());
}
o.setDelivered();
commit();
return o;
}
public List<PurchaseOrder> getOrders(PurchaseOrder.Status status, Customer customer) {
EntityManager em = begin();
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<PurchaseOrder> q = cb.createQuery(PurchaseOrder.class);
Root<PurchaseOrder> order = q.from(PurchaseOrder.class);
q.select(order);
List<Predicate> predicates = new ArrayList<Predicate>();
if (status != null) {
predicates.add(cb.equal(order.get(PurchaseOrder_.status), status));
}
if (customer != null) {
predicates.add(cb.equal(order.get(PurchaseOrder_.customer), customer));
}
if (!predicates.isEmpty())
q.where(predicates.toArray(new Predicate[predicates.size()]));
q.orderBy(cb.desc(order.get(PurchaseOrder_.placedOn)));
TypedQuery<PurchaseOrder> query = em.createQuery(q);
List<PurchaseOrder> result = query.getResultList();
commit();
return result;
}
/**
* <A name="placeOrder"/>
* Creates a new {@linkplain PurchaseOrder} from the content of the given {@linkplain ShoppingCart}.
* The content of the cart is cleared as a result.
* <br>
* The transaction is not expected to fail because the inventory is
* not modified by placing an order.
*
* @param cart a non-null Shopping cart.
*/
public PurchaseOrder placeOrder(ShoppingCart cart) {
EntityManager em = begin();
PurchaseOrder order = new PurchaseOrder(cart);
em.persist(order);
commit();
cart.clear();
return order;
}
/**
* Supply books that have low inventory.
* <br>
* Queries for books with low inventory and supply each book in separate
* transaction. Some of the transactions may fail due to concurrent modification on
* the {@linkplain Inventory} by the {@linkplain #deliver() delivery} process.
*/
public Book supply(Book b, int quantity) {
EntityManager em = begin();
b = em.merge(b);
b.getInventory().increment(quantity);
commit();
return b;
}
public List<Inventory> getReorderableBooks(int limit) {
EntityManager em = begin();
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Inventory> q = cb.createQuery(Inventory.class);
Root<Inventory> inv = q.from(Inventory.class);
q.select(inv);
Expression<Integer> inStock = cb.diff(
inv.get(Inventory_.supplied),
inv.get(Inventory_.sold));
q.orderBy(cb.asc(inStock));
List<Inventory> result = em.createQuery(q)
.setMaxResults(limit)
.getResultList();
commit();
return result;
}
public long count(Class<?> cls) {
EntityManager em = getEntityManager();
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Long> c = cb.createQuery(Long.class);
Root<?> from = c.from(cls);
c.select(cb.count(from));
return em.createQuery(c).getSingleResult();
}
public List<Book> selectByExample(Book b, QueryDecorator...decorators) {
return queryByTemplate(Book.class, b);
}
private <T> List<T> queryByTemplate(Class<T> type, T template) {
EntityManager em = begin();
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<T> c = cb.createQuery(type);
c.where(((OpenJPACriteriaBuilder)cb).qbe(c.from(type), template));
List<T> result = em.createQuery(c).getResultList();
commit();
return result;
}
public <T> List<T> getExtent(Class<T> entityClass) {
EntityManager em = begin();
CriteriaQuery<T> c = em.getCriteriaBuilder().createQuery(entityClass);
c.from(entityClass);
List<T> result = em.createQuery(c).getResultList();
commit();
return result;
}
public <T> List<T> query(String jpql, Class<T> resultClass, QueryDecorator... decorators) {
EntityManager em = begin();
TypedQuery<T> query = em.createQuery(jpql, resultClass);
if (decorators != null) {
for (QueryDecorator decorator : decorators) {
decorator.decorate(query);
}
}
List<T> result = query.getResultList();
commit();
return result;
}
public void clean() {
EntityManager em = begin();
Set<EntityType<?>> entities = em.getMetamodel().getEntities();
for (EntityType<?> type : entities) {
System.err.println("Deleting all instances of " + type.getName());
em.createQuery("delete from " + type.getName() + " p").executeUpdate();
}
commit();
}
}