| /******************************************************************************* |
| * (c) Copyright 2015 l33t labs LLC and others. |
| * All rights reserved. This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License v1.0 |
| * which accompanies this distribution, and is available at |
| * http://www.eclipse.org/legal/epl-v10.html |
| * |
| * Contributors: |
| * l33t labs LLC and others - initial contribution |
| *******************************************************************************/ |
| |
| package org.eclipse.images.renderer; |
| |
| import java.awt.RenderingHints; |
| import java.awt.image.BufferedImage; |
| import java.io.ByteArrayInputStream; |
| import java.io.ByteArrayOutputStream; |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.FileWriter; |
| import java.io.IOException; |
| import java.io.OutputStream; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.concurrent.Callable; |
| import java.util.concurrent.ExecutorService; |
| import java.util.concurrent.Executors; |
| import java.util.concurrent.atomic.AtomicInteger; |
| |
| import javax.imageio.ImageIO; |
| |
| import org.apache.batik.dom.svg.SAXSVGDocumentFactory; |
| import org.apache.batik.gvt.renderer.ImageRenderer; |
| import org.apache.batik.transcoder.ErrorHandler; |
| import org.apache.batik.transcoder.TranscoderException; |
| import org.apache.batik.transcoder.TranscoderInput; |
| import org.apache.batik.transcoder.TranscoderOutput; |
| import org.apache.batik.transcoder.image.PNGTranscoder; |
| import org.apache.batik.util.XMLResourceDescriptor; |
| import org.apache.maven.plugin.AbstractMojo; |
| import org.apache.maven.plugin.MojoExecutionException; |
| import org.apache.maven.plugin.MojoFailureException; |
| import org.apache.maven.plugin.logging.Log; |
| import org.w3c.dom.Element; |
| import org.w3c.dom.svg.SVGDocument; |
| |
| import com.jhlabs.image.ContrastFilter; |
| import com.jhlabs.image.GrayscaleFilter; |
| import com.jhlabs.image.HSBAdjustFilter; |
| |
| /** |
| * <p>Mojo which renders SVG icons into PNG format.</p> |
| * |
| * @goal render-icons |
| * @phase generate-resources |
| */ |
| public class RenderMojo extends AbstractMojo { |
| |
| /** Maven logger */ |
| Log log; |
| |
| /** Used for high resolution (HiDPI) rendering support. */ |
| public static final String ECLIPSE_SVG_SCALE = "eclipse.svg.scale"; |
| |
| /** Used to specify the number of render threads when rasterizing icons. */ |
| public static final String RENDERTHREADS = "eclipse.svg.renderthreads"; |
| |
| /** Used to specify the directory name where the SVGs are taken from. */ |
| public static final String SOURCE_DIR = "eclipse.svg.sourcedirectory"; |
| |
| /** Used to specify the directory name where the PNGs are saved to. */ |
| public static final String TARGET_DIR = "eclipse.svg.targetdirectory"; |
| |
| /** A list of directories with svg sources to rasterize. */ |
| private List<IconEntry> icons; |
| |
| /** The pool used to render multiple icons concurrently. */ |
| private ExecutorService execPool; |
| |
| /** The number of threads to use when rendering icons. */ |
| private int threads; |
| |
| /** |
| * A counter used to keep track of the number of rendered icons. Atomic is |
| * used to make it easy to access between threads concurrently. |
| */ |
| private AtomicInteger counter; |
| |
| /** List of icons that failed to render, made safe for parallel access */ |
| List<IconEntry> failedIcons = Collections |
| .synchronizedList(new ArrayList<IconEntry>(5)); |
| |
| /** The amount of scaling to apply to rasterized images. */ |
| private double outputScale; |
| |
| /** |
| * @return the number of icons rendered at the time of the call |
| */ |
| public int getIconsRendered() { |
| return counter.get(); |
| } |
| |
| /** |
| * @return the number of icons that failed during the rendering process |
| */ |
| public int getFailedIcons() { |
| return failedIcons.size(); |
| } |
| |
| /** |
| * <p>Generates raster images from the input SVG vector image.</p> |
| * |
| * @param icon |
| * the icon to render |
| * @param grayFilter |
| * @param desaturator |
| * @param decontrast |
| */ |
| public void rasterize(IconEntry icon, GrayscaleFilter grayFilter, HSBAdjustFilter desaturator, ContrastFilter decontrast) { |
| if (icon == null) { |
| log.error("Null icon definition, skipping."); |
| failedIcons.add(icon); |
| return; |
| } |
| |
| if (icon.inputPath == null) { |
| log.error("Null icon input path, skipping: " |
| + icon.nameBase); |
| failedIcons.add(icon); |
| return; |
| } |
| |
| if (!icon.inputPath.exists()) { |
| log.error("Input path specified does not exist, skipping: " |
| + icon.nameBase); |
| failedIcons.add(icon); |
| return; |
| } |
| |
| if (icon.outputPath != null && !icon.outputPath.exists()) { |
| icon.outputPath.mkdirs(); |
| } |
| |
| if (icon.disabledPath != null && !icon.disabledPath.exists()) { |
| icon.disabledPath.mkdirs(); |
| } |
| |
| // Create the document to rasterize |
| SVGDocument svgDocument = generateSVGDocument(icon); |
| |
| if(svgDocument == null) { |
| return; |
| } |
| |
| // Determine the output sizes (native, double, quad) |
| // We render at quad size and resample down for output |
| Element svgDocumentNode = svgDocument.getDocumentElement(); |
| String nativeWidthStr = svgDocumentNode.getAttribute("width"); |
| String nativeHeightStr = svgDocumentNode.getAttribute("height"); |
| int nativeWidth = -1; |
| int nativeHeight = -1; |
| |
| try{ |
| if (!"".equals(nativeWidthStr) && !"".equals(nativeHeightStr)){ |
| nativeWidthStr = stripOffPx(nativeWidthStr); |
| nativeHeightStr = stripOffPx(nativeHeightStr); |
| nativeWidth = Integer.parseInt(nativeWidthStr); |
| nativeHeight = Integer.parseInt(nativeHeightStr); |
| } else { |
| // Vector graphics editing programs don't always output height and width attributes on SVG. |
| // As fall back: parse the viewBox attribute (which is almost always set). |
| String viewBoxStr = svgDocumentNode.getAttribute("viewBox"); |
| if ("".equals(viewBoxStr)){ |
| log.error("Icon defines neither width/height nor a viewBox, skipping: " + icon.nameBase); |
| failedIcons.add(icon); |
| return; |
| } |
| String[] splitted = viewBoxStr.split(" "); |
| if (splitted.length != 4){ |
| log.error("Dimension could not be parsed. Skipping: " + icon.nameBase); |
| failedIcons.add(icon); |
| return; |
| } |
| String widthStr = splitted[2]; |
| widthStr = stripOffPx(widthStr); |
| String heightStr = splitted[3]; |
| heightStr = stripOffPx(heightStr); |
| |
| nativeWidth = Integer.parseInt(widthStr); |
| nativeHeight = Integer.parseInt(heightStr); |
| } |
| }catch (NumberFormatException e){ |
| log.error("Dimension could not be parsed ( "+e.getMessage()+ "), skipping: " + icon.nameBase); |
| failedIcons.add(icon); |
| return; |
| } |
| |
| int outputWidth = (int) (nativeWidth * outputScale); |
| int outputHeight = (int) (nativeHeight * outputScale); |
| |
| // Guesstimate the PNG size in memory, BAOS will enlarge if necessary. |
| int outputInitSize = nativeWidth * nativeHeight * 4 + 1024; |
| ByteArrayOutputStream iconOutput = new ByteArrayOutputStream( |
| outputInitSize); |
| |
| // Render to SVG |
| try { |
| log.info(Thread.currentThread().getName() + " " |
| + " Rasterizing: " + icon.nameBase + ".png at " + outputWidth |
| + "x" + outputHeight); |
| |
| TranscoderInput svgInput = new TranscoderInput(svgDocument); |
| |
| boolean success = renderIcon(icon.nameBase, outputWidth, outputHeight, svgInput, iconOutput); |
| |
| if (!success) { |
| log.error("Failed to render icon: " + icon.nameBase + ".png, skipping."); |
| failedIcons.add(icon); |
| return; |
| } |
| } catch (Exception e) { |
| log.error("Failed to render icon: " + e.getMessage()); |
| failedIcons.add(icon); |
| return; |
| } |
| |
| // Generate a buffered image from Batik's png output |
| byte[] imageBytes = iconOutput.toByteArray(); |
| ByteArrayInputStream imageInputStream = new ByteArrayInputStream(imageBytes); |
| |
| BufferedImage inputImage = null; |
| try { |
| inputImage = ImageIO.read(imageInputStream); |
| |
| if(inputImage == null) { |
| log.error("Failed to generate BufferedImage from rendered icon, ImageIO returned null: " + icon.nameBase); |
| failedIcons.add(icon); |
| return; |
| } |
| } catch (IOException e2) { |
| log.error("Failed to generate BufferedImage from rendered icon: " + icon.nameBase + " - " + e2.getMessage()); |
| failedIcons.add(icon); |
| return; |
| } |
| |
| writeIcon(icon, outputWidth, outputHeight, inputImage); |
| |
| try { |
| if (icon.disabledPath != null) { |
| BufferedImage desaturated16 = desaturator.filter( |
| grayFilter.filter(inputImage, null), null); |
| |
| BufferedImage deconstrast = decontrast.filter(desaturated16, null); |
| |
| String outputName = getOutputName(icon.nameBase); |
| ImageIO.write(deconstrast, "PNG", new File(icon.disabledPath, outputName)); |
| } |
| } catch (Exception e1) { |
| log.error("Failed to render disabled icon: " + |
| icon.nameBase, e1); |
| failedIcons.add(icon); |
| } |
| } |
| |
| private String stripOffPx(String dimensionString) { |
| if (dimensionString.endsWith("px")){ |
| dimensionString = dimensionString.substring(0, dimensionString.length()-2); |
| } |
| return dimensionString; |
| } |
| |
| /** |
| * <p>Generates a Batik SVGDocument for the supplied IconEntry's input |
| * file.</p> |
| * |
| * @param icon the icon entry to generate an SVG document for |
| * |
| * @return a batik SVGDocument instance or null if one could not be generated |
| */ |
| private SVGDocument generateSVGDocument(IconEntry icon) { |
| // Load the document and find out the native height/width |
| // We reuse the document later for rasterization |
| SVGDocument svgDocument = null; |
| try { |
| FileInputStream iconDocumentStream = new FileInputStream(icon.inputPath); |
| |
| String parser = XMLResourceDescriptor.getXMLParserClassName(); |
| SAXSVGDocumentFactory f = new SAXSVGDocumentFactory(parser); |
| |
| // What kind of URI is batik expecting here??? the docs don't say |
| svgDocument = f.createSVGDocument("file://" + icon.nameBase + ".svg", iconDocumentStream); |
| } catch (Exception e3) { |
| log.error("Error parsing SVG icon document: " + e3.getMessage()); |
| failedIcons.add(icon); |
| return null; |
| } |
| return svgDocument; |
| } |
| |
| /** |
| * <p>Resizes the supplied inputImage to the specified width and height, using |
| * lanczos resampling techniques.</p> |
| * |
| * @param icon the icon that's being resized |
| * @param width the desired output width after rescaling operations |
| * @param height the desired output height after rescaling operations |
| * @param sourceImage the source image to resource |
| */ |
| private void writeIcon(IconEntry icon, int width, int height, BufferedImage sourceImage) { |
| try { |
| String outputName = getOutputName(icon.nameBase); |
| ImageIO.write(sourceImage, "PNG", new File(icon.outputPath, outputName)); |
| } catch (Exception e1) { |
| log.error("Failed to resize rendered icon to output size: " + |
| icon.nameBase, e1); |
| failedIcons.add(icon); |
| } |
| } |
| |
| private String getOutputName(String outputName) { |
| if (outputScale != 1) { |
| String scaleId = outputScale == (double) (int) outputScale ? Integer.toString((int) outputScale) |
| : Double.toString(outputScale); |
| outputName += "@" + scaleId + "x"; |
| } |
| outputName += ".png"; |
| return outputName; |
| } |
| |
| /** |
| * <p>Handles concurrently rasterizing the icons to |
| * reduce the duration on multicore systems.</p> |
| */ |
| public void rasterizeAll() { |
| // The number of icons that haven't been distributed to |
| // callables |
| int remainingIcons = icons.size(); |
| |
| // The number of icons to distribute to a rendering callable |
| final int threadExecSize = Math.max(1, icons.size() / this.threads); |
| |
| // The current offset to start a batch, as they're distributed |
| // between rendering callables |
| int batchOffset = 0; |
| |
| // A list of callables used to render icons on multiple threads |
| // Each callable gets a set of icons to render |
| List<Callable<Object>> tasks = new ArrayList<>( |
| this.threads); |
| |
| // Distribute the rasterization operations between multiple threads |
| while (remainingIcons > 0) { |
| // The current start index for the current batch |
| final int batchStart = batchOffset; |
| |
| // Increment the offset to reflect this batch (used for the next batch) |
| batchOffset += threadExecSize; |
| |
| // Determine this batch size, used for batches that have less than |
| // threadExecSize at the end of the distribution operation |
| int batchSize = 0; |
| |
| // Determine if we can fit a full batch in this callable |
| // or if we are at the end of gathered icons |
| if (remainingIcons > threadExecSize) { |
| batchSize = threadExecSize; |
| } else { |
| // We have less than a full batch worth of remaining icons |
| // just add them all |
| batchSize = remainingIcons; |
| } |
| |
| // Deincrement the remaining Icons |
| remainingIcons -= threadExecSize; |
| |
| // Used for access in the callable's scope |
| final int execCount = batchSize; |
| |
| // Create the callable and add it to the task pool |
| Callable<Object> runnable = new Callable<Object>() { |
| @Override |
| public Object call() throws Exception { |
| // The jhlabs filters are not thread safe, so provide one set per thread |
| GrayscaleFilter grayFilter = new GrayscaleFilter(); |
| |
| HSBAdjustFilter desaturator = new HSBAdjustFilter(); |
| desaturator.setSFactor(0.0f); |
| |
| ContrastFilter decontrast = new ContrastFilter(); |
| decontrast.setBrightness(2.9f); |
| decontrast.setContrast(0.2f); |
| |
| // Rasterize this batch |
| for (int count = 0; count < execCount; count++) { |
| rasterize(icons.get(batchStart + count), grayFilter, desaturator, decontrast); |
| } |
| |
| // Update the render counter |
| counter.getAndAdd(execCount); |
| log.info("Finished rendering batch, index: " + batchStart); |
| |
| return null; |
| } |
| }; |
| |
| tasks.add(runnable); |
| } |
| |
| // Execute the rasterization operations that |
| // have been added to the pool |
| try { |
| execPool.invokeAll(tasks); |
| } catch (InterruptedException e) { |
| // TODO Auto-generated catch block |
| e.printStackTrace(); |
| } |
| |
| // Print info about failed render operations, so they can be fixed |
| log.info("Failed Icon Count: " + failedIcons.size()); |
| for (IconEntry icon : failedIcons) { |
| log.info("Failed Icon: " + icon.nameBase); |
| } |
| |
| } |
| |
| /** |
| * Use batik to rasterize the input SVG into a raster image at the specified |
| * image dimensions. |
| * |
| * @param iconName |
| * @param width the width to render the icons at |
| * @param height the height to render the icon at |
| * @param tInput the SVG transcoder input |
| * @param stream the stream to write the PNG data to |
| */ |
| public boolean renderIcon(final String iconName, int width, int height, |
| TranscoderInput tInput, OutputStream stream) { |
| PNGTranscoder transcoder = new PNGTranscoder() { |
| protected ImageRenderer createRenderer() { |
| ImageRenderer renderer = super.createRenderer(); |
| |
| RenderingHints renderHints = renderer.getRenderingHints(); |
| |
| renderHints.add(new RenderingHints(RenderingHints.KEY_TEXT_ANTIALIASING, |
| RenderingHints.VALUE_TEXT_ANTIALIAS_OFF)); |
| |
| renderHints.add(new RenderingHints(RenderingHints.KEY_RENDERING, |
| RenderingHints.VALUE_RENDER_QUALITY)); |
| |
| renderHints.add(new RenderingHints(RenderingHints.KEY_DITHERING, |
| RenderingHints.VALUE_DITHER_DISABLE)); |
| |
| renderHints.add(new RenderingHints(RenderingHints.KEY_INTERPOLATION, |
| RenderingHints.VALUE_INTERPOLATION_BICUBIC)); |
| |
| renderHints.add(new RenderingHints(RenderingHints.KEY_ALPHA_INTERPOLATION, |
| RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY)); |
| |
| renderHints.add(new RenderingHints(RenderingHints.KEY_ANTIALIASING, |
| RenderingHints.VALUE_ANTIALIAS_ON)); |
| |
| renderHints.add(new RenderingHints(RenderingHints.KEY_COLOR_RENDERING, |
| RenderingHints.VALUE_COLOR_RENDER_QUALITY)); |
| |
| renderHints.add(new RenderingHints(RenderingHints.KEY_STROKE_CONTROL, |
| RenderingHints.VALUE_STROKE_PURE)); |
| |
| renderHints.add(new RenderingHints(RenderingHints.KEY_FRACTIONALMETRICS, |
| RenderingHints.VALUE_FRACTIONALMETRICS_ON)); |
| |
| renderer.setRenderingHints(renderHints); |
| |
| return renderer; |
| } |
| }; |
| |
| transcoder.addTranscodingHint(PNGTranscoder.KEY_WIDTH, new Float(width)); |
| transcoder.addTranscodingHint(PNGTranscoder.KEY_HEIGHT, new Float(height)); |
| |
| transcoder.setErrorHandler(new ErrorHandler() { |
| public void warning(TranscoderException arg0) |
| throws TranscoderException { |
| log.error("Icon: " + iconName + " - WARN: " + arg0.getMessage()); |
| } |
| |
| public void fatalError(TranscoderException arg0) |
| throws TranscoderException { |
| log.error("Icon: " + iconName + " - FATAL: " + arg0.getMessage()); |
| } |
| |
| public void error(TranscoderException arg0) |
| throws TranscoderException { |
| log.error("Icon: " + iconName + " - ERROR: " + arg0.getMessage()); |
| } |
| }); |
| |
| // Transcode the SVG document input to a PNG via the output stream |
| TranscoderOutput output = new TranscoderOutput(stream); |
| |
| try { |
| transcoder.transcode(tInput, output); |
| return true; |
| } catch (Exception e) { |
| e.printStackTrace(); |
| return false; |
| } finally { |
| try { |
| stream.close(); |
| } catch (IOException e) { |
| // TODO Auto-generated catch block |
| e.printStackTrace(); |
| } |
| } |
| } |
| |
| /** |
| * <p>Initializes rasterizer defaults</p> |
| * |
| * @param threads the number of threads to render with |
| * @param scale multiplier to use with icon output dimensions |
| */ |
| private void init(int threads, double scale) { |
| this.threads = threads; |
| this.outputScale = Math.max(1, scale); |
| icons = new ArrayList<>(); |
| execPool = Executors.newFixedThreadPool(threads); |
| counter = new AtomicInteger(); |
| } |
| |
| /** |
| * @see AbstractMojo#execute() |
| */ |
| public void execute() throws MojoExecutionException, MojoFailureException { |
| log = getLog(); |
| |
| // Default to 2x the number of processor cores but allow override via jvm arg |
| int threads = Math.max(1, Runtime.getRuntime().availableProcessors() * 2); |
| String threadStr = System.getProperty(RENDERTHREADS); |
| if (threadStr != null) { |
| try { |
| threads = Integer.parseInt(threadStr); |
| } catch (Exception e) { |
| e.printStackTrace(); |
| System.out |
| .println("Could not parse thread count, using default thread count"); |
| } |
| } |
| |
| // if high res is enabled, the icons output size will be scaled by iconScale |
| // Defaults to 1, meaning native size |
| double iconScale = 1; |
| String iconScaleStr = System.getProperty(ECLIPSE_SVG_SCALE); |
| if (iconScaleStr != null) { |
| iconScale = Double.parseDouble(iconScaleStr); |
| if (iconScale != 1 && iconScale != 1.5 && iconScale != 2) { |
| log.warn("Unusual scale factor: " + iconScaleStr + " (@" + iconScale + "x)"); |
| } |
| } |
| |
| // Defaults to "eclipse-svg" |
| String sourceDir = "eclipse-svg"; |
| String sourceDirProp = System.getProperty(SOURCE_DIR); |
| if (sourceDirProp != null) { |
| sourceDir = sourceDirProp; |
| } |
| |
| // Defaults to "eclipse-png" |
| String targetDir = "eclipse-png"; |
| String targetDirProp = System.getProperty(TARGET_DIR); |
| if (targetDirProp != null) { |
| targetDir = targetDirProp; |
| } |
| |
| // Track the time it takes to render the entire set |
| long totalStartTime = System.currentTimeMillis(); |
| |
| // initialize defaults (the old renderer was instantiated via constructor) |
| init(threads, iconScale); |
| |
| String workingDirectory = System.getProperty("user.dir"); |
| |
| File outputDir = new File(workingDirectory + (iconScale == 1 ? "/" + targetDir + "/" : "/" + targetDir + "-hidpi/")); |
| File iconDirectoryRoot = new File(sourceDir + "/"); |
| |
| if (!iconDirectoryRoot.exists()){ |
| log.error("Source directory' "+sourceDir+"' does not exist."); |
| return; |
| } |
| |
| // Search each subdir in the root dir for svg icons |
| for (File file : iconDirectoryRoot.listFiles()) { |
| if(!file.isDirectory()) { |
| continue; |
| } |
| |
| String dirName = file.getName(); |
| |
| // Where to place the rendered icon |
| File outputBase = new File(outputDir, (iconScale == 1 ? dirName : dirName + ".hidpi")); |
| if (iconScale != 1) { |
| createFragmentFiles(outputBase, dirName); |
| } |
| |
| IconGatherer.gatherIcons(icons, "svg", file, file, outputBase, true); |
| } |
| |
| log.info("Working directory: " + outputDir.getAbsolutePath()); |
| log.info("SVG Icon Directory: " + iconDirectoryRoot.getAbsolutePath()); |
| log.info("Rendering icons with " + threads + " threads, scaling output to " + iconScale + "x"); |
| long startTime = System.currentTimeMillis(); |
| |
| // Render the icons |
| rasterizeAll(); |
| |
| // Print summary of operations |
| int iconRendered = getIconsRendered(); |
| int failedIcons = getFailedIcons(); |
| int fullIconCount = iconRendered - failedIcons; |
| |
| log.info(fullIconCount + " Icons Rendered"); |
| log.info(failedIcons + " Icons Failed"); |
| log.info("Took: " + (System.currentTimeMillis() - startTime) + " ms."); |
| |
| log.info("Rasterization operations completed, Took: " |
| + (System.currentTimeMillis() - totalStartTime) + " ms."); |
| } |
| |
| private void createFragmentFiles(File outputBase, String dirName) { |
| createFile(new File(outputBase, "build.properties"), "bin.includes = META-INF/,icons/,.\n"); |
| createFile(new File(outputBase, ".project"), "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + |
| "<projectDescription>\n" + |
| " <name>" + dirName + ".hidpi</name>\n" + |
| " <comment></comment>\n" + |
| " <projects>\n" + |
| " </projects>\n" + |
| " <buildSpec>\n" + |
| " <buildCommand>\n" + |
| " <name>org.eclipse.pde.ManifestBuilder</name>\n" + |
| " <arguments>\n" + |
| " </arguments>\n" + |
| " </buildCommand>\n" + |
| " <buildCommand>\n" + |
| " <name>org.eclipse.pde.SchemaBuilder</name>\n" + |
| " <arguments>\n" + |
| " </arguments>\n" + |
| " </buildCommand>\n" + |
| " </buildSpec>\n" + |
| " <natures>\n" + |
| " <nature>org.eclipse.pde.PluginNature</nature>\n" + |
| " </natures>\n" + |
| "</projectDescription>\n"); |
| createFile(new File(outputBase, "META-INF/MANIFEST.MF"), "Manifest-Version: 1.0\n" + |
| "Bundle-ManifestVersion: 2\n" + |
| "Bundle-Name: " + dirName + ".hidpi\n" + |
| "Bundle-SymbolicName: " + dirName + ".hidpi\n" + |
| "Bundle-Version: 0.1.0.qualifier\n" + |
| "Fragment-Host: " + dirName + "\n"); |
| } |
| |
| private void createFile(File file, String contents) { |
| try { |
| file.getParentFile().mkdirs(); |
| FileWriter writer = new FileWriter(file); |
| writer.write(contents); |
| writer.close(); |
| } catch (IOException e) { |
| log.error(e); |
| } |
| } |
| |
| } |