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