001/*
002 * #%L
003 * Netarchivesuite - monitor
004 * %%
005 * Copyright (C) 2005 - 2014 The Royal Danish Library, the Danish State and University 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.monitor.webinterface;
025
026import java.io.BufferedReader;
027import java.io.IOException;
028import java.io.StringReader;
029import java.util.List;
030import java.util.Locale;
031import java.util.Random;
032
033import javax.management.MalformedObjectNameException;
034import javax.servlet.http.HttpServletRequest;
035import javax.servlet.http.HttpServletResponse;
036import javax.servlet.jsp.JspWriter;
037import javax.servlet.jsp.PageContext;
038
039import org.slf4j.Logger;
040import org.slf4j.LoggerFactory;
041
042import dk.netarkivet.common.exceptions.ArgumentNotValid;
043import dk.netarkivet.common.exceptions.ForwardedToErrorPage;
044import dk.netarkivet.common.utils.DomainUtils;
045import dk.netarkivet.common.utils.I18n;
046import dk.netarkivet.common.utils.Settings;
047import dk.netarkivet.common.utils.StringUtils;
048import dk.netarkivet.common.webinterface.HTMLUtils;
049import dk.netarkivet.monitor.Constants;
050import dk.netarkivet.monitor.MonitorSettings;
051
052/**
053 * Various utility methods and classes for the JMX Monitor page. and a bunch of JMX properties used by
054 * Monitor-JMXsummary.jsp.
055 */
056public class JMXSummaryUtils {
057
058    private static final Logger log = LoggerFactory.getLogger(JMXSummaryUtils.class);
059
060
061    /** JMX property for remove application button. */
062    public static final String JMXRemoveApplication = dk.netarkivet.common.management.Constants.REMOVE_JMX_APPLICATION;
063    /** JMX property for the physical location. */
064    public static final String JMXPhysLocationProperty = dk.netarkivet.common.management.Constants.PRIORITY_KEY_LOCATION;
065    /** JMX property for the machine name. */
066    public static final String JMXMachineNameProperty = dk.netarkivet.common.management.Constants.PRIORITY_KEY_MACHINE;
067    /** JMX property for the application name. */
068    public static final String JMXApplicationNameProperty = dk.netarkivet.common.management.Constants.PRIORITY_KEY_APPLICATIONNAME;
069    /** JMX property for the application instance id. */
070    public static final String JMXApplicationInstIdProperty = dk.netarkivet.common.management.Constants.PRIORITY_KEY_APPLICATIONINSTANCEID;
071    /** JMX property for the HTTP port. */
072    public static final String JMXHttpportProperty = dk.netarkivet.common.management.Constants.PRIORITY_KEY_HTTP_PORT;
073    /** JMX property for the harvest channel. */
074    public static final String JMXHarvestChannelProperty = dk.netarkivet.common.management.Constants.PRIORITY_KEY_CHANNEL;
075    /** JMX property for the replica name. */
076    public static final String JMXArchiveReplicaNameProperty = dk.netarkivet.common.management.Constants.PRIORITY_KEY_REPLICANAME;
077    /** JMX property for the index. */
078    public static final String JMXIndexProperty = dk.netarkivet.common.management.Constants.PRIORITY_KEY_INDEX;
079
080    /**
081     * The preferred length of lines in the jmx log.
082     */
083    private static final int PREFERRED_LENGTH = Settings.getInt(MonitorSettings.JMX_PREFERRED_MAX_LOG_LENGTH);
084
085    /**
086     * The maximum length of lines in the jmx log.
087     */
088    private static final int MAX_LENGTH = Settings.getInt(MonitorSettings.JMX_ABSOLUTE_MAX_LOG_LENGTH);
089
090    /** JMX properties, which can set to star. */
091    public static final String[] STARRABLE_PARAMETERS = new String[] {JMXRemoveApplication, JMXPhysLocationProperty,
092            JMXMachineNameProperty, JMXApplicationNameProperty, JMXApplicationInstIdProperty, JMXHttpportProperty,
093            JMXHarvestChannelProperty, JMXArchiveReplicaNameProperty, JMXIndexProperty};
094    /** Status/Monitor-JMXsummary.jsp. */
095    public static final String STATUS_MONITOR_JMXSUMMARY = "Status/Monitor-JMXsummary.jsp";
096
097    /** The log MBean name prefix. */
098    private static final String LOGGING_MBEAN_NAME_PREFIX = "dk.netarkivet.common.logging:";
099
100    /** Internationalisation object. */
101    private static final I18n I18N = new I18n(dk.netarkivet.monitor.Constants.TRANSLATIONS_BUNDLE);
102
103    /** The character for show all. */
104    private static final String CHARACTER_SHOW_ALL = "*";
105    /** The character for don't show column. */
106    private static final String CHARACTER_NOT_COLUMN = "-";
107    /** The character for only seeing the first row of the log. */
108    private static final String CHARACTER_FIRST_ROW = "0";
109    /** The number of log lines showed in generateMessage. */
110    private static final int NUMBER_OF_LOG_LINES = 5;
111
112    /** Instance of class Random used to generate a unique id for each div. */
113    private static final Random random = new Random();
114
115    /**
116     * Reduce the class name of an application to the essentials.
117     *
118     * @param applicationName The class name of the application, should not be null.
119     * @return A reduced name suitable for user output.
120     * @throws ArgumentNotValid if argument isn't valid.
121     */
122    public static String reduceApplicationName(String applicationName) throws ArgumentNotValid {
123        ArgumentNotValid.checkNotNull(applicationName, "String applicationName");
124        String[] split = applicationName.split("\\.");
125        return split[split.length - 1];
126    }
127
128    /**
129     * Creates the show links for showing columns again.
130     * <p>
131     * Goes through all parameters to check if their column is active. If a column is not active, the link to showing a
132     * specific column again is generated.
133     *
134     * @param starredRequest A request to take parameters from, should be different from null.
135     * @param l For retrieving the correct words form the current language.
136     * @return The link to show the parameter again.
137     * @throws ArgumentNotValid if argument isn't valid.
138     */
139    public static String generateShowColumn(StarredRequest starredRequest, Locale l) throws ArgumentNotValid {
140        ArgumentNotValid.checkNotNull(starredRequest, "StarredRequest starredRequest");
141
142        StringBuilder res = new StringBuilder();
143
144        for (String parameter : STARRABLE_PARAMETERS) {
145            if (CHARACTER_NOT_COLUMN.equals(starredRequest.getParameter(parameter))) {
146                // generate the link, but use the parameter applied to the
147                // table field value.
148                res.append(generateLink(starredRequest, parameter, CHARACTER_SHOW_ALL,
149                        I18N.getString(l, "tablefield;" + parameter)));
150                res.append(",");
151            }
152        }
153
154        // If any content, then remove last ',' and put 'show' in front
155        if (res.length() > 0) {
156            res.deleteCharAt(res.length() - 1);
157            res.insert(0, I18N.getString(l, "show") + ": ");
158        }
159
160        return res.toString();
161    }
162
163    /**
164     * Generate HTML to show at the top of the table, containing a "show all" link if the parameter is currently
165     * restricted. This function is only used by JMXIndexProperty field, the other properties uses generateShowLing
166     * instead.
167     *
168     * @param starredRequest A request to take parameters from, should not be null.
169     * @param parameter The parameter that, if not already unrestricted, should be unrestricted in the "show all" link,
170     * should not be null.
171     * @param l the current locale.
172     * @return HTML to insert at the top of the JMX monitor table.
173     * @throws ArgumentNotValid if arguments isn't valid.
174     */
175    public static String generateShowAllLink(StarredRequest starredRequest, String parameter, Locale l)
176            throws ArgumentNotValid {
177        ArgumentNotValid.checkNotNull(starredRequest, "StarredRequest starredRequest");
178        ArgumentNotValid.checkNotNull(parameter, "String parameter");
179        if (CHARACTER_SHOW_ALL.equals(starredRequest.getParameter(parameter))) {
180            return "";
181        } else {
182            return "(" + generateLink(starredRequest, parameter, CHARACTER_SHOW_ALL, I18N.getString(l, "showall"))
183                    + ")";
184        }
185    }
186
187    /**
188     * Generate HTML to show at the top of the table, containing a "show all" and a "off" links if the parameter is
189     * currently restricted.
190     *
191     * @param starredRequest A request to take parameters from, should not be null.
192     * @param parameter The parameter that, if not already unrestricted, should be unrestricted in the "show all",
193     * should not be null.
194     * @param l the current locale.
195     * @return HTML to insert at the top of the JMX monitor table.
196     * @throws ArgumentNotValid if arguments isn't valid.
197     */
198    public static String generateShowLink(StarredRequest starredRequest, String parameter, Locale l)
199            throws ArgumentNotValid {
200        ArgumentNotValid.checkNotNull(starredRequest, "StarredRequest starredRequest");
201        ArgumentNotValid.checkNotNull(parameter, "String parameter");
202        if (CHARACTER_SHOW_ALL.equals(starredRequest.getParameter(parameter))) {
203            return "(" + generateLink(starredRequest, parameter, CHARACTER_NOT_COLUMN, I18N.getString(l, "hide")) + ")";
204        } else {
205            return "(" + generateLink(starredRequest, parameter, CHARACTER_SHOW_ALL, I18N.getString(l, "showall"))
206                    + ", " + generateLink(starredRequest, parameter, CHARACTER_NOT_COLUMN, I18N.getString(l, "hide"))
207                    + ")";
208        }
209    }
210
211    /**
212     * Tests if a parameter in the request is "-" (thus off).
213     *
214     * @param starredRequest A request to take parameters from, should not be null.
215     * @param parameter The parameter that should be tested.
216     * @return Whether the parameter is set to "-".
217     * @throws ArgumentNotValid if argument isn't valid.
218     */
219    public static boolean showColumn(StarredRequest starredRequest, String parameter) throws ArgumentNotValid {
220        ArgumentNotValid.checkNotNull(starredRequest, "StarredRequest starredRequest");
221        ArgumentNotValid.checkNotNullOrEmpty(parameter, "String parameter");
222        if (CHARACTER_NOT_COLUMN.equals(starredRequest.getParameter(parameter))) {
223            return false;
224        }
225        return true;
226    }
227
228    /**
229     * Generate an HTML link to the JMX summary page with one part of the URL parameters set to a specific value.
230     *
231     * @param request A request to draw other parameter values from, should not be null.
232     * @param setPart Which of the parameters to set.
233     * @param setValue The value to set that parameter to.
234     * @param linkText The HTML text that should go inside the link. Remember to escape HTML values if inserting a
235     * normal string.
236     * @return A link to insert in the page, or an unlinked text, if setPart or setValue is null, or an empty string if
237     * linkText is null.
238     * @throws ArgumentNotValid if request is null.
239     */
240    public static String generateLink(StarredRequest request, String setPart, String setValue, String linkText)
241            throws ArgumentNotValid {
242        ArgumentNotValid.checkNotNull(request, "StarredRequest request");
243        if (linkText == null) {
244            return "";
245        }
246        if (setPart == null || setValue == null) {
247            return linkText;
248        }
249        StringBuilder builder = new StringBuilder();
250        builder.append("<a href=\"/" + STATUS_MONITOR_JMXSUMMARY + "?");
251        boolean isFirst = true;
252        for (String queryPart : STARRABLE_PARAMETERS) {
253            if (isFirst) {
254                isFirst = false;
255            } else {
256                builder.append("&amp;");
257            }
258            builder.append(queryPart);
259            builder.append("=");
260            if (queryPart.equals(setPart)) {
261                builder.append(HTMLUtils.encode(setValue));
262            } else {
263                builder.append(HTMLUtils.encode(request.getParameter(queryPart)));
264            }
265        }
266        builder.append("\">");
267        builder.append(linkText);
268        builder.append("</a>");
269        return builder.toString();
270    }
271
272    /**
273     * Get status entries from JMX based on a request and some parameters.
274     *
275     * @param parameters The parameters to query JMX for, should not be null.
276     * @param request A request possibly containing values for some of the parameters, should not be null.
277     * @param context the current JSP context, should not be null.
278     * @return Status entries for the MBeans that match the parameters.
279     * @throws ArgumentNotValid if the query is invalid (typically caused by invalid parameters).
280     * @throws ForwardedToErrorPage if unable to create JMX-query.
281     */
282    public static List<StatusEntry> queryJMXFromRequest(String[] parameters, StarredRequest request, PageContext context)
283            throws ArgumentNotValid, ForwardedToErrorPage {
284        ArgumentNotValid.checkNotNull(parameters, "String[] parameters");
285        ArgumentNotValid.checkNotNull(request, "StarredRequest request");
286        ArgumentNotValid.checkNotNull(context, "PageContext context");
287
288        String query = null;
289        try {
290            query = createJMXQuery(parameters, request);
291            log.debug("Executing JMX query: " + query);
292            return JMXStatusEntry.queryJMX(query);
293        } catch (MalformedObjectNameException e) {
294            if (query != null) {
295                HTMLUtils.forwardWithErrorMessage(context, I18N, e, "errormsg;error.in.querying.jmx.with.query.0",
296                        query);
297                throw new ForwardedToErrorPage("Error in querying JMX with query '" + query + "'.", e);
298            } else {
299                HTMLUtils.forwardWithErrorMessage(context, I18N, e, "errormsg;error.in.building.jmxquery");
300                throw new ForwardedToErrorPage("Error building JMX query", e);
301            }
302        }
303    }
304
305    /**
306     * Select zero or more beans from JMX and unregister these.
307     *
308     * @param parameters The parameters to query JMX for, should not be null.
309     * @param request A request possibly containing values for some of the parameters, which select zero or more beans.
310     * @param context the current JSP context, should not be null.
311     * @throws ArgumentNotValid if arguments isn't valid.
312     */
313    public static void unregisterJMXInstance(String[] parameters, StarredRequest request, PageContext context)
314            throws ArgumentNotValid {
315        ArgumentNotValid.checkNotNull(parameters, "String[] parameters");
316        ArgumentNotValid.checkNotNull(context, "PageContext context");
317        String query = null;
318        try {
319            query = createJMXQuery(parameters, request);
320            JMXStatusEntry.unregisterJMXInstance(query);
321        } catch (MalformedObjectNameException e) {
322            if (query != null) {
323                HTMLUtils.forwardWithErrorMessage(context, I18N, e, "errormsg;error.in.querying.jmx.with.query.0",
324                        query);
325                throw new ForwardedToErrorPage("Error in querying JMX with query '" + query + "'.", e);
326            } else {
327                HTMLUtils.forwardWithErrorMessage(context, I18N, e, "errormsg;error.in.building.jmxquery");
328                throw new ForwardedToErrorPage("Error building JMX query", e);
329            }
330        } catch (Exception e) {
331            // Both InstanceNotFoundException and MBeanRegistrationException
332            // are treated equal.
333            HTMLUtils.forwardWithErrorMessage(context, I18N, e, "errormsg;error.when.unregistering.mbean.0", query);
334            throw new ForwardedToErrorPage("Error when unregistering JMX MBean with query '" + query + "'.", e);
335        }
336    }
337
338    /**
339     * Build a JMX query string (ObjectName) from a request and a list of parameters to query for. This string is always
340     * a property pattern (wildcarded), even if all the values we define in the names are specified.
341     *
342     * @param parameters The parameters to query for. These should make up the parts of the unique identification of an
343     * MBean.
344     * @param starredRequest A request containing current values for the given parameters.
345     * @return A query, wildcarded for those parameters that are or missing in starredRequest.
346     * @throws ArgumentNotValid if one or all of the arguements are null.
347     */
348    public static String createJMXQuery(String[] parameters, StarredRequest starredRequest) throws ArgumentNotValid {
349        ArgumentNotValid.checkNotNull(parameters, "String[] parameters");
350        ArgumentNotValid.checkNotNull(starredRequest, "StarredRequest starredRequest");
351        StringBuilder query = new StringBuilder(LOGGING_MBEAN_NAME_PREFIX + "*");
352        for (String queryPart : parameters) {
353            if (!CHARACTER_SHOW_ALL.equals(starredRequest.getParameter(queryPart))
354                    && !CHARACTER_NOT_COLUMN.equals(starredRequest.getParameter(queryPart))) {
355                query.append(",");
356                query.append(queryPart);
357                query.append("=");
358                String parameter = starredRequest.getParameter(queryPart);
359                query.append(parameter);
360            }
361        }
362
363        return query.toString();
364    }
365
366    /**
367     * Make an HTML fragment that shows a log message preformatted. If the log message is longer than
368     * NUMBER_OF_LOG_LINES lines, the rest are hidden and replaced with an internationalized link "More..." that will
369     * show the rest. Long loglines are split to length approximately monitor.preferredMaxJMXLogLength and then split
370     * again to ensure that no lines are wider than monitor.absoluteMaxJMXLogLength. However as the wrapping algorithm
371     * is not sophisticated it is better to split the lines in a meaningful way where they are generated.
372     *
373     * @param logMessage The log message to present.
374     * @param l the current Locale.
375     * @return An HTML fragment as defined above.
376     * @throws ArgumentNotValid if argument isn't valid.
377     */
378    public static String generateMessage(String logMessage, Locale l) throws ArgumentNotValid {
379        ArgumentNotValid.checkNotNull(logMessage, "String logMessage");
380        StringBuilder msg = new StringBuilder();
381        logMessage = HTMLUtils.escapeHtmlValues(logMessage);
382        // All strings starting with "http:" or "https:" are replaced with
383        // a proper HTML Anchor
384        logMessage = logMessage.replaceAll("(https?://[^ \\t\\n\"]*)", "<a href=\"$1\">$1</a>");
385        logMessage = StringUtils.splitStringOnWhitespace(logMessage, PREFERRED_LENGTH);
386        logMessage = StringUtils.splitStringForce(logMessage, MAX_LENGTH);
387        BufferedReader sr = new BufferedReader(new StringReader(logMessage));
388        msg.append("<pre>");
389        int lineno = 0;
390        String line;
391        try {
392            while (lineno < NUMBER_OF_LOG_LINES && (line = sr.readLine()) != null) {
393                msg.append(line);
394                msg.append('\n');
395                ++lineno;
396            }
397            msg.append("</pre>");
398            if ((line = sr.readLine()) != null) {
399                // We use a random number for generating a unique id for the div.
400                // TODO should change method to take an integer, so no
401                // colidation is happening.
402                int id = random.nextInt();
403                msg.append("<a id=\"show");
404                msg.append(id);
405                msg.append("\" onclick=\"document.getElementById('log");
406                msg.append(id);
407                msg.append("').style.display='block';" + "document.getElementById('show");
408                msg.append(id);
409
410                msg.append("').style.display='none';\">");
411                msg.append(I18N.getString(l, "more.dot.dot.dot"));
412                msg.append("</a>");
413
414                msg.append("<div id=\"log");
415                msg.append(id);
416                msg.append("\" style=\"display:none\">");
417                msg.append("<pre>");
418                // Display the rest of the message in a div, which are not
419                // visible.
420                do {
421                    msg.append(HTMLUtils.escapeHtmlValues(line));
422                    msg.append('\n');
423                } while ((line = sr.readLine()) != null);
424                msg.append("</pre>");
425                msg.append("</div>");
426            }
427        } catch (IOException e) {
428            // This should never happen, but if it does, just return the string
429            // without all the fancy stuff.
430            return "<pre>" + HTMLUtils.escapeHtmlValues(logMessage) + "</pre>";
431        }
432        return msg.toString();
433    }
434
435    /**
436     * This class encapsulates a HttpServletRequest, making non-existing parameters appear as "*" for wildcard (or "0"
437     * for the index parameter).
438     */
439    public static class StarredRequest {
440        /** A http request, for a starred request. */
441        HttpServletRequest req;
442
443        /**
444         * Makes the request reusable for this class.
445         *
446         * @param req A http request, for a starred request, should not be null.
447         * @throws ArgumentNotValid if argument isn't valid.
448         */
449        public StarredRequest(HttpServletRequest req) throws ArgumentNotValid {
450            ArgumentNotValid.checkNotNull(req, "HttpServletRequest req");
451            this.req = req;
452        }
453
454        /**
455         * Gets a parameter from the original request, except if the parameter is unset, return the following. "index" =
456         * "0". "applicationInstanceId" = "-". "location" = "-". "http-port" = "-". Default = "*".
457         *
458         * @param paramName The parameter.
459         * @return The parameter or "*", "0" or "-"; never null.
460         * @throws ArgumentNotValid if argument isn't valid.
461         */
462        public String getParameter(String paramName) throws ArgumentNotValid {
463            ArgumentNotValid.checkNotNull(paramName, "String paramName");
464            String value = req.getParameter(paramName);
465            if (value == null || value.length() == 0) {
466                if (JMXIndexProperty.equals(paramName)) {
467                    return CHARACTER_FIRST_ROW;
468                } else if (JMXPhysLocationProperty.equals(paramName)) {
469                    return CHARACTER_NOT_COLUMN;
470                } else if (JMXApplicationInstIdProperty.equals(paramName)) {
471                    return CHARACTER_NOT_COLUMN;
472                } else if (JMXHttpportProperty.equals(paramName)) {
473                    return CHARACTER_NOT_COLUMN;
474                } else {
475                    return CHARACTER_SHOW_ALL;
476                }
477            } else {
478                return value;
479            }
480        }
481    }
482
483    /**
484     * Remove an application. Used by Monitor-JMXsummary.jsp. Code has been moved from the jsp to here to avoid compile errors at
485     * runtime in correlation with the upgrade to java 1.8 and introduction of embedded tomcat to handle jsp pages. This was previously done via jetty 6.
486     *
487     * @param request http request from selvlet
488     * @param response http session for page context Harveststatus-running.jsp
489     * @param pageContext http session for page context Harveststatus-running.jsp
490     */
491    public static void RemoveApplication(HttpServletRequest request, HttpServletResponse response, PageContext pageContext)
492            throws IOException, ArgumentNotValid {
493        ArgumentNotValid.checkNotNull(request, "ServletRequest request");
494        ArgumentNotValid.checkNotNull(response, "ServletRequest response");
495        ArgumentNotValid.checkNotNull(pageContext, "PageContext pageContext");
496
497        // remove application if parameter remove is set.
498        String remove = request.getParameter(Constants.REMOVE);
499        if(remove != null) {
500            JMXSummaryUtils.StarredRequest starredRequest =
501                    new JMXSummaryUtils.StarredRequest(request);
502            try {
503                JMXSummaryUtils.unregisterJMXInstance(
504                        JMXSummaryUtils.STARRABLE_PARAMETERS, starredRequest,
505                        pageContext);
506                StringBuilder builder = new StringBuilder("/");
507                builder.append(JMXSummaryUtils.STATUS_MONITOR_JMXSUMMARY);
508                builder.append("?");
509                /**
510                 * oldquery is set in the link to remove an application from the summary view. It is used to
511                 * enable us to return to the previous view after removing an application.
512                 */
513                String oldquery = starredRequest.getParameter("oldquery");
514                if (oldquery != null) {
515                    builder.append(java.net.URLDecoder.decode(oldquery));
516                }
517                response.sendRedirect(builder.toString());
518            } catch (ForwardedToErrorPage e) {
519                return;
520            }
521
522        }
523    }
524
525}