001/* 002 * #%L 003 * Netarchivesuite - common 004 * %% 005 * Copyright (C) 2005 - 2018 The Royal Danish Library, 006 * the National Library of France and the Austrian National Library. 007 * %% 008 * This program is free software: you can redistribute it and/or modify 009 * it under the terms of the GNU Lesser General Public License as 010 * published by the Free Software Foundation, either version 2.1 of the 011 * License, or (at your option) any later version. 012 * 013 * This program is distributed in the hope that it will be useful, 014 * but WITHOUT ANY WARRANTY; without even the implied warranty of 015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 016 * GNU General Lesser Public License for more details. 017 * 018 * You should have received a copy of the GNU General Lesser Public 019 * License along with this program. If not, see 020 * <http://www.gnu.org/licenses/lgpl-2.1.html>. 021 * #L% 022 */ 023 024package dk.netarkivet.common.webinterface; 025 026import java.io.IOException; 027import java.net.MalformedURLException; 028import java.net.URL; 029import java.util.ArrayList; 030import java.util.LinkedHashMap; 031import java.util.List; 032import java.util.Locale; 033import java.util.Map; 034 035import javax.servlet.http.HttpServletRequest; 036 037import org.slf4j.Logger; 038import org.slf4j.LoggerFactory; 039 040import dk.netarkivet.common.CommonSettings; 041import dk.netarkivet.common.exceptions.ArgumentNotValid; 042import dk.netarkivet.common.exceptions.IOFailure; 043import dk.netarkivet.common.utils.I18n; 044import dk.netarkivet.common.utils.Settings; 045 046/** 047 * This class holds information about one section of the site, including information about what to put in the menu 048 * sidebar and how to determine which page you're in. 049 */ 050public abstract class SiteSection { 051 052 private static final Logger log = LoggerFactory.getLogger(SiteSection.class); 053 054 /** The overall human-readable name of the section. */ 055 private final String mainname; 056 /** The number of pages that should be visible in the sidebar. */ 057 private final int visiblePages; 058 /** The map of page names ("path" part of URL) to page titles. */ 059 private final LinkedHashMap<String, String> pagesAndTitles = new LinkedHashMap<String, String>(); 060 /** The top level directory this section represents. */ 061 private String dirname; 062 /** The resource bundle with translations of this sitesection. */ 063 private String bundle; 064 /** 065 * Extension used for JSP files, including '.' separator. 066 */ 067 private static final String JSP_EXTENSION = ".jsp"; 068 /** 069 * Loaded list of site sections. 070 * 071 * @see #getSections() 072 */ 073 private static List<SiteSection> sections; 074 075 /** 076 * Create a new SiteSection object. 077 * 078 * @param mainname The name of the entire section used in the sidebar. 079 * @param prefix The prefix that all the JSP pages will have. 080 * @param visiblePages How many of the pages will be visible in the menu (taken from the start of the list). 081 * @param pagesAndTitles The actual pages and title-labels, without the prefix and jsp extension, involved in the 082 * section. They must be given as an array of 2-element arrays. 083 * @param dirname The top level directory this site section is deployed under. 084 * @param bundle The resource bundle with translations of this sitesection. 085 * @throws ArgumentNotValid if any of the elements of pagesAndTitles are not a 2-element array. 086 */ 087 public SiteSection(String mainname, String prefix, int visiblePages, String[][] pagesAndTitles, String dirname, 088 String bundle) { 089 ArgumentNotValid.checkNotNullOrEmpty(mainname, "mainname"); 090 ArgumentNotValid.checkNotNullOrEmpty(prefix, "prefix"); 091 ArgumentNotValid.checkNotNegative(visiblePages, "visiblePages"); 092 ArgumentNotValid.checkNotNull(pagesAndTitles, "String[][] pagesAndTitles"); 093 ArgumentNotValid.checkNotNullOrEmpty(dirname, "dirname"); 094 ArgumentNotValid.checkNotNull(bundle, "String bundle"); 095 this.dirname = dirname; 096 this.mainname = mainname; 097 this.visiblePages = visiblePages; 098 this.bundle = bundle; 099 for (String[] pageAndTitle : pagesAndTitles) { 100 if (pageAndTitle.length != 2) { 101 throw new ArgumentNotValid("Must have exactly page and title in " + prefix); 102 } 103 // Handle links outside webpages directory 104 // Assume pageurl ending with / as external url requiring no prefix 105 String pageurl= prefix + "-" + pageAndTitle[0] + JSP_EXTENSION; 106 if (pageAndTitle[0].endsWith("/")) { 107 pageurl = pageAndTitle[0]; // Add no prefix 108 } 109 String pagelabel = pageAndTitle[1]; 110 this.pagesAndTitles.put(pageurl, pagelabel); 111 } 112 } 113 114 /** 115 * Given a URL, returns the corresponding page title. 116 * 117 * @param req the HTTP request object to respond to 118 * @param url a given URL. 119 * @param locale the current locale. 120 * @return the corresponding page title, or null if it is not in this section, or is null. 121 * @throws ArgumentNotValid on null locale. 122 */ 123 public String getTitle(HttpServletRequest req, String url, Locale locale) { 124 ArgumentNotValid.checkNotNull(locale, "Locale locale"); 125 String page = getPage(req, url); 126 if (page == null) { 127 return null; 128 } 129 String label = pagesAndTitles.get(page); 130 if (label == null) { 131 return null; 132 } else { 133 return I18n.getString(bundle, locale, label); 134 } 135 } 136 137 /** 138 * Returns the page name from a URL, if the page is in this hierarchy, null otherwise. 139 * 140 * @param req the HTTP request object to respond to 141 * @param url Url to check 142 * @return Page name, or null for not in this hierarchy. 143 */ 144 private String getPage(HttpServletRequest req, String url) { 145 String contextPath; 146 URL parsed = null; 147 try { 148 parsed = new URL(url); 149 } catch (MalformedURLException e) { 150 return null; 151 } catch (NullPointerException e) { 152 return null; 153 } 154 String path = parsed.getPath(); 155 String page = null; 156 int index = path.lastIndexOf(dirname + "/"); 157 if (index != -1) { 158 page = path.substring(index + dirname.length() + 1); 159 } else if (req != null) { 160 contextPath = req.getContextPath(); 161 if (url.startsWith('/' + contextPath + '/')) { 162 // Context path is only /path. 163 page = url.substring(contextPath.length() + 2); 164 } 165 } 166 if (page != null) { 167 index = page.indexOf('/'); 168 if (index != -1) { 169 page = page.substring(0, index + 1); 170 } 171 } 172 return page; 173 } 174 175 /** 176 * Given a URL, returns the path part without schema, context path and query string. 177 * 178 * @param req the HTTP request object to respond to 179 * @param url a given URL. 180 * @return the path part of a URL without the schema, context path and query string 181 */ 182 public String getPath(HttpServletRequest req, String url) { 183 URL parsed; 184 String tmpPath; 185 int index; 186 String contextPath; 187 String path; 188 try { 189 parsed = new URL(url); 190 } catch (MalformedURLException e) { 191 return null; 192 } catch (NullPointerException e) { 193 return null; 194 } 195 tmpPath = parsed.getPath(); 196 index = tmpPath.indexOf('?'); 197 if (index != -1) { 198 tmpPath = tmpPath.substring(0, index); 199 } 200 path = null; 201 index = tmpPath.lastIndexOf(dirname + "/"); 202 if (index != -1) { 203 path = tmpPath.substring(index + dirname.length() + 1); 204 } else if (req != null) { 205 contextPath = req.getContextPath(); 206 if (url.startsWith('/' + contextPath + '/')) { 207 // Context path is only /path. 208 path = url.substring(contextPath.length() + 2); 209 } 210 } 211 return path; 212 } 213 214 /** 215 * Returns the first sub path of a given path without schema, context path and query string. 216 * 217 * @param page a processed path without schema, context path and query string 218 * @return 219 */ 220 public String getPageFromPage(String page) { 221 int index; 222 if (page != null) { 223 index = page.indexOf('/'); 224 if (index != -1) { 225 page = page.substring(0, index + 1); 226 } 227 } 228 return page; 229 } 230 231 /** 232 * Generate this section's part of the navigation tree (sidebar). This outputs balanced HTML to the JspWriter. It 233 * uses a locale to generate the right titles. 234 * 235 * @param out A place to write our HTML 236 * @param url The url of the page we're currently viewing. The list of subpages will only be displayed if the page 237 * we're viewing is one that belongs to this section. 238 * @param locale The locale to generate the navigation tree for. 239 * @throws IOException If there is a problem writing to the page. 240 */ 241 public void generateNavigationTree(StringBuilder sb, HttpServletRequest req, String url, String subMenu, Locale locale) throws IOException { 242 String firstPage = pagesAndTitles.keySet().iterator().next(); 243 sb.append("<tr>"); 244 sb.append("<td><a href=\"/"); 245 sb.append(HTMLUtils.encode(dirname)); 246 sb.append("/"); 247 sb.append(HTMLUtils.encode(firstPage)); 248 sb.append("\">"); 249 sb.append(HTMLUtils.escapeHtmlValues(I18n.getString(bundle, locale, mainname))); 250 sb.append("</a></td>\n"); 251 sb.append("</tr>"); 252 // If we are on the above page or one of its subpages, display the 253 // next level down in the tree 254 String path = getPath(req, url); 255 String page = getPageFromPage(path); 256 if (page == null) { 257 return; 258 } 259 if (pagesAndTitles.containsKey(page)) { 260 int i = 0; 261 String link; 262 for (Map.Entry<String, String> pageAndTitle : pagesAndTitles.entrySet()) { 263 if (i == visiblePages) { 264 break; 265 } 266 link = pageAndTitle.getKey(); 267 sb.append("<tr>"); 268 sb.append("<td> <a href=\"/"); 269 sb.append(HTMLUtils.encode(dirname)); 270 sb.append("/"); 271 sb.append(link); 272 sb.append("\"> "); 273 if (path.equals(link)) { 274 sb.append("<b>"); 275 sb.append(HTMLUtils.escapeHtmlValues(I18n.getString(bundle, locale, pageAndTitle.getValue()))); 276 sb.append("</b>"); 277 } else { 278 sb.append(HTMLUtils.escapeHtmlValues(I18n.getString(bundle, locale, pageAndTitle.getValue()))); 279 } 280 sb.append("</a></td>"); 281 sb.append("</tr>\n"); 282 if (subMenu != null && page != null && path.startsWith(link) && path.length() > link.length()) { 283 sb.append(subMenu); 284 } 285 ++i; 286 } 287 } 288 } 289 290 /** 291 * Return the directory name of this site section. 292 * 293 * @return The dirname. 294 */ 295 public String getDirname() { 296 return dirname; 297 } 298 299 /** 300 * Called when the site section is first deployed. Meant to be overridden by subclasses. 301 */ 302 public abstract void initialize(); 303 304 /** 305 * Called when webserver shuts down. Meant to be overridden by subclasses. 306 */ 307 public abstract void close(); 308 309 /** 310 * The list of sections of the website. Each section has a number of pages, as defined in the sitesection classes 311 * read from settings. These handle outputting their HTML part of the sidebar, depending on where in the site we 312 * are. 313 * 314 * @return A list of site sections instantiated from settings. 315 * @throws IOFailure if site sections cannot be read from settings. 316 */ 317 public static synchronized List<SiteSection> getSections() { 318 if (sections == null) { 319 sections = new ArrayList<>(); 320 String[] sitesections = Settings.getAll(CommonSettings.SITESECTION_CLASS); 321 log.debug("Loading {} site section(s).", sitesections.length); 322 for (String sitesection : sitesections) { 323 log.debug("Loading site section {}.", sitesection.toString()); 324 try { 325 ClassLoader loader = SiteSection.class.getClassLoader(); 326 sections.add((SiteSection) loader.loadClass(sitesection).newInstance()); 327 } catch (Exception e) { 328 log.warn("Error loading class {}.", sitesection, e); 329 throw new IOFailure("Cannot read site section " + sitesection + " from settings", e); 330 } 331 } 332 } 333 return sections; 334 } 335 336 /** 337 * Clean up site sections. This method calls close on all deployed site sections, and resets the list of site 338 * sections. 339 */ 340 public static synchronized void cleanup() { 341 if (sections != null) { 342 for (SiteSection section : sections) { 343 section.close(); 344 } 345 } 346 sections = null; 347 348 } 349 350 /** 351 * Check whether a section with a given dirName is deployed. 352 * 353 * @param dirName The dirName to check for 354 * @return True of deployed, false otherwise. 355 * @throws ArgumentNotValid if dirName is null or empty. 356 */ 357 public static boolean isDeployed(String dirName) { 358 ArgumentNotValid.checkNotNullOrEmpty(dirName, "String dirName"); 359 boolean sectionDeployed = false; 360 for (SiteSection section : getSections()) { 361 if (section.getDirname().equals(dirName)) { 362 sectionDeployed = true; 363 } 364 } 365 return sectionDeployed; 366 } 367}