001    /*******************************************************************************
002     * Copyright (c) 2005 Gabriel Handford.
003     * All rights reserved.
004     *
005     * Contributors - Gabriel Handford
006     ********************************************************************************/
007    package net.sf.tacos.services.impl;
008    
009    import java.util.ArrayList;
010    import java.util.Arrays;
011    import java.util.HashMap;
012    import java.util.Iterator;
013    import java.util.LinkedList;
014    import java.util.List;
015    import java.util.Map;
016    
017    import net.sf.tacos.services.CategoryInfo;
018    import net.sf.tacos.services.PageInfo;
019    import net.sf.tacos.services.SiteMap;
020    
021    import org.apache.commons.logging.Log;
022    import org.apache.commons.logging.LogFactory;
023    import org.apache.hivemind.Resource;
024    import org.dom4j.Document;
025    import org.dom4j.DocumentException;
026    import org.dom4j.Element;
027    import org.dom4j.Node;
028    import org.dom4j.io.SAXReader;
029    
030    /**
031     * This class reads the sitemap configuration, and provides access to
032     * basic relationship information, such as page categories, bread crumbs, and
033     * other information, that the application may need to ease navigation.
034     *
035     * @author Gabriel Handford
036     */
037    public class SiteMapImpl implements SiteMap {
038    
039        private static final Log log = LogFactory.getLog(SiteMapImpl.class);
040    
041        private Resource resource;
042        private Document document;
043    
044        private List cachedCategoryList = null;
045        private Map cachedPageMap = new HashMap();
046        private Map cachedCategoryInfo = new HashMap();
047        
048        private boolean compact;
049        
050        
051        /**
052         * Empty constructor.
053         */
054        public SiteMapImpl() { }
055        
056        /**
057         * Set the sitemap resource (xml).
058         * @param resource The resource (xml) file
059         */
060        public void setResource(Resource resource) {
061            this.resource = resource;        
062        }
063        
064        /**
065         * Initialize site map from a url (xml document).
066         * @param document
067         * @throws DocumentException on error
068         * @throws IllegalStateException If no resource has been set.
069         */
070        public void initialize() throws DocumentException {
071            if (resource == null)
072                throw new IllegalStateException(
073                        "No resource set, call setResource(..) before initialize()");
074            if (resource.getResourceURL() == null)
075                throw new IllegalStateException(
076                        "No valid resource URL could be resolved:" + resource);
077            log.debug("Sitemap resource: " + resource.getResourceURL());
078            SAXReader reader = new SAXReader();
079            document = reader.read(resource.getResourceURL());
080            
081            Element main = (Element)document.selectSingleNode("/sitemap");
082            
083            String val = main.attributeValue("compact");
084            if (val != null && !val.trim().equals("")) 
085                compact = Boolean.valueOf(val).booleanValue();
086        }
087    
088        /**
089         * Check if page name is contained in the page element tree.
090         * @param parentName The page to look for.
091         * @param pageName The page to start at (moves bottom-up).
092         * @return True if the parent contains the page in its tree.
093         */
094        public boolean contains(String parentName, String pageName) {
095            PageInfo pageInfo = getPageInfo(pageName);
096            return containsImpl(parentName, pageInfo);
097        }
098    
099        /**
100         * Check if page name is contained in the page element tree.
101         * @param parentName The page to look for.
102         * @param pageInfo The page to start at (moves bottom-up).
103         * @return True if the parent contains the page in its tree.
104         */
105        private boolean containsImpl(String parentName, PageInfo pageInfo) {
106            if (pageInfo == null) return false;
107            PageInfo parent = pageInfo.getParent();
108            if (parent == null) return false;
109            else if (parent.getName().equals(parentName)) return true;
110            else return containsImpl(parentName, parent);
111        }
112        
113        /**
114         * Get page information.
115         * @param name The page name
116         * @return The page info
117         */
118        public PageInfo getPageInfo(String name) {
119            return getPageInfoCacheImpl(name, null);
120        }
121    
122        /**
123         * Get page information (from cache).
124         * @param name The page name.
125         * @param source The page name source (for cycle detetion, first time is null).
126         * @return The page.
127         */
128        private PageInfo getPageInfoCacheImpl(String name, String source) {
129            PageInfo pi = (PageInfo)cachedPageMap.get(name);
130            if (pi == null) {
131                pi = getPageInfoImpl(name, source);
132                if (pi != null) cachedPageMap.put(name, pi);
133            } else {
134                //log.debug("Returning cached page info: " + name);
135            }
136            return pi;
137        }
138    
139        /**
140         * Get page information.
141         * @param name The page name
142         * @param source Original page name (for cycle detection)
143         * @return The page info
144         */
145        private PageInfo getPageInfoImpl(String name, String source) {
146            if (document == null) throw new IllegalStateException("No document set, call setResource and initialize.");
147            
148            // Cycle check
149            if (source != null && name.equals(source))
150                throw new IllegalStateException("Found a cycle, " + name + "->" + source);
151            else if (source == null)
152                source = name;
153            
154            log.debug("Getting page info for: " + name);
155            Element node = findPageNode(name);
156            if (node != null) {
157                
158                PageInfo page = parsePageNode(node);
159                
160                Node pageCategoryNode = document.selectSingleNode("/sitemap/category//page[@name='" + name + "']");
161                if (pageCategoryNode != null) {
162                    parseNodeTree(page, pageCategoryNode);
163                    //Because it will have been re-parsed
164                    return (PageInfo)cachedPageMap.get(page.getName());
165                }
166                
167                return page;
168            }
169            
170            log.warn("No page available for: " + name);
171            return null;
172        }
173        
174        /**
175         * Finds the specified page node in the sitemap, first trying to find
176         * it by name and then path.
177         * @param page
178         * @return The page, or null if not found.
179         */
180        protected Element findPageNode(String page)
181        {
182            Element element = (Element)document.selectSingleNode("/sitemap/page[@name='" + page + "']");
183            if (element == null && compact)
184            {
185                element = (Element)document.selectSingleNode("/sitemap/category//page[@name='" + page + "']");
186            }
187            return element;       
188        }
189        
190        /**
191         * Parses a particular page node, doesn't parse
192         * children or parent of {@link PageInfo}.
193         * 
194         * @param node Node to parse.
195         * @return
196         */
197        protected PageInfo parsePageNode(Element node)
198        {
199            if (node == null || !node.getName().equals("page"))
200                throw new IllegalArgumentException("Node was null or not a page node.");
201            
202            String name = node.attributeValue("name");
203            String desc = node.attributeValue("desc");
204            String perm = node.attributeValue("permission");
205            String listItem = node.attributeValue("navigable");
206            log.debug("Parsed perm of " + perm + " for page <" + name + ">");
207            //Whether or not to list this page if a child.
208            boolean list = true;
209            if (listItem != null && !listItem.trim().equals("")) 
210                list = Boolean.valueOf(listItem).booleanValue();
211            
212            PageInfo page = new PageInfoImpl(name, desc, perm, new ArrayList(), list);
213            return page;
214        }
215        
216        /**
217         * Iterates through the category tree represented 
218         * by this page node and parses all children and parents.
219         * 
220         * @param page The page that caused the tree parse
221         * @param pageNode
222         */
223        protected void parseNodeTree(PageInfo page, Node pageNode)
224        {
225            //First find top-level parent
226            Node parent = pageNode;
227            while (parent.getParent() != null 
228                    && parent.getParent().getName().equals("page")) {
229                parent = parent.getParent();
230            }
231            
232            log.debug("Parsing tree for page <" + page.getName() + ">");
233            //We can now parse all children of parent
234            parsePageNodes(parent, page);
235        }
236        
237        /**
238         * Parses all children of specified node and adds them
239         * to the page cache.
240         * 
241         * @param parent
242         * @param source Page that caused the original parse.
243         * @return The parent node that was parsed.
244         */
245        protected PageInfo parsePageNodes(Node parent, PageInfo source)
246        {
247            PageInfo parentInfo = parsePageNode(findPageNode(((Element)parent).attributeValue("name")));
248            cachedPageMap.put(parentInfo.getName(), parentInfo);
249            
250            log.debug("Parent page name <" + parent.valueOf("@name") + ">");
251            Iterator it = parent.selectNodes("*").iterator();
252            while (it.hasNext()) {
253                Node child = (Node)it.next();
254                log.debug("Adding child page name <" + child.valueOf("@name") + ">");
255                PageInfo childInfo = parsePageNodes(child, source);
256                
257                childInfo.setParent(parentInfo);
258                parentInfo.addChild(childInfo);
259            }
260            
261            return parentInfo;
262        }
263        
264        /**
265         * Get category listing.
266         * @return The category list
267         */
268        public synchronized List getCategories() {
269            if (document == null) throw new IllegalStateException("No document set, call setResource and initialize.");
270            if (cachedCategoryList == null) {
271                log.debug("Getting category listing...");
272                List nodes = document.selectNodes("/sitemap/category[@name]");
273                cachedCategoryList = new ArrayList(nodes.size());
274                for(int i = 0, size = nodes.size(); i < size; i++) {
275                    String category = ((Node)nodes.get(i)).valueOf("@name");
276                    log.debug("Adding category: " + category);
277                    cachedCategoryList.add(category);
278                }
279            }
280            return cachedCategoryList;
281        }
282    
283        /**
284         * Get category info for named category.
285         * @param name Category name
286         * @return The category info
287         * @see SiteMap#getCategoryInfo(java.lang.String)
288         */
289        public CategoryInfo getCategoryInfo(String name) {
290            CategoryInfo ci = (CategoryInfo)cachedCategoryInfo.get(name);
291            if (ci != null) {
292                //log.debug("Returning cached category info: " + name);
293                return ci;
294            }
295            
296            if (document == null) throw new IllegalStateException("No document set, call setResource and initialize.");
297            log.debug("Building category info for: " + name);
298            
299            String imgName = null;
300            String inactiveImgName = null;
301            
302            Element catnode = (Element)document.selectSingleNode("/sitemap/category[@name='" + name + "']");
303            if (catnode != null) {
304                imgName = catnode.attributeValue("image-active");
305                inactiveImgName = catnode.attributeValue("image-inactive");
306            }
307            
308            //Node node = document.selectSingleNode("/sitemap/category[@name='" + name + "']");
309            List nodes = document.selectNodes("/sitemap/category[@name='" + name + "']/page");
310            if (nodes != null) {
311                List pageNames = new ArrayList(nodes.size());
312                for(int i = 0; i < nodes.size(); i++) {
313                    Node node = (Node)nodes.get(i);
314                    String pageName = node.valueOf("@name");
315                    pageNames.add(pageName);
316                }
317                ci = new CategoryInfoImpl(name, pageNames, imgName, inactiveImgName);
318                cachedCategoryInfo.put(name, ci);
319                return ci;
320            }
321            log.debug("Returning empty category info for: " + name);
322            ci = new CategoryInfoImpl(name, null);
323            cachedCategoryInfo.put(name, ci);
324            return ci;
325        }
326    
327        /**
328         * Traverse up the tree to find the super parent.
329         * @param pageInfo The page.
330         * @return The topmost page.
331         */
332        private PageInfo getOrigin(PageInfo pageInfo) {
333            if (pageInfo != null && pageInfo.getParent() != null) return getOrigin(pageInfo.getParent());
334            return pageInfo;
335        }
336    
337        /**
338         * Get the category info for the page name.
339         * @param pageName The page name.
340         * @return The first category associated with this page.
341         */
342        public CategoryInfo getCategoryFromPage(String pageName) {
343            PageInfo pageInfo = getPageInfo(pageName);
344            if (pageInfo != null) {
345                PageInfo topMostPage = getOrigin(pageInfo);
346                String topPageName = topMostPage.getName();
347    
348                Node node = document.selectSingleNode("/sitemap/category/page[@name='" + topPageName + "']");
349                if (node != null) {
350                    String categoryName = node.getParent().valueOf("@name");
351                    return getCategoryInfo(categoryName);
352                }
353            }
354            return null;
355        }
356        
357        /**
358         * Check if page name is in the specified category.
359         * @param pageName Page name
360         * @param category Category
361         * @return True if in the specified category
362         */
363        public boolean inCategory(String pageName, String category) {                
364            CategoryInfo ci = getCategoryFromPage(pageName);
365            return (ci != null && ci.getName().equals(category));
366        }
367        
368        /**
369         * Get the default page.
370         * @param category Category
371         * @return Default page
372         */
373        public PageInfo getDefaultPage(String category) {
374            CategoryInfo ci = getCategoryInfo(category);
375            if (ci == null) return null;
376            return getPageInfo(ci.getDefaultPage());
377        }
378        
379        /**
380         * Get pages for a specific page names category.
381         * @param pageName Page name
382         * @return Pages
383         */
384        public List getCategoryPages(String pageName) {
385            CategoryInfo ci = getCategoryFromPage(pageName);
386            if (ci == null) return Arrays.asList(new Object[0]);
387            return ci.getPageNames();        
388        }
389        
390        /**
391         * Get the default page description.
392         * @param category Category
393         * @return Category default page description
394         */
395        public String getDefaultPageDesc(String category) {
396            PageInfo pi = getDefaultPage(category);
397            if (pi == null) return null;
398            return pi.getDesc();
399        }
400    
401        /**
402         * Get bread crumbs.
403         * @param pageName The page name.
404         * @return The bread crumbs (page name list).
405         */
406        public List getBreadCrumbs(String pageName) {
407            List list = new LinkedList();
408            PageInfo pageInfo = getPageInfo(pageName);
409            if (pageInfo == null) return Arrays.asList(new Object[0]);
410            loadBreadCrumbs(pageInfo, list);
411            return list;
412        }
413        
414        /**
415         * Load bread crumbs.
416         * @param pageInfo The page.
417         * @param breadCrumbs The list.
418         */
419        private void loadBreadCrumbs(PageInfo pageInfo, List breadCrumbs) {
420            PageInfo parent = pageInfo.getParent();
421            if (parent == null || parent.getName().equals(""))
422                return;
423    
424            breadCrumbs.add(0, parent.getName());
425            loadBreadCrumbs(parent, breadCrumbs);
426        }
427        
428        public boolean getCompact()
429        {
430            return compact;
431        }
432    
433    }