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 }