001/*
002 * CDDL HEADER START
003 *
004 * The contents of this file are subject to the terms of the
005 * Common Development and Distribution License, Version 1.0 only
006 * (the "License").  You may not use this file except in compliance
007 * with the License.
008 *
009 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
010 * or http://forgerock.org/license/CDDLv1.0.html.
011 * See the License for the specific language governing permissions
012 * and limitations under the License.
013 *
014 * When distributing Covered Code, include this CDDL HEADER in each
015 * file and include the License file at legal-notices/CDDLv1_0.txt.
016 * If applicable, add the following below this CDDL HEADER, with the
017 * fields enclosed by brackets "[]" replaced with your own identifying
018 * information:
019 *      Portions Copyright [yyyy] [name of copyright owner]
020 *
021 * CDDL HEADER END
022 *
023 *
024 *      Copyright 2008-2010 Sun Microsystems, Inc.
025 *      Portions Copyright 2011-2015 ForgeRock AS.
026 */
027package org.forgerock.opendj.maven.doc;
028
029import static org.apache.maven.plugins.annotations.LifecyclePhase.*;
030import static org.forgerock.opendj.maven.doc.DocsMessages.*;
031import static org.forgerock.util.Utils.*;
032
033import java.io.File;
034import java.io.FileInputStream;
035import java.io.IOException;
036import java.io.PrintWriter;
037import java.io.Writer;
038import java.text.SimpleDateFormat;
039import java.util.Date;
040import java.util.HashMap;
041import java.util.HashSet;
042import java.util.LinkedList;
043import java.util.List;
044import java.util.Map;
045import java.util.Properties;
046import java.util.Set;
047import java.util.TreeMap;
048import java.util.TreeSet;
049
050import freemarker.template.Configuration;
051import freemarker.template.TemplateException;
052import freemarker.template.TemplateExceptionHandler;
053import org.apache.maven.plugin.AbstractMojo;
054import org.apache.maven.plugin.MojoExecutionException;
055import org.apache.maven.plugin.MojoFailureException;
056import org.apache.maven.plugins.annotations.Mojo;
057import org.apache.maven.plugins.annotations.Parameter;
058import org.apache.maven.project.MavenProject;
059import org.forgerock.i18n.LocalizableMessage;
060
061/**
062 * Generates an XML file of log messages found in properties files.
063 */
064@Mojo(name = "generate-xml-messages-doc", defaultPhase = PRE_SITE)
065public class GenerateMessageFileMojo extends AbstractMojo {
066
067    /**
068     * The Maven Project.
069     */
070    @Parameter(property = "project", readonly = true, required = true)
071    private MavenProject project;
072
073    /**
074     * The tag of the locale for which to generate the documentation.
075     */
076    @Parameter(defaultValue = "en")
077    private String locale;
078
079    /**
080     * The path to the directory containing the message properties files.
081     */
082    @Parameter(required = true)
083    private String messagesDirectory;
084
085    /**
086     * The path to the directory where the XML file should be written.
087     * This path must be relative to ${project.build.directory}.
088     */
089    @Parameter(required = true)
090    private String outputDirectory;
091
092    /**
093     * A list which contains all file names, the extension is not needed.
094     */
095    @Parameter(required = true)
096    private List<String> messageFileNames;
097
098    /**
099     * One-line descriptions for log reference categories.
100     */
101    private static final HashMap<String, LocalizableMessage> CATEGORY_DESCRIPTIONS = new HashMap<>();
102    static {
103        CATEGORY_DESCRIPTIONS.put("ACCESS_CONTROL", CATEGORY_ACCESS_CONTROL.get());
104        CATEGORY_DESCRIPTIONS.put("ADMIN", CATEGORY_ADMIN.get());
105        CATEGORY_DESCRIPTIONS.put("ADMIN_TOOL", CATEGORY_ADMIN_TOOL.get());
106        CATEGORY_DESCRIPTIONS.put("BACKEND", CATEGORY_BACKEND.get());
107        CATEGORY_DESCRIPTIONS.put("CONFIG", CATEGORY_CONFIG.get());
108        CATEGORY_DESCRIPTIONS.put("CORE", CATEGORY_CORE.get());
109        CATEGORY_DESCRIPTIONS.put("DSCONFIG", CATEGORY_DSCONFIG.get());
110        CATEGORY_DESCRIPTIONS.put("EXTENSIONS", CATEGORY_EXTENSIONS.get());
111        CATEGORY_DESCRIPTIONS.put("JEB", CATEGORY_JEB.get());
112        CATEGORY_DESCRIPTIONS.put("LOG", CATEGORY_LOG.get());
113        CATEGORY_DESCRIPTIONS.put("PLUGIN", CATEGORY_PLUGIN.get());
114        CATEGORY_DESCRIPTIONS.put("PROTOCOL", CATEGORY_PROTOCOL.get());
115        CATEGORY_DESCRIPTIONS.put("QUICKSETUP", CATEGORY_QUICKSETUP.get());
116        CATEGORY_DESCRIPTIONS.put("RUNTIME_INFORMATION", CATEGORY_RUNTIME_INFORMATION.get());
117        CATEGORY_DESCRIPTIONS.put("SCHEMA", CATEGORY_SCHEMA.get());
118        CATEGORY_DESCRIPTIONS.put("SYNC", CATEGORY_SYNC.get());
119        CATEGORY_DESCRIPTIONS.put("TASK", CATEGORY_TASK.get());
120        CATEGORY_DESCRIPTIONS.put("THIRD_PARTY", CATEGORY_THIRD_PARTY.get());
121        CATEGORY_DESCRIPTIONS.put("TOOLS", CATEGORY_TOOLS.get());
122        CATEGORY_DESCRIPTIONS.put("USER_DEFINED", CATEGORY_USER_DEFINED.get());
123        CATEGORY_DESCRIPTIONS.put("UTIL", CATEGORY_UTIL.get());
124        CATEGORY_DESCRIPTIONS.put("VERSION", CATEGORY_VERSION.get());
125    }
126
127    /** Message giving formatting rules for string keys. */
128    public static final String KEY_FORM_MSG = ".\n\nOpenDJ message property keys must be of the form\n\n"
129            + "\t\'[CATEGORY]_[SEVERITY]_[DESCRIPTION]_[ORDINAL]\'\n\n";
130
131    private static final String ERROR_SEVERITY_IDENTIFIER_STRING = "ERR_";
132
133    /** FreeMarker template configuration. */
134    private Configuration configuration;
135
136    private Configuration getConfiguration() {
137        if (configuration == null) {
138            configuration = new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS);
139            configuration.setClassForTemplateLoading(GenerateSchemaDocMojo.class, "/templates");
140            configuration.setDefaultEncoding("UTF-8");
141            configuration.setTemplateExceptionHandler(TemplateExceptionHandler.DEBUG_HANDLER);
142        }
143        return configuration;
144    }
145
146    /**
147     * Writes the result of applying the FreeMarker template to the data.
148     * @param file                  The file to write to.
149     * @param template              The name of a file in {@code resources/templates/}.
150     * @param map                   The data to use in the template.
151     * @throws IOException          Failed to write to the file.
152     * @throws TemplateException    Failed to load the template.
153     */
154    private void writeLogRef(final File file, final String template, final Map<String, Object> map)
155            throws IOException, TemplateException {
156        // FreeMarker requires a configuration to find the template.
157        configuration = getConfiguration();
158
159        // FreeMarker takes the data and a Writer to process the template.
160        Writer writer = null;
161        try {
162            writer = new PrintWriter(file);
163            configuration.getTemplate(template).process(map, writer);
164        } finally {
165            closeSilently(writer);
166        }
167    }
168
169    /**
170     * Represents a log reference entry for an individual message.
171     */
172    private static class MessageRefEntry implements Comparable<MessageRefEntry> {
173        private Integer ordinal;
174        private String xmlId;
175        private String formatString;
176
177        /**
178         * Build log reference entry for an log message.
179         */
180        public MessageRefEntry(final String msgPropKey, final Integer ordinal, final String formatString) {
181            this.formatString = formatString;
182            this.ordinal = ordinal;
183            xmlId = getXmlId(msgPropKey);
184        }
185
186        private String getXmlId(final String messagePropertyKey) {
187            // XML IDs must be unique, must begin with a letter ([A-Za-z]),
188            // and may be followed by any number of letters, digits ([0-9]),
189            // hyphens ("-"), underscores ("_"), colons (":"), and periods (".").
190
191            final String invalidChars = "[^A-Za-z0-9\\-_:\\.]";
192            return messagePropertyKey.replaceAll(invalidChars, "-");
193        }
194
195        /**
196         * Returns a map of this log reference entry, suitable for use with FreeMarker.
197         * This implementation copies the message string verbatim.
198         * @return A map of this log reference entry, suitable for use with FreeMarker.
199         */
200        public Map<String, Object> toMap() {
201            Map<String, Object> map = new HashMap<>();
202            String id = (ordinal != null) ? ordinal.toString() : MESSAGE_NO_ORDINAL.get().toString();
203            map.put("xmlId", "log-ref-" + xmlId);
204            map.put("id", MESSAGE_ORDINAL_ID.get(id));
205            map.put("severity", MESSAGE_SEVERITY.get(ERROR_SEVERITY_PRINTABLE.get()));
206            map.put("message", MESSAGE_MESSAGE.get(formatString));
207            return map;
208        }
209
210        /**
211         * Compare message entries by unique identifier.
212         *
213         * @return See {@link java.lang.Comparable#compareTo(Object)}.
214         */
215        @Override
216        public int compareTo(MessageRefEntry mre) {
217            if (this.ordinal != null && mre.ordinal != null) {
218                return this.ordinal.compareTo(mre.ordinal);
219            }
220            return 0;
221        }
222    }
223
224    /** Represents a log reference list of messages for a category. */
225    private static class MessageRefCategory {
226        private String category;
227        private TreeSet<MessageRefEntry> messages;
228
229        MessageRefCategory(final String category, final TreeSet<MessageRefEntry> messages) {
230            this.category = category;
231            this.messages = messages;
232        }
233
234        /**
235         * Returns a map of this log reference category, suitable for use with FreeMarker.
236         * @return A map of this log reference category, suitable for use with FreeMarker.
237         */
238        public Map<String, Object> toMap() {
239            Map<String, Object> map = new HashMap<>();
240            map.put("id", category);
241            map.put("category", MESSAGE_CATEGORY.get(category));
242            List<Map<String, Object>> messageEntries = new LinkedList<>();
243            for (MessageRefEntry entry : messages) {
244                messageEntries.add(entry.toMap());
245            }
246            map.put("entries", messageEntries);
247            return map;
248        }
249    }
250
251    private static class MessagePropertyKey implements Comparable<MessagePropertyKey> {
252        private String description;
253        private Integer ordinal;
254
255        /**
256         * Creates a message property key from a string value.
257         *
258         * @param key
259         *            from properties file
260         * @return MessagePropertyKey created from string
261         */
262        public static MessagePropertyKey parseString(String key) {
263            int li = key.lastIndexOf("_");
264            if (li == -1) {
265                throw new IllegalArgumentException("Incorrectly formatted key " + key);
266            }
267
268            final String description = key.substring(0, li).toUpperCase();
269            Integer ordinal = null;
270            try {
271                String ordString = key.substring(li + 1);
272                ordinal = Integer.parseInt(ordString);
273            } catch (Exception nfe) {
274                // Ignore exception, the message has no ordinal.
275            }
276            return new MessagePropertyKey(description, ordinal);
277        }
278
279        /**
280         * Creates a parameterized instance.
281         *
282         * @param description
283         *            of this key
284         * @param ordinal
285         *            of this key
286         */
287        public MessagePropertyKey(String description, Integer ordinal) {
288            this.description = description;
289            this.ordinal = ordinal;
290        }
291
292        /**
293         * Gets the ordinal of this key.
294         *
295         * @return ordinal of this key
296         */
297        public Integer getOrdinal() {
298            return this.ordinal;
299        }
300
301        /** {@inheritDoc} */
302        @Override
303        public String toString() {
304            if (ordinal != null) {
305                return description + "_" + ordinal;
306            }
307            return description;
308        }
309
310        /** {@inheritDoc} */
311        @Override
312        public int compareTo(MessagePropertyKey k) {
313            if (ordinal == k.ordinal) {
314                return description.compareTo(k.description);
315            } else {
316                return ordinal.compareTo(k.ordinal);
317            }
318        }
319    }
320
321    /**
322     * For maven exec plugin execution. Generates for all included message files
323     * (sample.properties), a xml log ref file (log-ref-sample.xml)
324     *
325     * @throws MojoExecutionException
326     *          if a problem occurs
327     * @throws MojoFailureException
328     *          if a problem occurs
329     */
330    @Override
331    public void execute() throws MojoExecutionException, MojoFailureException {
332        String projectBuildDir = project.getBuild().getDirectory();
333
334        if (!outputDirectory.contains(projectBuildDir)) {
335            String errorMsg = String.format("outputDirectory parameter (%s) must be included "
336                    + "in ${project.build.directory} (%s)", outputDirectory, projectBuildDir);
337            getLog().error(errorMsg);
338            throw new MojoExecutionException(errorMsg);
339        }
340
341        Map<String, Object> map = new HashMap<>();
342        map.put("year", new SimpleDateFormat("yyyy").format(new Date()));
343        map.put("lang", locale);
344        map.put("title", LOG_REF_TITLE.get());
345        map.put("indexterm", LOG_REF_INDEXTERM.get());
346        map.put("intro", LOG_REF_INTRO.get());
347        List<Map<String, Object>> categories = new LinkedList<>();
348        for (String category : messageFileNames) {
349            File source = new File(messagesDirectory, category + ".properties");
350            categories.add(getCategoryMap(source, category.toUpperCase()));
351        }
352        map.put("categories", categories);
353        File file = new File(outputDirectory, "log-message-reference.xml");
354        try {
355            createOutputDirectory();
356            writeLogRef(file, "log-message-reference.ftl", map);
357        } catch (Exception e) {
358            throw new MojoFailureException(e.getMessage(), e);
359        }
360    }
361
362    private void createOutputDirectory() throws IOException {
363        File outputDir = new File(outputDirectory);
364        if (outputDir != null && !outputDir.exists()) {
365            if (!outputDir.mkdirs()) {
366                throw new IOException("Failed to create output directory.");
367            }
368        }
369    }
370
371    private Map<String, Object> getCategoryMap(File source, String globalCategory) throws MojoExecutionException {
372        Properties properties = new Properties();
373        try {
374            properties.load(new FileInputStream(source));
375            Map<MessagePropertyKey, String> errorMessages = loadErrorProperties(properties);
376            TreeSet<MessageRefEntry> messageRefEntries = new TreeSet<>();
377            Set<Integer> usedOrdinals = new HashSet<>();
378
379            for (MessagePropertyKey msgKey : errorMessages.keySet()) {
380                String formatString = errorMessages.get(msgKey).replaceAll("<", "&lt;");
381                Integer ordinal = msgKey.getOrdinal();
382                if (ordinal != null && usedOrdinals.contains(ordinal)) {
383                    throw new Exception("The ordinal value \'" + ordinal + "\' in key " + msgKey
384                            + " has been previously defined in " + source + KEY_FORM_MSG);
385                }
386                usedOrdinals.add(ordinal);
387                messageRefEntries.add(new MessageRefEntry(msgKey.toString(), ordinal, formatString));
388            }
389
390            return messageRefEntries.isEmpty()
391                    ? new HashMap<String, Object>()
392                    : new MessageRefCategory(globalCategory, messageRefEntries).toMap();
393        } catch (Exception e) {
394            throw new MojoExecutionException(e.getMessage(), e);
395        }
396    }
397
398    private Map<MessagePropertyKey, String> loadErrorProperties(Properties properties) throws Exception {
399        Map<MessagePropertyKey, String> errorMessage = new TreeMap<>();
400        for (Object propO : properties.keySet()) {
401            String propKey = propO.toString();
402            try {
403                // Document only ERROR messages.
404                if (propKey.startsWith(ERROR_SEVERITY_IDENTIFIER_STRING)) {
405                    MessagePropertyKey key = MessagePropertyKey.parseString(propKey);
406                    String formatString = properties.getProperty(propKey);
407                    errorMessage.put(key, formatString);
408                }
409            } catch (IllegalArgumentException iae) {
410                throw new Exception("invalid property key " + propKey + ": " + iae.getMessage() + KEY_FORM_MSG, iae);
411            }
412        }
413
414        return errorMessage;
415    }
416}