View Javadoc

1   /*
2    * Copyright (c) 2010 Kathryn Huxtable.
3    *
4    * This file is part of the Image Generator Maven plugin.
5    *
6    * Licensed under the Apache License, Version 2.0 (the "License");
7    * you may not use this file except in compliance with the License.
8    * You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   *
18   * $Id$
19   */
20  package org.kathrynhuxtable.maven.plugins.imageGenerator;
21  
22  import java.awt.Graphics;
23  import java.awt.image.BufferedImage;
24  
25  import java.io.BufferedReader;
26  import java.io.BufferedWriter;
27  import java.io.File;
28  import java.io.FileInputStream;
29  import java.io.FileNotFoundException;
30  import java.io.FileWriter;
31  import java.io.IOException;
32  import java.io.InputStream;
33  import java.io.InputStreamReader;
34  
35  import java.lang.reflect.Constructor;
36  
37  import java.util.ArrayList;
38  import java.util.HashMap;
39  import java.util.List;
40  import java.util.Map;
41  
42  import javax.imageio.ImageIO;
43  
44  import javax.swing.JComponent;
45  import javax.swing.JPanel;
46  import javax.swing.UIManager;
47  
48  import javax.xml.parsers.DocumentBuilder;
49  import javax.xml.parsers.DocumentBuilderFactory;
50  
51  import org.apache.maven.plugin.AbstractMojo;
52  import org.apache.maven.plugin.MojoExecutionException;
53  
54  import org.w3c.dom.Document;
55  import org.w3c.dom.Element;
56  import org.w3c.dom.Node;
57  import org.w3c.dom.NodeList;
58  
59  /**
60   * Goal creates a directory of images from Swing using a specified XML file.
61   *
62   * @description                  Create a directory of images from Swing using a
63   *                               specified XML file.
64   * @goal                         generate
65   * @phase                        pre-site
66   * @requiresDependencyResolution runtime
67   * @configurator                 include-project-dependencies
68   */
69  public class ImageGeneratorMojo extends AbstractMojo {
70  
71      /**
72       * Location of the configuration file.
73       *
74       * @parameter expression="${imagegenerator.configFile}"
75       *            default-value="${basedir}/src/site/image-generator.xml"
76       */
77      private File configFile;
78  
79      /**
80       * Name of the look and feel class.
81       *
82       * @parameter expression="${imagegenerator.lookAndFeel}"
83       * @required
84       */
85      private String lookAndFeel;
86  
87      /**
88       * Location of the output directory.
89       *
90       * @parameter expression="${imagegenerator.outputDirectory}"
91       *            default-value="${project.build.directory}/generated-site/resources/images"
92       */
93      private File outputDirectory;
94  
95      /**
96       * Location of the saved configuration file.
97       *
98       * @parameter expression="${imagegenerator.savedConfigFile}"
99       *            default-value="${project.build.directory}/generated-site/image-generator.xml"
100      */
101     private File savedConfigFile;
102 
103     /** A JPanel used for embedding the images. This is reused by each image. */
104     private JPanel panel;
105 
106     /**
107      * Set the config file.
108      *
109      * @param configFile the config file.
110      */
111     public void setConfigFile(File configFile) {
112         this.configFile = configFile;
113     }
114 
115     /**
116      * Set the look and feel.
117      *
118      * @param lookAndFeel the look and feel class name.
119      */
120     public void setLookAndFeel(String lookAndFeel) {
121         this.lookAndFeel = lookAndFeel;
122     }
123 
124     /**
125      * Set the output directory.
126      *
127      * @param outputDirectory the output directory.
128      */
129     public void setOutputDirectory(File outputDirectory) {
130         this.outputDirectory = outputDirectory;
131     }
132 
133     /**
134      * Set the saved config file.
135      *
136      * @param savedConfigFile the saved config file.
137      */
138     public void setSavedConfigFile(File savedConfigFile) {
139         this.savedConfigFile = savedConfigFile;
140     }
141 
142     /**
143      * @see org.apache.maven.plugin.AbstractMojo#execute()
144      */
145     public void execute() throws MojoExecutionException {
146         Map<String, ImageInfo> config    = parseConfigFile(configFile, true);
147         Map<String, ImageInfo> oldConfig = parseConfigFile(savedConfigFile, false);
148 
149         createOutputDirectoryIfNecessary();
150 
151         panel = null;
152 
153         generateImageFiles(config, oldConfig);
154 
155         copyConfigToOldConfig(configFile, savedConfigFile);
156     }
157 
158     /**
159      * Set the UI and create the JPanel if not already done. This allows us to
160      * only do this if we need to, saving execution time when no changes are
161      * made.
162      *
163      * @throws MojoExecutionException if unable to set the UI.
164      */
165     private void createUIIfNecessary() throws MojoExecutionException {
166         if (panel == null) {
167             try {
168                 UIManager.setLookAndFeel(lookAndFeel);
169             } catch (Exception e) {
170                 e.printStackTrace();
171                 throw new MojoExecutionException("Unable to set look and feel " + lookAndFeel, e);
172             }
173 
174             panel = new JPanel();
175             panel.setOpaque(true);
176         }
177     }
178 
179     /**
180      * Generate the image files, skipping any that are identical to a filename
181      * in the saved config file.
182      *
183      * @param  config    the current config file.
184      * @param  oldConfig the saved config file, for comparison.
185      *
186      * @throws MojoExecutionException if an error occurs.
187      */
188     private void generateImageFiles(Map<String, ImageInfo> config, Map<String, ImageInfo> oldConfig) throws MojoExecutionException {
189         for (String filename : config.keySet()) {
190             ImageInfo info    = config.get(filename);
191             ImageInfo oldInfo = oldConfig.get(filename);
192             File      file    = new File(outputDirectory, filename + ".png");
193 
194             if (oldInfo == null || !file.exists() || !info.equals(oldInfo)) {
195                 getLog().info("Creating image file " + filename);
196                 createUIIfNecessary();
197                 drawImage(file, info.className, info.width, info.height, info.panelWidth, info.panelHeight, info.args,
198                           info.properties);
199             }
200         }
201     }
202 
203     /**
204      * Create an image from the info and write it to a file.
205      *
206      * @param  file        the file to write the image to.
207      * @param  className   the class of control to be created, e.g.
208      *                     "javax.swing.JButton".
209      * @param  width       the desired width of the control.
210      * @param  height      the desired height of the control.
211      * @param  panelWidth  the desired width of the panel it is embedded in.
212      * @param  panelHeight the desired height of the panel it is embedded in.
213      * @param  args        any arguments to the control constructor.
214      * @param  properties  a map containing the client properties to set on the
215      *                     control.
216      *
217      * @throws MojoExecutionException if an error occurs.
218      */
219     private void drawImage(File file, String className, int width, int height, int panelWidth, int panelHeight, Object[] args,
220             Map<String, Object> properties) throws MojoExecutionException {
221         // Create the Swing object.
222         JComponent c = createSwingObject(className, args);
223 
224         // Set its properties.
225         for (String key : properties.keySet()) {
226             c.putClientProperty(key, properties.get(key));
227         }
228 
229         // Paint to a buffered image.
230         BufferedImage image = paintToBufferedImage(c, width, height, panelWidth, panelHeight);
231 
232         // Write the file.
233         writeImageFile(file, image);
234     }
235 
236     /**
237      * Create a Swing object from its class name and arguments.
238      *
239      * @param  className the class name.
240      * @param  args      the arguments. May be empty.
241      *
242      * @return the newly created Swing object.
243      *
244      * @throws MojoExecutionException if the Swing object cannot be created.
245      */
246     private JComponent createSwingObject(String className, Object... args) throws MojoExecutionException {
247         try {
248             Class<?> c = Class.forName(className);
249 
250             Class<?>[] argClasses = new Class[args.length];
251 
252             for (int i = 0; i < args.length; i++) {
253                 argClasses[i] = args[i].getClass();
254             }
255 
256             Constructor<?> constructor = c.getConstructor(argClasses);
257 
258             if (constructor == null) {
259                 throw new MojoExecutionException("Failed to find the constructor for the class: " + className);
260             }
261 
262             return (JComponent) constructor.newInstance(args);
263         } catch (Exception e) {
264             throw new MojoExecutionException("Unable to create the object " + className + "(" + args + ")", e);
265         }
266     }
267 
268     /**
269      * Paint the control to a newly created buffered image.
270      *
271      * @param  c           the control to paint.
272      * @param  width       the desired width of the control.
273      * @param  height      the desired height of the control.
274      * @param  panelWidth  the desired width of the panel it is embedded in.
275      * @param  panelHeight the desired height of the panel it is embedded in.
276      *
277      * @return the buffered image containing the printed control against a panel
278      *         background.
279      */
280     private BufferedImage paintToBufferedImage(JComponent c, int width, int height, int panelWidth, int panelHeight) {
281         panel.removeAll();
282         panel.setSize(panelWidth, panelHeight);
283 
284         panel.add(c);
285         c.setBounds((panelWidth - width) / 2, (panelHeight - height) / 2, width, height);
286 
287         BufferedImage image = new BufferedImage(panelWidth, panelHeight, BufferedImage.TYPE_INT_ARGB);
288         Graphics      g     = image.createGraphics();
289 
290         panel.paint(g);
291         return image;
292     }
293 
294     /**
295      * Write the buffered image to the file.
296      *
297      * @param  file  the file to write the image to.
298      * @param  image the buffered image.
299      *
300      * @throws MojoExecutionException if unable to write the file.
301      */
302     private void writeImageFile(File file, BufferedImage image) throws MojoExecutionException {
303         try {
304             ImageIO.write(image, "png", file);
305         } catch (IOException e) {
306             throw new MojoExecutionException("Error writing image file " + file, e);
307         }
308     }
309 
310     /**
311      * Parse an XML image config file into a hash of ImageInfo objects.
312      *
313      * @param  filename    the XML config file.
314      * @param  quitOnError {@code true} causes an exception to be thrown if an
315      *                     error occurs, {@code false} ignores the error and
316      *                     returns the list as it currently is.
317      *
318      * @return a map of ImageInfo objects, indexed by image filename.
319      *
320      * @throws MojoExecutionException if an error occurs and {@code quitOnError}
321      *                                is {@code true}.
322      */
323     private Map<String, ImageInfo> parseConfigFile(File filename, boolean quitOnError) throws MojoExecutionException {
324         Map<String, ImageInfo> list         = new HashMap<String, ImageInfo>();
325         InputStream            configStream = openInputStream(filename, quitOnError);
326         Document               doc          = null;
327 
328         if (configStream == null) {
329             // This only happens if quitOnError is false and the stream couldn't be opened.
330             return list;
331         }
332 
333         try {
334             DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
335             DocumentBuilder        db  = dbf.newDocumentBuilder();
336 
337             doc = db.parse(configStream);
338         } catch (Exception e) {
339             closeInputStream(configStream);
340             if (quitOnError) {
341                 throw new MojoExecutionException("Unable to parse XML config file " + filename, e);
342             }
343 
344             return list;
345         }
346 
347         doc.getDocumentElement().normalize();
348         NodeList nodeList = doc.getElementsByTagName("image");
349 
350         for (int i = 0; i < nodeList.getLength(); i++) {
351             Node imageNode = nodeList.item(i);
352 
353             if (imageNode.getNodeType() == Node.ELEMENT_NODE) {
354                 ImageInfo info      = new ImageInfo();
355                 String    imageFile = getImageInfo((Element) imageNode, info);
356 
357                 list.put(imageFile, info);
358             }
359         }
360 
361         closeInputStream(configStream);
362 
363         return list;
364     }
365 
366     /**
367      * Open an input stream from the filename.
368      *
369      * @param  filename    the file to create a stream from.
370      * @param  quitOnError {@code true} causes an exception to be thrown if an
371      *                     error occurs, {@code false} ignores the error and
372      *                     returns {@code null}.
373      *
374      * @return the input stream.
375      *
376      * @throws MojoExecutionException if an error occurs and {@code quitOnError}
377      *                                is {@code true}.
378      */
379     private InputStream openInputStream(File filename, boolean quitOnError) throws MojoExecutionException {
380         try {
381             return new FileInputStream(filename);
382         } catch (FileNotFoundException e) {
383             if (quitOnError) {
384                 throw new MojoExecutionException("Unable to open file \"" + filename, e);
385             }
386 
387             return null;
388         }
389     }
390 
391     /**
392      * Close the input stream.
393      *
394      * @param stream the stream to close
395      */
396     private void closeInputStream(InputStream stream) {
397         try {
398             stream.close();
399         } catch (IOException e) {
400             // Do nothing.
401         }
402     }
403 
404     /**
405      * Get the File for the output directory, creating it if necessary.
406      *
407      * @throws MojoExecutionException if for some reason the directory cannot be
408      *                                used, e.g. it is not a directory, or is
409      *                                not writable.
410      */
411     private void createOutputDirectoryIfNecessary() throws MojoExecutionException {
412         if (outputDirectory.exists()) {
413             if (!outputDirectory.isDirectory()) {
414                 throw new MojoExecutionException("Output directory \"" + outputDirectory + "\" exists, but is not a directory.");
415             } else if (!outputDirectory.canWrite()) {
416                 throw new MojoExecutionException("Output directory \"" + outputDirectory + "\" exists, but is not writable.");
417             }
418         } else if (!outputDirectory.mkdirs()) {
419             throw new MojoExecutionException("Output directory \"" + outputDirectory + "\" could not be created.");
420         }
421     }
422 
423     /**
424      * Copy the current config file to the saved config file.
425      *
426      * @param  configFilename    the current config file name.
427      * @param  oldConfigFilename the saved config file name.
428      *
429      * @throws MojoExecutionException if an error occurs.
430      */
431     private void copyConfigToOldConfig(File configFilename, File oldConfigFilename) throws MojoExecutionException {
432         BufferedReader reader = new BufferedReader(new InputStreamReader(openInputStream(configFilename, true)));
433 
434         BufferedWriter writer = null;
435 
436         try {
437             writer = new BufferedWriter(new FileWriter(oldConfigFilename));
438         } catch (IOException e) {
439             throw new MojoExecutionException("Unable to open for writing the file " + oldConfigFilename, e);
440         }
441 
442         try {
443             for (String line = null; (line = reader.readLine()) != null;) {
444                 writer.write(line);
445                 writer.write("\n");
446             }
447         } catch (IOException e) {
448             throw new MojoExecutionException("Unable to write " + oldConfigFilename, e);
449         } finally {
450             try {
451                 writer.close();
452             } catch (IOException e) {
453                 // Do nothing.
454             }
455         }
456     }
457 
458     /**
459      * Parse the XML for an image element to create the ImageInfo class for it.
460      *
461      * @param  imageElem the W3C Element containing the image information.
462      * @param  info      the ImageInfo object to fill with data from XML.
463      *
464      * @return the filename to write the image into.
465      *
466      * @throws MojoExecutionException if an error occurs.
467      */
468     private String getImageInfo(Element imageElem, ImageInfo info) throws MojoExecutionException {
469         String filename = imageElem.getAttribute("file");
470 
471         String w  = imageElem.getAttribute("width");
472         String h  = imageElem.getAttribute("height");
473         String pw = imageElem.getAttribute("panelWidth");
474         String ph = imageElem.getAttribute("panelHeight");
475 
476         if (pw.length() == 0) {
477             pw = w;
478         }
479 
480         if (ph.length() == 0) {
481             ph = h;
482         }
483 
484         info.className   = imageElem.getAttribute("class");
485         info.width       = Integer.parseInt(w);
486         info.height      = Integer.parseInt(h);
487         info.panelWidth  = Integer.parseInt(pw);
488         info.panelHeight = Integer.parseInt(ph);
489 
490         List<Object> argList = new ArrayList<Object>();
491 
492         NodeList nodeList = imageElem.getElementsByTagName("argument");
493 
494         for (int i = 0; i < nodeList.getLength(); i++) {
495             Node node = nodeList.item(i);
496 
497             if (node.getNodeType() == Node.ELEMENT_NODE) {
498                 Element argElem = (Element) node;
499                 String  type    = argElem.getAttribute("type");
500                 String  value   = argElem.getAttribute("value");
501 
502                 argList.add(parseObject(type, value));
503             }
504         }
505 
506         info.args = argList.toArray();
507 
508         info.properties = new HashMap<String, Object>();
509 
510         nodeList = imageElem.getElementsByTagName("clientProperty");
511 
512         for (int i = 0; i < nodeList.getLength(); i++) {
513             Node node = nodeList.item(i);
514 
515             if (node.getNodeType() == Node.ELEMENT_NODE) {
516                 Element argElem = (Element) node;
517                 String  name    = argElem.getAttribute("name");
518                 String  type    = argElem.getAttribute("type");
519                 String  value   = argElem.getAttribute("value");
520 
521                 info.properties.put(name, parseObject(type, value));
522             }
523         }
524 
525         info.args = argList.toArray();
526 
527         return filename;
528     }
529 
530     /**
531      * Parse the value as an Object, given its type and value.
532      *
533      * @param  type  the type, e.g. String, Integer, Float, or Double.
534      * @param  value the String value.
535      *
536      * @return the value as an Object of the specified type.
537      *
538      * @throws MojoExecutionException if the type is not recognized.
539      */
540     private Object parseObject(String type, String value) throws MojoExecutionException {
541         Object obj = null;
542 
543         if ("String".equals(type)) {
544             obj = value;
545         } else if ("Integer".equals(type)) {
546             obj = Integer.parseInt(value);
547         } else if ("Float".equals(type)) {
548             obj = Float.parseFloat(value);
549         } else if ("Double".equals(type)) {
550             obj = Double.parseDouble(value);
551         } else {
552             throw new MojoExecutionException("Unknown argument type: " + type);
553         }
554 
555         return obj;
556     }
557 
558     /**
559      * Information used to generate each image.
560      */
561     public static class ImageInfo {
562         String              className;
563         int                 width;
564         int                 height;
565         int                 panelWidth;
566         int                 panelHeight;
567         Object[]            args;
568         Map<String, Object> properties;
569 
570         /**
571          * @see java.lang.Object#equals(java.lang.Object)
572          */
573         @Override
574         public boolean equals(Object obj) {
575             if (!(obj instanceof ImageInfo)) {
576                 return false;
577             }
578 
579             ImageInfo other = (ImageInfo) obj;
580 
581             return (className.equals(other.className) && width == other.width && height == other.height && panelWidth == other.panelWidth
582                         && panelHeight == other.panelHeight && argsEquals(other) && properties.equals(other.properties));
583         }
584 
585         /**
586          * Compare args objects for equality.
587          *
588          * @param  other the other ImageInfo object.
589          *
590          * @return {@code true} if the two args have the same number and each
591          *         element is equal, {@code false} otherwise.
592          */
593         private boolean argsEquals(ImageInfo other) {
594             if (args.length != other.args.length) {
595                 return false;
596             }
597 
598             for (int i = 0; i < args.length; i++) {
599                 if (!args[i].equals(other.args[i])) {
600                     return false;
601                 }
602             }
603 
604             return true;
605         }
606     }
607 }