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