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 }