Java tutorial
/** * Tricode Blog module * Is a Blog module for Magnolia CMS. * Copyright (C) 2015 Tricode Business Integrators B.V. * * 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 nl.tricode.magnolia.blogs.templates; import info.magnolia.cms.util.QueryUtil; import info.magnolia.context.MgnlContext; import info.magnolia.context.WebContext; import info.magnolia.jcr.util.ContentMap; import info.magnolia.jcr.util.NodeUtil; import info.magnolia.jcr.wrapper.I18nNodeWrapper; import info.magnolia.rendering.model.RenderingModel; import info.magnolia.rendering.model.RenderingModelImpl; import info.magnolia.rendering.template.RenderableDefinition; import info.magnolia.templating.functions.TemplatingFunctions; import nl.tricode.magnolia.blogs.BlogsNodeTypes; import nl.tricode.magnolia.blogs.util.BlogWorkspaceUtil; import org.apache.commons.collections.IteratorUtils; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; import javax.jcr.Node; import javax.jcr.NodeIterator; import javax.jcr.RepositoryException; import javax.jcr.Value; import javax.jcr.query.Query; import java.text.DateFormat; import java.text.DateFormatSymbols; import java.text.SimpleDateFormat; import java.util.*; import java.util.Map.Entry; /** * Magnolia Renderable Definition for blog items. * * @author gtenham */ public class BlogRenderableDefinition<RD extends RenderableDefinition> extends RenderingModelImpl { private static final Logger log = LoggerFactory.getLogger(BlogRenderableDefinition.class); private static final int DEFAULT_LATEST_COUNT = 5; private static final String BLOG_NODE_TYPE = "mgnl:blog"; private static final String DEFAULT_LANGUAGE = "en"; private static final String PARAM_CATEGORY = "category"; private static final String PARAM_TAG = "tag"; private static final String PARAM_AUTHOR = "author"; private static final String PARAM_PAGE = "page"; private static final String PARAM_YEAR = "year"; private static final String PARAM_MONTH = "month"; private final WebContext webContext = MgnlContext.getWebContext(); private static final List<String> WHITELISTED_PARAMETERS = Arrays.asList(PARAM_CATEGORY, PARAM_TAG, PARAM_AUTHOR, PARAM_PAGE, PARAM_YEAR, PARAM_MONTH); private final Map<String, String> filter; protected final TemplatingFunctions templatingFunctions; @Override public String execute() { webContext.getResponse().setHeader("Cache-Control", "no-cache"); return super.execute(); } @Inject public BlogRenderableDefinition(Node content, RD definition, RenderingModel<?> parent, TemplatingFunctions templatingFunctions) { super(content, definition, parent); this.templatingFunctions = templatingFunctions; filter = new HashMap<String, String>(); // TODO: why remove from iterator? final Iterator<Entry<String, String>> it = MgnlContext.getWebContext().getParameters().entrySet() .iterator(); while (it.hasNext()) { final Map.Entry<String, String> pairs = it.next(); if (WHITELISTED_PARAMETERS.contains(pairs.getKey()) && StringUtils.isNotEmpty(pairs.getValue())) { filter.put(pairs.getKey(), pairs.getValue()); log.debug("Added to filter: " + pairs.getKey()); } it.remove(); // avoids a ConcurrentModificationException } } public TemplatingFunctions getTemplatingFunctions() { return templatingFunctions; } /** * Get all available nodes of type mgnl:blog. * * @param path Start node path in hierarchy * @param maxResultSize Number of items to return. When empty <code>Integer.MAX_VALUE</code> will be used. * @return List of blog nodes sorted by date created in descending order * @throws RepositoryException */ public List<ContentMap> getBlogs(String path, String maxResultSize) throws RepositoryException { int resultSize = Integer.MAX_VALUE; if (StringUtils.isNumeric(maxResultSize)) { resultSize = Integer.parseInt(maxResultSize); } String customFilters = getAuthorPredicate() + getTagPredicate(filter) + getCategoryPredicate(filter) + getDateCreatedPredicate(); final String sqlBlogItems = buildQuery(path, true, customFilters, BLOG_NODE_TYPE); return templatingFunctions.asContentMapList( getWrappedNodesFromQuery(sqlBlogItems, resultSize, getPageNumber(), BlogsNodeTypes.Blog.NAME)); } /** * Get latest nodes of type mgnl:blog. * * @param path Start node path in hierarchy * @param maxResultSize Number of items to return. When empty <code>5</code> will be used. * @return List of blog nodes sorted by date created in descending order * @throws RepositoryException */ public List<ContentMap> getLatestBlogs(String path, String maxResultSize) throws RepositoryException { return getLatest(path, maxResultSize, BLOG_NODE_TYPE, getPageNumber(), BlogsNodeTypes.Blog.NAME); } public List<ContentMap> getLatest(String path, String maxResultSize, String nodeType, int pageNumber, String nodeTypeName) throws RepositoryException { int resultSize = DEFAULT_LATEST_COUNT; if (StringUtils.isNumeric(maxResultSize)) { resultSize = Integer.parseInt(maxResultSize); } final String sqlBlogItems = buildQuery(path, nodeType); return templatingFunctions .asContentMapList(getWrappedNodesFromQuery(sqlBlogItems, resultSize, pageNumber, nodeTypeName)); } /** * @param path * @param maxResultSize the result size that is returned * @param categoryUuid the category uuid to take only the blogs from this category * @throws RepositoryException * @returns a list of blog nodes sorted by date created in descending order for the specified maxResultSize parameter */ public List<ContentMap> getLatestBlogs(String path, String maxResultSize, String categoryUuid) throws RepositoryException { int resultSize = DEFAULT_LATEST_COUNT; if (StringUtils.isNumeric(maxResultSize)) { resultSize = Integer.parseInt(maxResultSize); } StringBuilder queryString = formQueryString(new StringBuilder(), categoryUuid); return templatingFunctions.asContentMapList(getWrappedNodesFromQuery( "SELECT p.* from [mgnl:blog] AS p WHERE ISDESCENDANTNODE(p,'/') AND CONTAINS(p.categories, '" + categoryUuid + "') " + queryString + " ORDER BY p.[mgnl:created] desc", resultSize, 1, BlogsNodeTypes.Blog.NAME)); } /** * Forms a query string like this "OR CONTAINS(p.categories, '"uuid"')" and appends it to each other. * * @param query a new StringBuilder to keep the content on recursive calls * @param categoryUuid the uuid of the category * @return a query string used to filter the blogs by categories * @throws RepositoryException */ private StringBuilder formQueryString(StringBuilder query, String categoryUuid) throws RepositoryException { List<ContentMap> childCategories; if (categoryUuid.equalsIgnoreCase("cafebabe-cafe-babe-cafe-babecafebabe")) { childCategories = templatingFunctions.children(templatingFunctions.contentByPath("/", "categories")); } else { childCategories = templatingFunctions .children(templatingFunctions.contentById(categoryUuid, "categories")); } for (ContentMap childCategory : childCategories) { if (!templatingFunctions.children(childCategory).isEmpty()) { formQueryString(query, childCategory.getJCRNode().getIdentifier()); } query.append("OR CONTAINS(p.categories, '" + childCategory.getJCRNode().getIdentifier() + "') "); } return query; } /** * Get total number of blog posts for current state. * (Performs additional JCR-SQL2 query to obtain count!) * * @param path Start node path in hierarchy * @param useFilters <code>true</code> to use filters * @return long Number of blog posts */ public int getBlogCount(String path, boolean useFilters) throws RepositoryException { String customFilters = getAuthorPredicate() + getTagPredicate(filter) + getCategoryPredicate(filter) + getDateCreatedPredicate(); final String sqlBlogItems = buildQuery(path, useFilters, customFilters, BLOG_NODE_TYPE); return IteratorUtils.toList(QueryUtil.search(BlogWorkspaceUtil.COLLABORATION, sqlBlogItems, Query.JCR_SQL2, BlogsNodeTypes.Blog.NAME)).size(); } public int getRelatedBlogCount(String filterProperty, String filterIdentifier) throws RepositoryException { final String sqlBlogItems = "SELECT p.* from [mgnl:blog] AS p WHERE ISDESCENDANTNODE(p,'/') AND contains(p." + filterProperty + ", '" + filterIdentifier + "')"; return IteratorUtils.toList(QueryUtil.search(BlogWorkspaceUtil.COLLABORATION, sqlBlogItems, Query.JCR_SQL2, BlogsNodeTypes.Blog.NAME)).size(); } /** * Determine if older blog posts exists * * @param path * @param maxResultSize * @return Boolean true when older blog posts exists */ public boolean hasOlderPosts(String path, int maxResultSize) throws RepositoryException { final long totalBlogs = getBlogCount(path, true); final int pageNumber = getPageNumber(); return hasOlderPosts(path, maxResultSize, totalBlogs, pageNumber); } /** * Determine if older blog posts exists * * @param path * @param maxResultSize * @return Boolean true when older blog posts exists */ public boolean hasOlderPosts(String path, int maxResultSize, long totalBlogs, int pageNumber) throws RepositoryException { final int maxPage = (int) Math.ceil((double) totalBlogs / (double) maxResultSize); return maxPage >= pageNumber + 1; } /** * Determine the next following page number containing older blog posts * * @param path * @param maxResultSize * @return page number with older blog posts */ public int pageOlderPosts(String path, int maxResultSize) throws RepositoryException { return (hasOlderPosts(path, maxResultSize)) ? getPageNumber() + 1 : getPageNumber(); } /** * Determine if newer blog posts exists * * @return Boolean true when newer blog posts exists */ public Boolean hasNewerPosts() { return getPageNumber() > 1; } /** * Determine the previous following page number containing newer blog posts * * @return page number with newer blog posts */ public int pageNewerPosts() { return (hasNewerPosts()) ? getPageNumber() - 1 : getPageNumber(); } /** * Get tags for given blog node * * @param blog * @return List of tag nodes */ public List<ContentMap> getBlogTags(ContentMap blog) { return getItems(blog.getJCRNode(), BlogsNodeTypes.Blog.PROPERTY_TAGS, BlogWorkspaceUtil.COLLABORATION); } /** * Get tag cloud items having a score based on total blogs and referenced tags. * * @return Collection of tags with relative score */ public List<CloudMap> getTagCloud() { try { final Iterable<Node> nodes = NodeUtil.asIterable(QueryUtil.search(BlogWorkspaceUtil.COLLABORATION, "SELECT p.* from [mgnl:tag] AS p WHERE ISDESCENDANTNODE(p,'/')")); return getCloudData(nodes, BlogsNodeTypes.Blog.PROPERTY_TAGS, false); } catch (RepositoryException e) { log.error("Exception while getting tag cloud", e.getMessage()); return Collections.emptyList(); } } /** * Get categories for given blog node * * @param blog * @return List of category nodes */ public List<ContentMap> getBlogCategories(ContentMap blog) { return getItems(blog.getJCRNode(), BlogsNodeTypes.Blog.PROPERTY_CATEGORIES, BlogWorkspaceUtil.COLLABORATION); } /** * Get category cloud items having a score based on total blogs and referenced categories. * * @return Collection of categories with relative score */ public List<CloudMap> getCategoryCloud() { try { final Iterable<Node> nodes = NodeUtil.asIterable(QueryUtil.search(BlogWorkspaceUtil.COLLABORATION, "SELECT p.* from [mgnl:category] AS p WHERE ISDESCENDANTNODE(p,'/')")); return getCloudData(nodes, BlogsNodeTypes.Blog.PROPERTY_CATEGORIES, false); } catch (RepositoryException e) { log.error("Exception while getting category cloud", e.getMessage()); return Collections.emptyList(); } } /** * Get category cloud items having a score based on total blogs and referenced categories. * * @return Collection of categories with relative score */ public List<CloudMap> getAuthorCloud() { try { final Iterable<Node> nodes = NodeUtil.asIterable(QueryUtil.search(BlogWorkspaceUtil.CONTACTS, "SELECT p.* from [mgnl:contact] AS p WHERE ISDESCENDANTNODE(p,'/')")); return getCloudData(nodes, BlogsNodeTypes.Blog.PROPERTY_AUTHOR, true); } catch (RepositoryException e) { log.error("Exception while getting author cloud", e.getMessage()); return Collections.emptyList(); } } /** * Get distinct year and month list for all available blogs * * @return List<Map<String, Object>> containing properties <i>year</i> and <i>month</i> */ public List<Map<String, Object>> getArchivedDates() { final Set<Map<String, Object>> set = new HashSet<Map<String, Object>>(); for (Node blog : getAllBlogs()) { try { final Calendar dateCreated = blog.getProperty("mgnl:created").getDate(); final int year = dateCreated.get(Calendar.YEAR); final int month = dateCreated.get(Calendar.MONTH) + 1; final Map<String, Object> map = new HashMap<String, Object>(); map.put("year", Integer.toString(year)); map.put("month", String.format("%02d", month)); set.add(map); } catch (RepositoryException e) { log.debug("Exception getting created date", e.getMessage()); } } return new ArrayList<Map<String, Object>>(set); } private List<CloudMap> getCloudData(Iterable<Node> nodeList, String propName, boolean excludeNonExisting) throws RepositoryException { final List<CloudMap> cloudData = new ArrayList<CloudMap>(0); final int maxCloudUsage = getBlogCount("/", false); for (Node entry : nodeList) { try { final int count = getRelatedBlogCount(propName, entry.getIdentifier()); if (count == 0 && excludeNonExisting) { continue; } final int scale = getScale(count, maxCloudUsage); if (log.isDebugEnabled()) { log.debug(count + " blogs out of " + maxCloudUsage + " leaving a score of " + scale); } cloudData.add(new CloudMap(entry, count, scale)); } catch (RepositoryException e) { log.warn("Exception getting related blog count", e.getMessage()); } } return cloudData; } private static int getScale(int count, int max) { int scale = 0; if (max > 0) { scale = (count * 10) / max; } if (scale < 0) { scale = 0; } if (scale > 9) { scale = 9; } return scale; } /** * Get all available blogs starting from root node * * @return List<Node> blogs */ public List<Node> getAllBlogs() { final String sqlBlogItems = buildQuery("/", BLOG_NODE_TYPE); try { final NodeIterator items = QueryUtil.search(BlogWorkspaceUtil.COLLABORATION, sqlBlogItems, Query.JCR_SQL2, BlogsNodeTypes.Blog.NAME); return NodeUtil.asList(NodeUtil.asIterable(items)); } catch (RepositoryException e) { log.error("Exception getting all blogs", e.getMessage()); return Collections.emptyList(); } } private int getPageNumber() { int pageNumber = 1; if (filter.containsKey(PARAM_PAGE)) { pageNumber = Integer.parseInt(filter.get(PARAM_PAGE)); } return pageNumber; } protected String getAuthorPredicate() { if (filter.containsKey(PARAM_AUTHOR)) { final ContentMap contentMap = templatingFunctions.contentByPath(filter.get(PARAM_AUTHOR), BlogWorkspaceUtil.CONTACTS); if (contentMap != null) { final String authorId = (String) contentMap.get("@id"); if (StringUtils.isNotEmpty(authorId)) { return "AND p.author = '" + authorId + "' "; } } else { log.debug("Author [{}] not found", filter.get(PARAM_AUTHOR)); } } return StringUtils.EMPTY; } protected String getDateCreatedPredicate() { if (filter.containsKey(PARAM_YEAR)) { final int year = Integer.parseInt(filter.get(PARAM_YEAR)); final Calendar start = Calendar.getInstance(); start.set(year, Calendar.JANUARY, 1, 0, 0, 0); start.set(Calendar.MILLISECOND, 0); final Calendar end = Calendar.getInstance(); end.set(year, Calendar.DECEMBER, 1, 23, 59, 59); end.set(Calendar.MILLISECOND, 999); if (filter.containsKey(PARAM_MONTH)) { final int month = Integer.parseInt(filter.get(PARAM_MONTH)) - 1; start.set(Calendar.MONTH, month); end.set(Calendar.MONTH, month); } // Determine last day of the end month end.set(Calendar.DAY_OF_MONTH, end.getActualMaximum(Calendar.DAY_OF_MONTH)); final DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); // Format start date and end data for use in jcr sql predicate return "AND p.[mgnl:created] >= CAST('" + dateFormat.format(start.getTime()) + "' AS DATE) " + "AND p.[mgnl:created] <= CAST('" + dateFormat.format(end.getTime()) + "' AS DATE) "; } return StringUtils.EMPTY; } public static String getMonthName(String month) { int monthNr = Integer.parseInt(month); Locale locale = new Locale(DEFAULT_LANGUAGE); DateFormatSymbols symbols = new DateFormatSymbols(locale); String[] monthNames = symbols.getMonths(); return monthNames[monthNr - 1]; } protected String getTagPredicate(Map<String, String> filter) { if (filter.containsKey(PARAM_TAG)) { final ContentMap contentMap = templatingFunctions.contentByPath(filter.get(PARAM_TAG), BlogWorkspaceUtil.COLLABORATION); if (contentMap != null) { final String tagId = (String) contentMap.get("@id"); if (StringUtils.isNotEmpty(tagId)) { return "AND p.tags like '%" + tagId + "%' "; } } else { log.debug("Tag [{}] not found", filter.get(PARAM_TAG)); } } return StringUtils.EMPTY; } protected String getCategoryPredicate(Map<String, String> filter) { if (filter.containsKey(PARAM_CATEGORY)) { final ContentMap contentMap = templatingFunctions.contentByPath(filter.get(PARAM_CATEGORY), BlogWorkspaceUtil.CATEGORIES); if (contentMap != null) { final String categoryId = (String) contentMap.get("@id"); if (StringUtils.isNotEmpty(categoryId)) { return "AND p.categories like '%" + categoryId + "%' "; } } else { log.debug("Category [{}] not found", filter.get(PARAM_CATEGORY)); } } return StringUtils.EMPTY; } /** * Query blog items using JCR SQL2 syntax. * * @param query Query string * @param maxResultSize Max results returned * @param pageNumber paging number * @return List<Node> List of blog nodes * @throws javax.jcr.RepositoryException */ public static List<Node> getWrappedNodesFromQuery(String query, int maxResultSize, int pageNumber, String nodeTypeName) throws RepositoryException { return getWrappedNodesFromQuery(query, maxResultSize, pageNumber, nodeTypeName, BlogWorkspaceUtil.COLLABORATION); } /** * Query items using JCR SQL2 syntax. * * @param query Query string * @param maxResultSize Max results returned * @param pageNumber paging number * @return List<Node> List of nodes * @throws javax.jcr.RepositoryException */ public static List<Node> getWrappedNodesFromQuery(String query, int maxResultSize, int pageNumber, String nodeTypeName, String workspace) throws RepositoryException { final List<Node> itemsListPaged = new ArrayList<Node>(0); final NodeIterator items = QueryUtil.search(workspace, query, Query.JCR_SQL2, nodeTypeName); // Paging result set final int startRow = (maxResultSize * (pageNumber - 1)); if (startRow > 0) { try { items.skip(startRow); } catch (NoSuchElementException e) { //log.error("No more blog items found beyond this item number: " + startRow); } } int count = 1; while (items.hasNext() && count <= maxResultSize) { itemsListPaged.add(new I18nNodeWrapper(items.nextNode())); count++; } return itemsListPaged; } public static List<Node> getWrappedNodesFromQuery(String query, String nodeTypeName, String workspace) throws RepositoryException { final List<Node> itemsListPaged = new ArrayList<Node>(0); final NodeIterator items = QueryUtil.search(workspace, query, Query.JCR_SQL2, nodeTypeName); while (items.hasNext()) { itemsListPaged.add(new I18nNodeWrapper(items.nextNode())); } return itemsListPaged; } public String buildQuery(String path, boolean useFilters, String customFilters, String contentType) { String filters = StringUtils.EMPTY; if (useFilters) { filters = customFilters; } return "SELECT p.* FROM [" + contentType + "] AS p " + "WHERE ISDESCENDANTNODE(p, '" + StringUtils.defaultIfEmpty(path, "/") + "') " + filters + "ORDER BY p.[mgnl:created] desc"; } public String buildQuery(String path, String contentType) { return "SELECT p.* FROM [" + contentType + "] AS p " + "WHERE ISDESCENDANTNODE(p, '" + StringUtils.defaultIfEmpty(path, "/") + "') " + "ORDER BY p.[mgnl:created] desc"; } public List<ContentMap> getItems(Node item, String nodeType, String workspace) { final List<ContentMap> items = new ArrayList<ContentMap>(0); try { final Value[] values = item.getProperty(nodeType).getValues(); if (values != null) { for (Value value : values) { items.add(templatingFunctions.contentById(value.getString(), workspace)); } } } catch (RepositoryException e) { log.error("Exception while getting items", e.getMessage()); } return items; } }