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 }