/* * GNU GENERAL PUBLIC LICENSE, Version 3, 29 June 2007 */ package hudson.gwtmarketplace.domain.manager; import hudson.gwtmarketplace.client.exception.ExistingEntityException; import hudson.gwtmarketplace.client.exception.InvalidAccessException; import hudson.gwtmarketplace.client.model.Category; import hudson.gwtmarketplace.client.model.Pair; import hudson.gwtmarketplace.client.model.Product; import hudson.gwtmarketplace.client.model.ProductComment; import hudson.gwtmarketplace.client.model.ProductRating; import hudson.gwtmarketplace.client.model.Top10Lists; import hudson.gwtmarketplace.client.model.Triple; import hudson.gwtmarketplace.client.model.search.SearchResults; import hudson.gwtmarketplace.server.model.ProductImage; import java.io.Serializable; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.logging.Logger; import com.google.appengine.api.datastore.Blob; import com.google.appengine.api.datastore.EntityNotFoundException; import com.google.appengine.api.users.User; import com.google.appengine.api.users.UserServiceFactory; import com.googlecode.objectify.Key; import com.googlecode.objectify.Objectify; import com.googlecode.objectify.Query; public class ProductManager extends AbstractManager { private static final String TOKEN_CATEGORIES = "categories"; private static final String TOKEN_TOP10_MOST_VIEWED = "top10MostViewed"; public static final String TOKEN_VIEWS_BY_IP = "viewsByIP"; private static final String TOKEN_TOP10_HIGHEST_RATED = "top10HighestRates"; private static final String TOKEN_TOP10_RECENT_UPDATED = "top10RecentUpdated"; private static final String TOKEN_RATINGS_BY_IP = "ratingsByPi"; private static final Logger log = Logger.getLogger(ProductManager.class .getName()); private static final Comparator<Product> top10MostViewedComparator = new Comparator<Product>() { public int compare(Product obj1, Product obj2) { if (null == obj1) return -1; else if (null == obj2) return 1; else return -1 * obj1.getNumDailyViews().compareTo( obj2.getNumDailyViews()); }; }; private static final Comparator<Product> top10BestRatedComparator = new Comparator<Product>() { public int compare(Product obj1, Product obj2) { if (null == obj1 || null == obj1.getRating()) return -1; else if (null == obj2 || null == obj2.getRating()) return 1; else { if (obj1.getRating().equals(obj2.getRating())) { return (-1 * obj1.getTotalRatings().compareTo( obj2.getTotalRatings())); } else { return -1 * obj1.getRating().compareTo(obj2.getRating()); } } }; }; private static final Comparator<Product> top10RecentUpdatesComparator = new Comparator<Product>() { public int compare(Product obj1, Product obj2) { if (null == obj1) return -1; else if (null == obj2) return 1; else return -1 * (obj1.getUpdatedDate().compareTo(obj2 .getUpdatedDate())); }; }; public void resetDailyViews() { List<Product> products = toList(noTx().query(Product.class)); for (Product p : products) { p.setNumDailyViews(0); } noTx().put(products); getCache().clear(); } public byte[] getImageData(long productId) { String cacheKey = "thumbs:" + Long.toString(productId); byte[] data = (byte[]) getCache().get(cacheKey); if (null == data) { ProductImage image = singleResult(noTx().query(ProductImage.class) .ancestor(new Key<Product>(Product.class, productId))); if (null != image) { data = image.getData().getBytes(); getCache().put(cacheKey, data); } } return data; } public String setImageData(long productId, byte[] data) throws InvalidAccessException { Key<Product> productKey = new Key<Product>(Product.class, productId); Product product = getById(productId); if (null == product || null == data) return null; String iconKey = Long.toString(System.currentTimeMillis()); product.setIconKey(iconKey); update(product); Iterator<ProductImage> images = AbstractManager.noTx() .query(ProductImage.class).ancestor(productKey).fetch() .iterator(); while (images.hasNext()) { AbstractManager.noTx().delete(images.next()); } ProductImage img = new ProductImage(); img.setProductId(productKey); img.setData(new Blob(data)); AbstractManager.noTx().put(img); String cacheKey = "thumbs:" + productId; AbstractManager.getCache().put(cacheKey, data); return iconKey; } public ArrayList<Category> getCategories() { ArrayList<Category> categories = (ArrayList<Category>) getCache().get( TOKEN_CATEGORIES); if (null == categories) { categories = toList(noTx().query(Category.class).order("name")); getCache().put(TOKEN_CATEGORIES, categories); } return categories; } public Top10Lists getTops(Date highestKnownDate) { Top10Lists rtn = new Top10Lists(getTop10BestRated(), getTop10RecentUpdates(), getTop10MostViewed()); if (null == rtn.getMaxDate() || null == highestKnownDate || rtn.getMaxDate().getTime() > highestKnownDate.getTime()) return rtn; else return rtn; } public SearchResults<ProductComment> getComments(long productId, int pageNumber, int pageSize) { return toSearchResults( noTx().query(ProductComment.class) .ancestor(new Key<Product>(Product.class, productId)) .order("-createdDate").limit(pageSize) .offset(pageNumber * pageSize), null); } public Product getByAlias(String alias) { String cacheKey = "productByAlias:" + alias; if (getCache().containsKey(cacheKey)) return (Product) getCache().get(cacheKey); return getByAlias(alias, noTx()); } Product getByAlias(String alias, Objectify ofy) { String cacheKey = "productByAlias:" + alias; Product product = singleResult(ofy.query(Product.class).filter("alias", alias)); getCache().put(cacheKey, product); return product; } public Product getById(long id) { String cacheKey = "productByKey:" + id; if (getCache().containsKey(cacheKey)) return (Product) getCache().get(cacheKey); return getById(id, noTx()); } public Product getById(long id, Objectify ofy) { String cacheKey = "productByKey:" + id; Product product = ofy.find(new Key<Product>(Product.class, id)); getCache().put(cacheKey, product); return product; } public Pair<Product, Date> getForViewing(String alias, String ipAddress) { Product product = getByAlias(alias); if (null == product) return null; String prodToken = alias + ":" + ipAddress; Map<String, Boolean> viewCache = (Map<String, Boolean>) getCache().get( TOKEN_VIEWS_BY_IP); if (null == viewCache) { viewCache = new HashMap<String, Boolean>(); // we don't need to put it here because we will once we add the // entry } if (null == viewCache.get(prodToken)) { viewCache.put(prodToken, Boolean.TRUE); getCache().put(TOKEN_VIEWS_BY_IP, viewCache); if (null == product.getNumMonthlyViews()) product.setNumMonthlyViews(0); product.setNumDailyViews(product.getNumDailyViews().intValue() + 1); product.setNumMonthlyViews(product.getNumMonthlyViews().intValue() + 1); try { boolean reordered = updateTop10MostViewed(product); product.setActivityDate(new Date()); updateCache(product); noTx().put(product); return new Pair<Product, Date>(product, reordered ? product.getActivityDate() : toTopsDate(product)); } catch (Exception e) { wrap(e); return null; } } else { return new Pair<Product, Date>(product, toTopsDate(product)); } } public Pair<Product, String> getForEditing(String alias) throws InvalidAccessException { Product product = getByAlias(alias); if (null == product) return null; User user = UserServiceFactory.getUserService().getCurrentUser(); if (null == user || !user.getUserId().equals(product.getUserId())) throw new InvalidAccessException(); // String uploadKey = // BlobstoreServiceFactory.getBlobstoreService().createUploadUrl( // "/gwt_marketplace/uploadImage"); // return new Pair<Product, String>(product, uploadKey); return new Pair<Product, String>(product, null); } public SearchResults<Product> search( HashMap<String, String> namedParameters, ArrayList<String> generalParameters, int startIndex, int limit, String ordering, boolean ascending, Integer knownRowCount) { Query<Product> query = noTx().query(Product.class); addOrdering(query, ordering, ascending, "name", true); if (null != namedParameters && namedParameters.size() > 0) { for (Map.Entry<String, String> param : namedParameters.entrySet()) { if (param.getKey().equals("category")) { query.filter("categoryId", param.getValue()); } else if (param.getKey().equals("status")) { query.filter("status", param.getValue()); } else if (param.getKey().equals("license")) { query.filter("license", param.getValue()); } else if (param.getKey().equals("name")) { query.filter("name", param.getValue()); } else if (param.getKey().equals("tag")) { query.filter("tags", param.getValue()); } } } if (null != generalParameters && generalParameters.size() > 0) { query.filter("searchFields in", generalParameters); } return toSearchResults(query, knownRowCount); } public ArrayList<Product> getTop10MostViewed() { String cacheKey = TOKEN_TOP10_MOST_VIEWED; ArrayList<Product> products = (ArrayList<Product>) getCache().get( cacheKey); if (null == products) { products = toList(noTx().query(Product.class) .order("-numDailyViews").limit(10)); // for some reason, the > 0 filter isn't working for (int i = products.size() - 1; i >= 0; i--) { if (products.get(i).getNumDailyViews() == 0) products.remove(i); } Collections.sort(products, top10MostViewedComparator); getCache().put(cacheKey, products); } return products; } public ArrayList<Product> getTop10BestRated() { ArrayList<Product> products = null; String cacheKey = TOKEN_TOP10_HIGHEST_RATED; if (getCache().containsKey(cacheKey)) { products = (ArrayList<Product>) getCache().get(cacheKey); } else { products = toList(noTx().query(Product.class) .filter("rating !=", null).order("-rating").limit(10)); Collections.sort(products, top10BestRatedComparator); getCache().put(cacheKey, products); } return products; } public ArrayList<Product> getTop10RecentUpdates() { ArrayList<Product> products = null; String cacheKey = TOKEN_TOP10_RECENT_UPDATED; if (getCache().containsKey(cacheKey)) { products = (ArrayList<Product>) getCache().get(cacheKey); } else { products = toList(noTx().query(Product.class).order("-updatedDate") .limit(10)); getCache().put(cacheKey, products); } return products; } public Pair<Product, Date> addRating(long productId, int rating, Long userId) { Objectify ofy = tx(); try { Key<Product> productKey = new Key<Product>(Product.class, productId); Product product = ofy.find(productKey); product.setTotalRatings(product.getTotalRatings() + 1); product.setTotalRatingScore(product.getTotalRatingScore() + rating); product.setRating((float) ((float) product.getTotalRatingScore() / (float) product .getTotalRatings())); product.setUpdatedDate(new Date()); product.setActivityDate(new Date()); ofy.put(product); updateCache(product); ofy.getTxn().commit(); return new Pair<Product, Date>(product, toTopsDate(product)); } catch (Exception e) { ofy.getTxn().rollback(); wrap(e); return null; } } public Triple<ProductComment, Product, Date> addComment(long productId, ProductComment comment, String ipAddress) { User user = UserServiceFactory.getUserService().getCurrentUser(); Objectify ofy = tx(); try { Key<Product> productKey = new Key<Product>(Product.class, productId); Product product = ofy.find(productKey); List<Serializable> thingsToSave = new ArrayList<Serializable>(); boolean addComment = false; if (null != comment.getCommentText() && comment.getCommentText().length() > 0) { if (null != user) { comment.setUserAlias(user.getNickname()); comment.setUserId(user.getUserId()); } comment.setCreatedDate(new Date()); comment.setProductId(productKey); thingsToSave.add(comment); addComment = true; } // deal with the rating if (null != comment.getRating() && comment.getRating().intValue() > 0) { String ratingCacheKey = TOKEN_RATINGS_BY_IP + ":" + productId; Map<String, Integer> ratingsCache = (Map<String, Integer>) getCache() .get(ratingCacheKey); if (null == ratingsCache) { ratingsCache = new HashMap<String, Integer>(); ArrayList<ProductRating> ratings = toList(noTx().query( ProductRating.class).ancestor(productKey)); for (ProductRating rating : ratings) { ratingsCache.put(rating.getIpAddress(), rating.getRating()); } getCache().put(ratingCacheKey, ratingsCache); } if (null == ratingsCache.get(ipAddress)) { int rating = comment.getRating().intValue(); if (null == product.getTotalRatings()) product.setTotalRatings(1); else product.setTotalRatings(product.getTotalRatings() + 1); if (null == product.getTotalRatingScore()) product.setTotalRatingScore(rating); else product.setTotalRatingScore(product .getTotalRatingScore() + rating); product.setRating((float) ((float) product .getTotalRatingScore() / (float) product .getTotalRatings())); product.setUpdatedDate(new Date()); getCache().remove(TOKEN_TOP10_HIGHEST_RATED); ProductRating productRating = new ProductRating(); productRating.setCreatedDate(new Date()); productRating.setIpAddress(ipAddress); productRating.setProductId(productKey); productRating.setRating(rating); if (null != user) { productRating.setUserAlias(user.getNickname()); productRating.setUserId(user.getUserId()); } thingsToSave.add(productRating); ratingsCache.put(ipAddress, rating); getCache().put(ratingCacheKey, ratingsCache); } else { // we can't add the rating comment.setUnableToRate(Boolean.TRUE); } } if (thingsToSave.size() > 0) { if (addComment) { if (null == product.getNumComments()) product.setNumComments(1); else product.setNumComments(product.getNumComments() + 1); } product.setActivityDate(new Date()); thingsToSave.add(product); ofy.put((Iterable) thingsToSave); updateCache(product); ofy.getTxn().commit(); return new Triple<ProductComment, Product, Date>(comment, product, toTopsDate(product)); } else { return new Triple<ProductComment, Product, Date>(null, product, null); } } catch (Exception e) { ofy.getTxn().rollback(); wrap(e); return null; } } public void update(Product product) throws InvalidAccessException { log.info("Updating product '" + product.getAlias() + "'"); try { User user = UserServiceFactory.getUserService().getCurrentUser(); Product orig = noTx().get( new Key<Product>(Product.class, product.getId())); if (null == user || !user.getUserId().equals(orig.getUserId())) throw new InvalidAccessException(); if (!orig.getCategoryId().equals(product.getCategoryId())) { log.info("Category '" + product.getCategoryId() + "' does not equal '" + orig.getCategoryId() + "'"); Category category1 = singleResult(noTx().query(Category.class) .filter("alias", orig.getCategoryId())); if (null != category1) { if (null == category1.getNumProducts()) category1.setNumProducts(0); else { category1.setNumProducts(category1.getNumProducts() .intValue() - 1); } log.info("Previous category products: '" + category1.getNumProducts()); } Category category2 = singleResult(noTx().query(Category.class) .filter("alias", product.getCategoryId())); if (null == category2.getNumProducts()) category2.setNumProducts(1); else { category2.setNumProducts(category2.getNumProducts() .intValue() + 1); } log.info("New category products: '" + category2.getNumProducts()); List<Category> toUpdate = new ArrayList<Category>(); toUpdate.add(category1); toUpdate.add(category2); noTx().put(toUpdate); getCache().remove(TOKEN_CATEGORIES); product.setCategoryName(category2.getName()); } } catch (EntityNotFoundException e) { // this is a problem - we should be saving here throw new RuntimeException(e); } product.setUpdatedDate(new Date()); updateProductSearchFields(product); noTx().put(product); updateCache(product); getCache().remove(TOKEN_TOP10_RECENT_UPDATED); } public Product save(Product product) throws ExistingEntityException, InvalidAccessException { User user = getCurrentUser(); if (null == user) throw new InvalidAccessException(); product.setUserId(user.getUserId()); Date date = new Date(); product.setUpdatedDate(date); product.setCreatedDate(date); product.setActivityDate(date); Integer zero = new Integer(0); product.setNumComments(zero); product.setNumDailyViews(zero); product.setNumMonthlyViews(zero); product.setTotalRatings(zero); product.setAlias(product.getName().replace(' ', '_').toLowerCase()); while (product.getAlias().startsWith("_")) product.setAlias(product.getAlias().substring(1)); Product another = getByAlias(product.getAlias()); if (null != another) { throw new ExistingEntityException("alias"); } Category category = singleResult(noTx().query(Category.class).filter( "alias", product.getCategoryId())); product.setCategoryName(category.getName()); updateProductSearchFields(product); noTx().put(product); updateCache(product); category.setNumProducts(category.getNumProducts().intValue() + 1); noTx().put(category); getCache().remove(TOKEN_CATEGORIES); getCache().remove(TOKEN_TOP10_RECENT_UPDATED); return product; } private void updateProductSearchFields(Product product) { List<String> searchFields = new ArrayList<String>(); for (String s : product.getName().split(" ")) searchFields.add(s); if (null != product.getTags()) { for (String s : product.getTags()) searchFields.add(s); } product.setSearchFields(searchFields.toArray(new String[searchFields .size()])); } private Date toTopsDate(Product product) { long maxTime = -1; ArrayList<Product> products = getTop10MostViewed(); for (Product _product : products) { if (_product.getActivityDate().getTime() > maxTime) maxTime = _product.getActivityDate().getTime(); } products = getTop10BestRated(); for (Product _product : products) { if (_product.getActivityDate().getTime() > maxTime) maxTime = _product.getActivityDate().getTime(); } products = getTop10RecentUpdates(); for (Product _product : products) { if (_product.getActivityDate().getTime() > maxTime) maxTime = _product.getActivityDate().getTime(); } if (maxTime == -1) return null; else return new Date(maxTime); } private void updateCache(Product product) { String cacheKey = "productByKey:" + product.getId(); getCache().put(cacheKey, product); cacheKey = "productByAlias:" + product.getAlias(); getCache().put(cacheKey, product); } private boolean updateTop10MostViewed(Product product) { int dailyViews = product.getNumDailyViews(); List<Product> top10 = getTop10MostViewed(); boolean needsReordering = false; if (top10.size() < 10) needsReordering = true; else if (top10.get(9).getNumDailyViews() < product.getNumDailyViews()) needsReordering = true; synchronized (ProductManager.class) { if (needsReordering) { if (!top10.contains(product)) { if (top10.size() < 10) top10.add(product); else { top10.remove(9); top10.add(product); } } else { top10.remove(product); top10.add(product); } Collections.sort(top10, top10MostViewedComparator); String cacheKey = TOKEN_TOP10_MOST_VIEWED; getCache().put(cacheKey, top10); return true; } else { return false; } } } private void updateTop10HighestRated(Product product) { if (null == product.getRating()) return; float rating = product.getRating(); List<Product> top10 = getTop10BestRated(); // see if we need to re-order boolean needsReordering = false; boolean entityFound = false; for (int i = 0; i < top10.size(); i++) { Product _p = top10.get(i); if (_p.equals(product)) { entityFound = true; if (i > 0) { _p = top10.get(i - 1); float _rating = _p.getRating(); if (_rating < rating) needsReordering = true; } if (i < (top10.size() - 1)) { _p = top10.get(i + 1); float _rating = _p.getRating(); if (_rating > rating) needsReordering = true; } break; } } if (!entityFound) { if (top10.size() < 10) { needsReordering = true; } else if (top10.get(9).getRating() < rating) { needsReordering = true; } } synchronized (ProductManager.class) { if (needsReordering) { if (!entityFound) { top10.add(product); } Collections.sort(top10, top10BestRatedComparator); String cacheKey = TOKEN_TOP10_HIGHEST_RATED; getCache().put(cacheKey, top10); } } } private void updateTop10RecentUpdated(Product product) { long date = product.getUpdatedDate().getTime(); List<Product> top10 = getTop10RecentUpdates(); // see if we need to re-order boolean needsReordering = false; boolean entityFound = false; for (int i = 0; i < top10.size(); i++) { Product _p = top10.get(i); if (_p.equals(product)) { entityFound = true; if (i > 0) { _p = top10.get(i - 1); long _date = _p.getUpdatedDate().getTime(); if (_date < date) needsReordering = true; } if (i < (top10.size() - 1)) { _p = top10.get(i + 1); long _date = _p.getUpdatedDate().getTime(); if (_date > date) needsReordering = true; } break; } } if (!entityFound) { if (top10.size() < 10) { needsReordering = true; } else if (top10.get(9).getUpdatedDate().getTime() < date) { needsReordering = true; } } synchronized (ProductManager.class) { if (needsReordering) { if (!entityFound) { top10.add(product); } Collections.sort(top10, top10RecentUpdatesComparator); String cacheKey = TOKEN_TOP10_RECENT_UPDATED; getCache().put(cacheKey, top10); } } } }