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 }