View Javadoc
1   /*
2    * Copyright 2016 Function1. All Rights Reserved.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *    http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package tools.gsf.navigation.siteplan;
17  
18  import org.slf4j.Logger;
19  import org.slf4j.LoggerFactory;
20  
21  import COM.FutureTense.Interfaces.ICS;
22  import COM.FutureTense.Interfaces.Utilities;
23  
24  import com.fatwire.assetapi.data.AssetId;
25  import com.fatwire.cs.core.db.PreparedStmt;
26  import com.fatwire.cs.core.db.StatementParam;
27  
28  import tools.gsf.facade.assetapi.AssetIdUtils;
29  import tools.gsf.facade.assetapi.asset.TemplateAssetAccess;
30  import tools.gsf.facade.runtag.render.LogDep;
31  import tools.gsf.facade.sql.IListIterable;
32  import tools.gsf.facade.sql.Row;
33  import tools.gsf.facade.sql.SqlHelper;
34  import tools.gsf.navigation.AssetNode;
35  import tools.gsf.navigation.ConfigurableNode;
36  import tools.gsf.navigation.NavService;
37  
38  import java.util.*;
39  import java.util.stream.Collectors;
40  import java.util.stream.Stream;
41  
42  /**
43   * Simple navigation service implementation that loads objects from the Site Plan.
44   * 
45   * Reads the full site plan tree in one query. It does not filter assets by site.
46   * 
47   * Nodes are instantiated via a dedicated method where you can load any data that
48   * is required; you get to (define and) use your own AssetNode implementation.
49   * 
50   * This NavService implementation REQUIRES that the SitePlan models your navigational
51   * structures after the following design:
52   * 
53   * SitePlan root node (SiteNavigation asset)
54   *        |
55   *        ----------- Nav Structure "A" Placeholder (Page asset)
56   *        |                     |
57   *        |                     |----------- Webpage A.1 (Page asset)
58   *        |                     |                  |
59   *        |                     |                  |----------- Webpage A.1.1 (Page asset)
60   *        |                     |                                    |----------- (...) (Page assets)
61   *        |                     |
62   *        |                     |----------- Webpage A.2 (Page asset)
63   *        |                     |                  |
64   *        |                     |                  |----------- (...) (Page assets)
65   *        |                     |
66   *        |                     |----------- Webpage A.3 (Page asset)
67   *        |                     |                  |
68   *        |                     |                  |----------- (...) (Page assets)
69   *        |
70   *        ----------- Nav Structure "B" Placeholder (Page asset)
71   *        |                     |
72   *        |                     |----------- Webpage B.1 (Page asset)
73   *        |                     |                  |
74   *        |                     |                  |----------- (...) (Page assets)
75   *        |                     |                  
76   *        |                     |----------- Webpage B.2 (Page asset)
77   *        |                     |                  |
78   *        |                     |                  |----------- (...) (Page assets)
79   *        |                     |                                    
80   *        |                     |----------- Webpage A.3 (Page asset)
81   *        |                                        |
82   *        |                                        |----------- (...) (Page assets)
83   *        |
84   *        ----------- (...) (Other nav structures)
85   *
86   * ... where:
87   * 
88   * - SiteNavigation nodes will always be excluded from a Webpage node's definitive breadcrumb.
89   * - SiteNavigation nodes are not part of any nav structure.
90   * - Nav Structure Placeholder nodes will always be excluded from a Webpage node's definitive breadcrumb.
91   * - Nav Structure Placeholder are not part of a nav structure, they are just the entry point to it.
92   *
93   *
94   *
95   *
96   * 
97   * @author Tony Field
98   * @since 2016-07-06
99   */
100 public abstract class SitePlanNavService<N extends AssetNode<N> & ConfigurableNode<N>> implements NavService<N, AssetId, AssetId> {
101 	
102 	private static final Logger LOG = LoggerFactory.getLogger(SitePlanNavService.class);
103 
104     private final ICS ics;
105     private final TemplateAssetAccess dao;
106     private final String sitename;
107     private final Map<AssetId, List<N>> nodesById = new HashMap<>();
108 
109     // Sure, we could join with PUBLICATION on the NAVIGATION_TREE_DUMP, but
110     // this way we leverage cache further.
111     private final static PreparedStmt FIND_PUBID = new PreparedStmt(
112             "select id from Publication where name = ?",
113             Arrays.asList("Publication"));
114 
115     private final static PreparedStmt NAVIGATION_TREE_DUMP = new PreparedStmt(
116             "select spt.* from SITEPLANTREE spt, ASSETPUBLICATION ap " +
117             "where " +
118             "spt.ncode = 'Placed' and " +
119             "spt.otype = ap.assettype and " +
120             "spt.oid = ap.assetid and " +
121             "((ap.pubid = ?) OR (ap.pubid = 0)) " + // Pages cannot be shared across sites in 12c, but let's play it safe, just in case that changes
122             "order by spt.nparentid, spt.nrank",
123             Arrays.asList("page", "siteplantree", "assetpublication", "Page", "SitePlanTree", "AssetPublication", "PAGE", "SITEPLANTREE", "ASSETPUBLICATION"));
124     
125     static {
126     	FIND_PUBID.setElement(0, "Publication", "name");
127     	NAVIGATION_TREE_DUMP.setElement(0, "ASSETPUBLICATION", "pubid");
128     }
129     
130     private final Long _getPubId(String sitename) {
131         final StatementParam params = FIND_PUBID.newParam();
132         params.setString(0, sitename);
133 
134         LOG.debug("Executing query to find out pubid for site name {}", sitename);
135                 
136         IListIterable results = SqlHelper.select(this.ics, FIND_PUBID, params);
137         if (results.size() == 0) {
138         	LOG.warn("There is no Publication whose name is '{}'", sitename);
139         	return null;
140         } else if (results.size() > 1) {
141         	throw new IllegalStateException("There cannot be 2+ sites (publications) in WCS with the same name, yet there seem to be " + results.size() + " sites whose name is '" + sitename + "'");
142         } else {
143         	Row row = results.iterator().next();
144         	return row.getLong("id");
145         }
146     }
147     
148     protected abstract N createAssetNode(AssetId assetId);
149     
150     protected TemplateAssetAccess getTemplateAssetAccess() {
151     	return this.dao;
152     }
153     
154     protected String getSitename() {
155     	return this.sitename;
156     }
157     
158     protected final ICS getIcs() {
159     	return this.ics;
160     }
161     
162     public SitePlanNavService(ICS ics, TemplateAssetAccess dao) {
163     	this(ics, dao, null);
164     }
165     
166     public SitePlanNavService(ICS ics, TemplateAssetAccess dao, String theSitename) {
167         this.ics = ics;
168         this.dao = dao;
169 
170         if (!Utilities.goodString(theSitename)) {
171     		// Fallback to ICS variable "site", if such
172         	theSitename = ics.GetVar("site");
173         	if (!Utilities.goodString(theSitename)) {
174         		throw new IllegalStateException("Missing argument sitename. Nav structure cannot be built unless you specify the name of the site (publication) it is for.");
175         	}
176     	}
177 		Long pubId = _getPubId(theSitename);
178 		if (pubId == null) {
179 			throw new IllegalStateException("Cannot determine pubid for site '" + theSitename + "'. Nav structure cannot be built unless you specify the name of the site (publication) it is for.");
180 		}
181         
182 		this.sitename = theSitename;
183         StatementParam params = NAVIGATION_TREE_DUMP.newParam();
184         params.setLong(0, pubId);
185 
186         // read the site plan tree in one massive query
187         Map<Long, SitePlanTreeData> rowMap = new HashMap<>();
188         Map<Long, List<SitePlanTreeData>> childrenMap = new HashMap<>();
189 
190         LOG.debug("Executing SitePlan query for gathering data for nav service...");
191                 
192         for (Row row : SqlHelper.select(ics, NAVIGATION_TREE_DUMP, params)) {
193             SitePlanTreeData nodeInfo = new SitePlanTreeData(row);
194             LOG.debug("Processing SitePlan row: {}", nodeInfo);
195             rowMap.put(nodeInfo.nid, nodeInfo);
196             LOG.debug("Added row {} to SitePlan rows map under key {}", nodeInfo, nodeInfo.nid);
197             List<SitePlanTreeData> children = childrenMap.get(nodeInfo.nparentid);
198             if (children == null) {
199             	// Initialize the list of children for the current row's parent
200             	children = new ArrayList<SitePlanTreeData>();
201             	childrenMap.put(nodeInfo.nparentid, children);
202             }
203             // Add the current row to its parent's list of children  
204             children.add(nodeInfo);
205             LOG.debug("Added SPT row {} to the list of children of SPT (parent) row {}. That list now looks like this: {}", nodeInfo, nodeInfo.nparentid, children);
206         }
207 
208         // create Node objects
209         Map<Long, N> nidNodeMap = new HashMap<Long, N>();
210         for (long nid : rowMap.keySet()) {
211         	LOG.debug("Will invoke createAssetNode for asset id {}", rowMap.get(nid).assetId);
212         	N node = createAssetNode(rowMap.get(nid).assetId);
213         	LOG.debug("AssetNode created for asset {}: {}", rowMap.get(nid).assetId, node);
214             
215             // Log a dependency with every node (asset) we populate
216             LogDep.logDep(ics, node.getId());
217         	LOG.debug("Logged dependency for asset {} inside nav service...", rowMap.get(nid).assetId);
218             
219             nidNodeMap.put(nid, node);
220             LOG.debug("Added node {} to nodes map under key {}", node, nid);
221             
222             // Stash for later. Probably won't have many duplicates so optimize
223             AssetId assetId = node.getId();
224             List<N> a1 = nodesById.get(assetId);
225             if (a1 == null) {
226             	a1 = Stream.of(node).collect(Collectors.toList());
227                 nodesById.put(assetId, a1);
228             } else {
229             	a1.add(node);
230             }
231             
232             LOG.debug("nodesById map: {}", nodesById);
233 
234         }
235 
236         // hook up parent-child relationships, starting from the list
237         // of nodes with a parent
238         for (long nparentid : childrenMap.keySet()) {
239         	LOG.debug("Processing parent-child relationships for SitePlanTree row with nid = {}", nparentid);
240         	N parent = nidNodeMap.get(nparentid);
241         	if (parent != null) {
242         		LOG.debug("AssetNode for nid {} is: {}", nparentid, parent);
243         		List<SitePlanTreeData> children = childrenMap.get(nparentid);
244         		if (children != null) {
245         			LOG.debug("List of children for SPT whose nid = {} is: {}", nparentid, children);
246         			for (SitePlanTreeData childRow : children) {
247 	        			N child = nidNodeMap.get(childRow.nid);
248 	        			if (child != null) {
249 	        				parent.addChild(child);
250 	        				child.setParent(parent);
251 	        				LOG.debug("Bound together parent node {} and child node {} as per SPT entry {}", parent, child, childRow);  
252 	        			} else {
253 	        				LOG.warn("There could be a problem here... we registered SPT row {} as a child of parent row {} but we did not instantiate a node for that child?", childRow, nparentid);
254 	        			}
255 	        		}
256         		} else {
257         			LOG.warn("Not sure how we ended up with a a parent node in the children nodes map with no children whatsoever.");
258         		}
259         	} else {
260         		LOG.warn("We have a list of children for SPT entry whose nid = {}. However, there is not a node matching that SPT entry's asset ({}). This is a bit weird, but also legitimate (for instance, the query excludes SiteNavigation assets)", nparentid, rowMap.get(nparentid));
261         	}
262         }
263     }
264 
265     private static class SitePlanTreeData {
266         final long nid;
267         final long nparentid;
268         final int nrank;
269         final AssetId assetId;
270 
271         SitePlanTreeData(Row row) {
272             nid = row.getLong("nid");
273             nparentid = row.getLong("nparentid");
274             nrank = row.getInt("nrank");
275             assetId = AssetIdUtils.createAssetId(row.getString("otype"), row.getLong("oid"));
276         }
277 
278         @Override
279         public String toString() {
280             return "SitePlanTreeData{" +
281                     "nid=" + nid +
282                     ", nparentid=" + nparentid +
283                     ", nrank=" + nrank +
284                     ", assetId=" + assetId +
285                     '}';
286         }
287     }
288 
289     public List<N> getNav(AssetId sitePlan) {
290         if (sitePlan == null) {
291             throw new IllegalArgumentException("Null param not allowed");
292         }
293 
294         // find the requested structure
295         List<N> spNodes = nodesById.get(sitePlan);
296         if (spNodes == null) throw new IllegalArgumentException("Could not locate nav structure corresponding to "+sitePlan);
297         if (spNodes.size() > 1) throw new IllegalStateException("Cannot have more than one site plan node with the same id in the tree");
298         N requestedRoot = spNodes.get(0); // never null
299 
300         // return the loaded children of the structure root
301         return (List<N>) requestedRoot.getChildren();
302     }
303     
304     public List<N> getBreadcrumb(AssetId id) {
305 
306         if (id == null) {
307             throw new IllegalArgumentException("Cannot calculate breadcrumb of a null asset");
308         }
309 
310         Collection<BreadcrumbCandidate<N>> breadcrumbs = new ArrayList<>();
311         LOG.debug("Obtaining all nodes whose ID matches {}", id);
312         List<N> nodes = nodesById.get(id);
313         if (nodes != null) {
314         	// Build all possible breadcrumbs
315 	        for (N node : nodesById.get(id)) {
316 	            breadcrumbs.add(new BreadcrumbCandidate<N>(getBreadcrumbForNode(node)));
317 	        }
318 	        
319 	        // Choose the preferred breadcrumb. Users may override this method
320 	        // to implement their own choosing logic.
321 	        List<N> breadcrumb = chooseBreadcrumb(breadcrumbs);
322 	        LOG.debug("This is the preferred breadcrumb for ID {}: {}", id, breadcrumb);
323 	        	        
324 	        return breadcrumb;
325         } else {
326         	LOG.info("Didn't find any node in this nav structure whose ID matched {}, thus a breadcrumb cannot be calculated. These are all available nodes: {}", id, this.nodesById);
327         	LOG.debug("Node not found, getBreadcrumb will return null");
328         	return null;
329         }
330 
331         
332     }
333 
334     /**
335      * Get the breadcrumb corresponding to the specified node.
336      *
337      * Default implementation simply uses the specified node's parents.
338      *
339      * @param node the node whose breadcrumb needs to be calculated
340      * @return the breadcrumb
341      */
342     protected List<N> getBreadcrumbForNode(N node) {
343         List<N> ancestors = new ArrayList<>();
344         do {
345             ancestors.add(node);
346             node = node.getParent();
347         } while (node != null);
348         Collections.reverse(ancestors);
349         return ancestors;
350     }
351 
352     /**
353      * Determines a node's "preferred" breadcrumb from a list of "candidates".
354      *
355      * This default implementation simply returns the first one returned by the specified
356      * collection's iterator.
357      * 
358      * However, you may override this method in order to implement your own ad-hoc logic for
359      * determining the preferred breadcrumb.
360      * 
361      * The candidates passed into this method have already parsed the raw breadcrumb so to split it
362      * up in the due SiteNavigation node, Nav Structure Placeholder node and the clean breadcrumb path.
363      * 
364      * @param candidates The candidates you get to choose the preferred breadcrumb from.
365      * @return The preferred breadcrumb (list of nodes).
366      */
367     protected List<N> chooseBreadcrumb(Collection<BreadcrumbCandidate<N>> candidates) {
368         return candidates.iterator().next().getBreadcrumb();
369     }
370     
371     protected static class BreadcrumbCandidate<N extends AssetNode<N>> {
372     	private List<N> breadcrumb;
373     	private N siteNavigation;
374     	private N navStructurePlaceholder;
375     	
376     	public BreadcrumbCandidate(List<N> rawBreadcrumb) {
377     		if (rawBreadcrumb.size() < 2) {
378     			throw new IllegalArgumentException("Breadcrumb candidate is not valid, insufficient nodes.");
379     		}
380     		this.siteNavigation = rawBreadcrumb.get(0);
381     		if (!this.siteNavigation.getId().getType().equals("SiteNavigation")) {
382     			throw new IllegalArgumentException("Breadcrumb candidate is not valid, first node must be the SiteNavigation node, got this instead: " + this.siteNavigation);
383     		}
384     		this.navStructurePlaceholder = rawBreadcrumb.get(1);
385     		if (!this.navStructurePlaceholder.getId().getType().equals("Page")) {
386     			throw new IllegalArgumentException("Breadcrumb candidate is not valid, second node must be the Nav Structure Placeholder (Page) node, got this instead: " + this.navStructurePlaceholder);
387     		}
388     		this.breadcrumb = rawBreadcrumb.subList(2, rawBreadcrumb.size());
389     	}
390     	
391     	public N getNavStructurePlaceholder() {
392     		return this.navStructurePlaceholder;
393     	}
394     	
395     	public List<N> getBreadcrumb() {
396     		return this.breadcrumb;
397     	}
398     }
399     
400 }