View Javadoc
1   /**
2    * This file Copyright (c) 2012-2017 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.module.googlesitemap.service;
35  
36  import info.magnolia.cms.i18n.I18nContentSupport;
37  import info.magnolia.context.MgnlContext;
38  import info.magnolia.jcr.util.NodeTypes;
39  import info.magnolia.jcr.util.NodeUtil;
40  import info.magnolia.link.LinkUtil;
41  import info.magnolia.module.googlesitemap.GoogleSiteMapConfiguration;
42  import info.magnolia.module.googlesitemap.SiteMapNodeTypes;
43  import info.magnolia.module.googlesitemap.bean.SiteMapEntry;
44  import info.magnolia.module.googlesitemap.service.query.QueryUtil;
45  import info.magnolia.module.site.Site;
46  import info.magnolia.module.site.SiteManager;
47  import info.magnolia.objectfactory.Components;
48  import info.magnolia.repository.RepositoryConstants;
49  
50  import java.util.ArrayList;
51  import java.util.HashSet;
52  import java.util.Iterator;
53  import java.util.List;
54  import java.util.Locale;
55  import java.util.Set;
56  
57  import javax.inject.Inject;
58  import javax.inject.Singleton;
59  import javax.jcr.Node;
60  import javax.jcr.NodeIterator;
61  import javax.jcr.RepositoryException;
62  import javax.jcr.Session;
63  
64  import org.apache.jackrabbit.commons.iterator.FilteringNodeIterator;
65  import org.apache.jackrabbit.commons.predicate.NodeTypePredicate;
66  import org.slf4j.Logger;
67  import org.slf4j.LoggerFactory;
68  
69  /**
70   * Main SiteMapService.
71   * This service is responsible for:
72   * Searching the appropriate nodes (pages or VirtualUri) that should be displayed (for XML rendering or Editing).
73   * From the search nodes, create SiteMapEntrys (POJO containing preformated infos used for the rendering).
74   */
75  @Singleton
76  public class SiteMapService {
77  
78      private static final Logger log = LoggerFactory.getLogger(SiteMapService.class);
79  
80      /**
81       * Injected service.
82       */
83      private SiteManager siteManager;
84      private I18nContentSupport i18nSupport;
85      private GoogleSiteMapConfiguration configuration;
86      private QueryUtil queryUtil;
87  
88      /**
89       * Constructor for injection.
90       *
91       * @param siteManager: Injected.
92       */
93      @Inject
94      public SiteMapService(SiteManager siteManager, GoogleSiteMapConfiguration configuration, QueryUtil queryUtil) {
95          this.siteManager = siteManager;
96          this.configuration = configuration;
97          this.queryUtil = queryUtil;
98          i18nSupport = Components.getComponent(I18nContentSupport.class);
99      }
100 
101     /**
102      * Create the SiteMapEntry List corresponding to <br>
103      * - uriMapping if isForVirtualUri = true or<br>
104      * - for all child's of the rootNode that are of type MgnlNodeType.NT_CONTENT otherwise.
105      */
106     public List<SiteMapEntry> getSiteMapBeans(Node siteMapNode, boolean isForVirtualUri, boolean isForEdit) throws RepositoryException {
107         // Init
108         List<SiteMapEntry> res = new ArrayList<SiteMapEntry>();
109         final String changefreq = SiteMapNodeTypes.SiteMap.getDefaultChangeFreq(siteMapNode) != null ? SiteMapNodeTypes.SiteMap.getDefaultChangeFreq(siteMapNode) : configuration.getChangeFrequency();
110         final Double priority = SiteMapNodeTypes.SiteMap.getDefaultPriority(siteMapNode) != null ? SiteMapNodeTypes.SiteMap.getDefaultPriority(siteMapNode) : configuration.getPriority();
111         Session webSiteSession = MgnlContext.getJCRSession(RepositoryConstants.WEBSITE);
112 
113         NodeIterator nodeIterator = null;
114         if (isForVirtualUri) {
115             log.debug("Requested virtualUri info's for EDIT='{}", isForEdit);
116             // Create a virtualUri info beans
117             nodeIterator = searchVirtualUriNodes();
118             feedVirtualUriMapBeans(res, nodeIterator, isForEdit, changefreq, priority);
119         } else {
120             log.debug("Requested siteMap info's for EDIT='{}' based on the following root {} ", isForEdit, siteMapNode.getPath());
121             // Create a site info beans
122 
123             List<String> pages = SiteMapNodeTypes.SiteMap.getPages(siteMapNode);
124             for (String id : pages) {
125 
126                 if (!webSiteSession.nodeExists(id)) {
127                     log.debug("Page '{}' is not visible. This page will not be added to the result list", id);
128                     continue;
129                 }
130                 Node page = webSiteSession.getNode(id);
131                 nodeIterator = searchSiteChildNodes(page);
132                 feedSiteMapBeans(res, nodeIterator, isForEdit, null, changefreq, priority, page.getDepth());
133             }
134         }
135 
136         return res;
137     }
138 
139     /**
140      * Search the nodes related to the UriMappings.
141      */
142     private NodeIterator searchVirtualUriNodes() {
143         NodeIterator nodes = null;
144 
145         String xpath = "//virtualURIMapping//element(*," + NodeTypes.ContentNode.NAME + ")";
146         nodes = queryUtil.query(RepositoryConstants.CONFIG, xpath, "xpath", NodeTypes.ContentNode.NAME);
147 
148         return nodes;
149     }
150 
151     /**
152      * Extract informations form the Node and create a Bean used in the display
153      * for Edit and XML rendering.
154      */
155     private void feedVirtualUriMapBeans(List<SiteMapEntry> siteMapBeans, NodeIterator nodes, boolean isForEdit, String changefreq, Double priority) throws RepositoryException {
156 
157         while (nodes.hasNext()) {
158             // Init
159             Node child = nodes.nextNode();
160             if (hasToBePopulated(child, true, isForEdit, null)) {
161                 // Populate the Bean used by the display
162                 SiteMapEntry siteMapBean = populateBean(child, true, isForEdit, null, null, changefreq, priority, 0);
163                 // Add the bean to the result
164                 siteMapBeans.add(siteMapBean);
165             }
166         }
167     }
168 
169     /**
170      * Search children of the root Node of type MgnlNodeType.NT_CONTENT and all inherited node types.
171      *
172      * @throws RepositoryException
173      */
174     private NodeIterator searchSiteChildNodes(Node root) throws RepositoryException {
175         NodeIterator nodes = null;
176 
177         NodeTypePredicate predicate = new NodeTypePredicate(NodeTypes.Content.NAME, true);
178         nodes = new FilteringNodeIterator(root.getNodes(), predicate);
179 
180         return nodes;
181     }
182 
183     /**
184      * Create a new SiteMapEntry POJO for all node from the NodeIterator if required.
185      */
186     private void feedSiteMapBeans(List<SiteMapEntry> siteMapBeans, NodeIterator nodes, boolean isForEdit, Boolean inheritedParentDisplayColor, String changefreq, Double priority, int rootLevel) throws RepositoryException {
187 
188         while (nodes.hasNext()) {
189             // Init
190             Node page = nodes.nextNode();
191             Site site = siteManager.getAssignedSite(page);
192             if (!hasToBePopulated(page, false, isForEdit, site)) {
193                 log.debug("Page '' is not part of the site map.", NodeUtil.getNodePathIfPossible(page));
194                 continue;
195             }
196 
197             // Populate the Bean
198             SiteMapEntry siteMapBean = populateBean(page, false, isForEdit, site, inheritedParentDisplayColor, changefreq, priority, rootLevel);
199             NodeIterator childNodes = searchSiteChildNodes(page);
200             boolean hasChild = (childNodes != null && childNodes.hasNext());
201 
202             if (isForEdit) {
203                 // Handle Edit Mode. Always all the bean.
204                 siteMapBeans.add(siteMapBean);
205 
206                 if (hasChild) {
207                     // Handle the children if any
208                     Boolean hideNodeChildInGoogleSitemapTmp = null;
209                     // Handle the color of the child to be display.
210                     if (inheritedParentDisplayColor != null) {
211                         hideNodeChildInGoogleSitemapTmp = inheritedParentDisplayColor;
212                     } else if (SiteMapNodeTypes.GoogleSiteMap.isHideChildren(page)) {
213                         hideNodeChildInGoogleSitemapTmp = Boolean.TRUE;
214                     }
215 
216                     feedSiteMapBeans(siteMapBeans, childNodes, isForEdit, hideNodeChildInGoogleSitemapTmp, changefreq, priority, rootLevel);
217                 }
218 
219             } else {
220                 // Handle XML Display Mode
221                 // If node has to be display.
222                 if (!SiteMapNodeTypes.GoogleSiteMap.isHide(page)) {
223                     // Check Multilang.
224                     if (i18nSupport.isEnabled() && site.getI18n().isEnabled()) {
225                         Locale currentLocale = i18nSupport.getLocale();
226                         for (Locale locale : site.getI18n().getLocales()) {
227                             i18nSupport.setLocale(locale);
228                             SiteMapEntry siteMapBeanLocale = populateBean(page, false, isForEdit, site, null, changefreq, priority, rootLevel);
229                             if (siteMapBeanLocale != null) {
230                                 siteMapBeans.add(siteMapBeanLocale);
231                             }
232                         }
233                         i18nSupport.setLocale(currentLocale);
234                     } else {
235                         siteMapBeans.add(siteMapBean);
236                     }
237                 }
238                 // Check if Child Node has to be included
239                 if (hasChild && !SiteMapNodeTypes.GoogleSiteMap.isHideChildren(page)) {
240                     // Call recursively
241                     feedSiteMapBeans(siteMapBeans, childNodes, isForEdit, null, changefreq, priority, rootLevel);
242                 }
243             }
244         }
245     }
246 
247     /**
248      * Populate the Bean.
249      */
250     private SiteMapEntry populateBean(Node child, boolean isForUriMapping, boolean isForEdit, Site site, Boolean inheritedParentDisplayColor, String changefreq, Double priority, int rootLevel) throws RepositoryException {
251         SiteMapEntry res = null;
252         // Populate only if true
253         if (hasToBePopulated(child, isForUriMapping, isForEdit, site)) {
254 
255             String loc = null;
256 
257             // Get loc
258             if (isForUriMapping) {
259                 loc = child.hasProperty("fromURI") ? MgnlContext.getContextPath() + child.getProperty("fromURI").getString() : "";
260             } else {
261                 loc = LinkUtil.createExternalLink(child);
262             }
263 
264             // Populate the bean:
265             res = new SiteMapEntry(configuration, loc, child, rootLevel, changefreq, priority);
266             log.debug("Populate Basic info for Node: " + child.getPath() + " with values " + res.toStringDisplay());
267 
268             if (isForEdit) {
269                 res = populateBeanForEdit(child, isForUriMapping, res, inheritedParentDisplayColor);
270             }
271         }
272 
273         return res;
274     }
275 
276     /**
277      * Add additional info's for Edit.
278      */
279     private SiteMapEntry populateBeanForEdit(Node child, boolean isForUriMapping, SiteMapEntry siteMapBean, Boolean inheritedParentDisplayColor) throws RepositoryException {
280 
281         if (isForUriMapping) {
282             // For VirtualUri
283             siteMapBean.setFrom(child.hasProperty("fromURI") ? child.getProperty("fromURI").getString() : "");
284             siteMapBean.setTo(child.hasProperty("toURI") ? child.getProperty("toURI").getString() : "");
285             siteMapBean.setStyleAlert(SiteMapNodeTypes.GoogleSiteMap.isHide(child));
286             log.debug("Populate Edit VirtualUri info for Node: " + child.getPath() + " with values " + siteMapBean.toStringVirtualUri());
287         } else {
288             // For Site
289             siteMapBean.setPageName(child.hasProperty("title") ? child.getProperty("title").getString() : "");
290             siteMapBean.setStyleAlert(inheritedParentDisplayColor != null ? inheritedParentDisplayColor.booleanValue() : SiteMapNodeTypes.GoogleSiteMap.isHide(child));
291             log.debug("Populate Edit Site info for Node: " + child.getPath() + " with values " + siteMapBean.toStringSite());
292         }
293         return siteMapBean;
294     }
295 
296     /**
297      * Check if the node has to be populated.<br>
298      * True for UriMapping if:<br>
299      * !hideInGoogleSitemap && node has a modification date<br>
300      * True for Site if:<br>
301      * !hideInGoogleSitemap && !hideInGoogleSitemapChildren && node has a creation date && site.isEnabled()<br>
302      */
303     private boolean hasToBePopulated(Node child, boolean isForUriMapping, boolean isForEdit, Site site) throws RepositoryException {
304         boolean hasCreationDate = NodeTypes.Created.getCreated(child) != null;
305         boolean hasModificationDate = NodeTypes.LastModified.getLastModified(child) != null;
306 
307         // For edit and if has creation date --> true.
308         if (isForEdit) {
309             return hasCreationDate;
310         }
311 
312         // Node has a property set to hide node to be display --> false.
313         if (SiteMapNodeTypes.GoogleSiteMap.isHide(child) && (isForUriMapping || SiteMapNodeTypes.GoogleSiteMap.isHideChildren(child))) {
314             return false;
315         }
316         // For uriMapping.
317         if (isForUriMapping && hasModificationDate) {
318             return true;
319         }
320         // For site.
321         if (!isForUriMapping && site.isEnabled() && hasCreationDate) {
322             return true;
323         }
324         return false;
325     }
326 
327     /**
328      * Get Site and virtualUri informations if they are defined as components.
329      * Notice that by using a HashSet, a single object will be keeped in case of equality of the location.
330      */
331     public Iterator<SiteMapEntry> getSiteMapBeans(Node siteMapNode) throws RepositoryException {
332         Set<SiteMapEntry> res = new HashSet<SiteMapEntry>();
333         // Return if no content set.
334         if (!NodeUtil.isNodeType(siteMapNode, SiteMapNodeTypes.SiteMap.NAME)) {
335             return res.iterator();
336         }
337         // Display Site informations part... if defined
338         res.addAll(getSiteMapBeans(siteMapNode, false, false));
339         if (SiteMapNodeTypes.SiteMap.isVirtualUriMappingIncluded(siteMapNode)) {
340             res.addAll(getSiteMapBeans(siteMapNode, true, false));
341         }
342         return res.iterator();
343     }
344 
345     /**
346      * Based on a {@link SiteMapEntry} bean, update the related page property. <br>
347      * Update the page last modification date in order to visualize that the changes has to be published <br>
348      * in order to be visible on the public instance.<br>
349      */
350     public Node updatePageNode(SiteMapEntry entry) throws RepositoryException {
351         Node pageNode = MgnlContext.getJCRSession(RepositoryConstants.WEBSITE).getNode(entry.getPath());
352         return updateNode(entry, pageNode);
353     }
354 
355     public Node updateVirtualUriNode(SiteMapEntry entry) throws RepositoryException {
356         Node virtualUriNode = MgnlContext.getJCRSession(RepositoryConstants.CONFIG).getNode(entry.getPath());
357         return updateNode(entry, virtualUriNode);
358     }
359 
360     private Node updateNode(SiteMapEntry entry, Node node) throws RepositoryException {
361         SiteMapNodeTypes.GoogleSiteMap.update(node, entry.getChangefreq(), entry.getPriority(), entry.isHide(), entry.isHideChildren());
362         NodeTypes.LastModified.update(node);
363         return node;
364     }
365 }