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>&nbsp; &nbsp; <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}