View Javadoc

1   /*
2    * #%L
3    * Native ARchive plugin for Maven
4    * %%
5    * Copyright (C) 2002 - 2014 NAR Maven Plugin developers.
6    * %%
7    * Licensed under the Apache License, Version 2.0 (the "License");
8    * you may not use this file except in compliance with the License.
9    * You may obtain a copy of the License at
10   * 
11   * http://www.apache.org/licenses/LICENSE-2.0
12   * 
13   * Unless required by applicable law or agreed to in writing, software
14   * distributed under the License is distributed on an "AS IS" BASIS,
15   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   * See the License for the specific language governing permissions and
17   * limitations under the License.
18   * #L%
19   */
20  package com.github.maven_nar.cpptasks;
21  
22  import java.io.BufferedWriter;
23  import java.io.File;
24  import java.io.FileOutputStream;
25  import java.io.FileWriter;
26  import java.io.IOException;
27  import java.io.OutputStreamWriter;
28  import java.io.UnsupportedEncodingException;
29  import java.util.Enumeration;
30  import java.util.Hashtable;
31  import java.util.Map;
32  import java.util.Vector;
33  
34  import javax.xml.parsers.SAXParser;
35  import javax.xml.parsers.SAXParserFactory;
36  
37  import org.apache.tools.ant.BuildException;
38  import org.xml.sax.Attributes;
39  import org.xml.sax.SAXException;
40  import org.xml.sax.helpers.DefaultHandler;
41  
42  import com.github.maven_nar.cpptasks.compiler.ProcessorConfiguration;
43  
44  /**
45   * A history of the compiler and linker settings used to build the files in the
46   * same directory as the history.
47   *
48   * @author Curt Arnold
49   */
50  public final class TargetHistoryTable {
51    /**
52     * This class handles populates the TargetHistory hashtable in response to
53     * SAX parse events
54     */
55    private class TargetHistoryTableHandler extends DefaultHandler {
56      private final File baseDir;
57      private String config;
58      private final Hashtable<String, TargetHistory> history;
59      private String output;
60      private long outputLastModified;
61      private final Vector<SourceHistory> sources = new Vector<>();
62  
63      /**
64       * Constructor
65       *
66       * @param history
67       *          hashtable of TargetHistory keyed by output name
68       */
69      private TargetHistoryTableHandler(final Hashtable<String, TargetHistory> history, final File baseDir) {
70        this.history = history;
71        this.config = null;
72        this.output = null;
73        this.baseDir = baseDir;
74      }
75  
76      @Override
77      public void endElement(final String namespaceURI, final String localName, final String qName) throws SAXException {
78        //
79        // if </target> then
80        // create TargetHistory object and add to hashtable
81        // if corresponding output file exists and
82        // has the same timestamp
83        //
84        if (qName.equals("target")) {
85          if (this.config != null && this.output != null) {
86            final File existingFile = new File(this.baseDir, this.output);
87            //
88            // if the corresponding files doesn't exist or has a
89            // different
90            // modification time, then discard this record
91            if (existingFile.exists()) {
92              //
93              // would have expected exact time stamps
94              // but have observed slight differences
95              // in return value for multiple evaluations of
96              // lastModified(). Check if times are within
97              // a second
98              final long existingLastModified = existingFile.lastModified();
99              if (!CUtil.isSignificantlyBefore(existingLastModified, this.outputLastModified)
100                 && !CUtil.isSignificantlyAfter(existingLastModified, this.outputLastModified)) {
101               final SourceHistory[] sourcesArray = new SourceHistory[this.sources.size()];
102               this.sources.copyInto(sourcesArray);
103               final TargetHistory targetHistory = new TargetHistory(this.config, this.output, this.outputLastModified,
104                   sourcesArray);
105               this.history.put(this.output, targetHistory);
106             }
107           }
108         }
109         this.output = null;
110         this.sources.setSize(0);
111       } else {
112         //
113         // reset config so targets not within a processor element
114         // don't pick up a previous processors signature
115         //
116         if (qName.equals("processor")) {
117           this.config = null;
118         }
119       }
120     }
121 
122     /**
123      * startElement handler
124      */
125     @Override
126     public void startElement(final String namespaceURI, final String localName, final String qName,
127         final Attributes atts) throws SAXException {
128       //
129       // if sourceElement
130       //
131       if (qName.equals("source")) {
132         final String sourceFile = atts.getValue("file");
133         final long sourceLastModified = Long.parseLong(atts.getValue("lastModified"), 16);
134         this.sources.addElement(new SourceHistory(sourceFile, sourceLastModified));
135       } else {
136         //
137         // if <target> element,
138         // grab file name and lastModified values
139         // TargetHistory object will be created in endElement
140         //
141         if (qName.equals("target")) {
142           this.sources.setSize(0);
143           this.output = atts.getValue("file");
144           this.outputLastModified = Long.parseLong(atts.getValue("lastModified"), 16);
145         } else {
146           //
147           // if <processor> element,
148           // grab signature attribute
149           //
150           if (qName.equals("processor")) {
151             this.config = atts.getValue("signature");
152           }
153         }
154       }
155     }
156   }
157 
158   /**
159    * Flag indicating whether the cache should be written back to file.
160    */
161   private boolean dirty;
162   /**
163    * a hashtable of TargetHistory's keyed by output file name
164    */
165   private final Hashtable<String, TargetHistory> history = new Hashtable<>();
166   /**
167    * The file the cache was loaded from.
168    */
169   private final/* final */File historyFile;
170   private final/* final */File outputDir;
171   private String outputDirPath;
172 
173   /**
174    * Creates a target history table from history.xml in the output directory,
175    * if it exists. Otherwise, initializes the history table empty.
176    *
177    * @param task
178    *          task used for logging history load errors
179    * @param outputDir
180    *          output directory for task
181    */
182   public TargetHistoryTable(final CCTask task, final File outputDir) throws BuildException
183 
184   {
185     if (outputDir == null) {
186       throw new NullPointerException("outputDir");
187     }
188     if (!outputDir.isDirectory()) {
189       throw new BuildException("Output directory is not a directory");
190     }
191     if (!outputDir.exists()) {
192       throw new BuildException("Output directory does not exist");
193     }
194     this.outputDir = outputDir;
195     try {
196       this.outputDirPath = outputDir.getCanonicalPath();
197     } catch (final IOException ex) {
198       this.outputDirPath = outputDir.toString();
199     }
200     //
201     // load any existing history from file
202     // suppressing any records whose corresponding
203     // file does not exist, is zero-length or
204     // last modified dates differ
205     this.historyFile = new File(outputDir, "history.xml");
206 
207     if (this.historyFile.exists()) {
208       final SAXParserFactory factory = SAXParserFactory.newInstance();
209       factory.setValidating(false);
210       try {
211         final SAXParser parser = factory.newSAXParser();
212         parser.parse(this.historyFile, new TargetHistoryTableHandler(this.history, outputDir));
213       } catch (final Exception ex) {
214         //
215         // a failure on loading this history is not critical
216         // but should be logged
217         task.log("Error reading history.xml: " + ex.toString());
218       }
219     } else {
220       //
221       // create empty history file for identifying new files by last
222       // modified
223       // timestamp comperation (to compare with
224       // System.currentTimeMillis() don't work on Unix, because it
225       // maesure timestamps only in seconds).
226       // try {
227 
228       try {
229         final File temp = File.createTempFile("history.xml", Long.toString(System.nanoTime()), outputDir);
230         try (FileWriter writer = new FileWriter(temp)) {
231           writer.write("<history/>");
232         }
233         if (!temp.renameTo(this.historyFile)) {
234           throw new IOException("Could not rename " + temp + " to " + this.historyFile);
235         }
236       } catch (final IOException ex) {
237         throw new BuildException("Can't create history file", ex);
238       }
239     }
240   }
241 
242   public void commit() throws IOException {
243     //
244     // if not dirty, no need to update file
245     //
246     if (this.dirty) {
247       //
248       // build (small) hashtable of config id's in history
249       //
250       final Hashtable<String, String> configs = new Hashtable<>(20);
251       Enumeration<TargetHistory> elements = this.history.elements();
252       while (elements.hasMoreElements()) {
253         final TargetHistory targetHistory = elements.nextElement();
254         final String configId = targetHistory.getProcessorConfiguration();
255         if (configs.get(configId) == null) {
256           configs.put(configId, configId);
257         }
258       }
259       final FileOutputStream outStream = new FileOutputStream(this.historyFile);
260       OutputStreamWriter outWriter;
261       //
262       // early VM's don't support UTF-8 encoding
263       // try and fallback to the default encoding
264       // otherwise
265       String encodingName = "UTF-8";
266       try {
267         outWriter = new OutputStreamWriter(outStream, "UTF-8");
268       } catch (final UnsupportedEncodingException ex) {
269         outWriter = new OutputStreamWriter(outStream);
270         encodingName = outWriter.getEncoding();
271       }
272       final BufferedWriter writer = new BufferedWriter(outWriter);
273       writer.write("<?xml version='1.0' encoding='");
274       writer.write(encodingName);
275       writer.write("'?>\n");
276       writer.write("<history>\n");
277       final StringBuffer buf = new StringBuffer(200);
278       final Enumeration<String> configEnum = configs.elements();
279       while (configEnum.hasMoreElements()) {
280         final String configId = configEnum.nextElement();
281         buf.setLength(0);
282         buf.append("   <processor signature=\"");
283         buf.append(CUtil.xmlAttribEncode(configId));
284         buf.append("\">\n");
285         writer.write(buf.toString());
286         elements = this.history.elements();
287         while (elements.hasMoreElements()) {
288           final TargetHistory targetHistory = elements.nextElement();
289           if (targetHistory.getProcessorConfiguration().equals(configId)) {
290             buf.setLength(0);
291             buf.append("      <target file=\"");
292             buf.append(CUtil.xmlAttribEncode(targetHistory.getOutput()));
293             buf.append("\" lastModified=\"");
294             buf.append(Long.toHexString(targetHistory.getOutputLastModified()));
295             buf.append("\">\n");
296             writer.write(buf.toString());
297             final SourceHistory[] sourceHistories = targetHistory.getSources();
298             for (final SourceHistory sourceHistorie : sourceHistories) {
299               buf.setLength(0);
300               buf.append("         <source file=\"");
301               buf.append(CUtil.xmlAttribEncode(sourceHistorie.getRelativePath()));
302               buf.append("\" lastModified=\"");
303               buf.append(Long.toHexString(sourceHistorie.getLastModified()));
304               buf.append("\"/>\n");
305               writer.write(buf.toString());
306             }
307             writer.write("      </target>\n");
308           }
309         }
310         writer.write("   </processor>\n");
311       }
312       writer.write("</history>\n");
313       writer.close();
314       this.dirty = false;
315     }
316   }
317 
318   public TargetHistory get(final String configId, final String outputName) {
319     TargetHistory targetHistory = this.history.get(outputName);
320     if (targetHistory != null) {
321       if (!targetHistory.getProcessorConfiguration().equals(configId)) {
322         targetHistory = null;
323       }
324     }
325     return targetHistory;
326   }
327 
328   public File getHistoryFile() {
329     return this.historyFile;
330   }
331 
332   public void markForRebuild(final Map<String, TargetInfo> targetInfos) {
333     for (final TargetInfo targetInfo : targetInfos.values()) {
334       markForRebuild(targetInfo);
335     }
336   }
337 
338   // FREEHEP added synchronized
339   public synchronized void markForRebuild(final TargetInfo targetInfo) {
340     //
341     // if it must already be rebuilt, no need to check further
342     //
343     if (!targetInfo.getRebuild()) {
344       final TargetHistory history = get(targetInfo.getConfiguration().toString(), targetInfo.getOutput().getName());
345       if (history == null) {
346         targetInfo.mustRebuild();
347       } else {
348         final SourceHistory[] sourceHistories = history.getSources();
349         final File[] sources = targetInfo.getSources();
350         if (sourceHistories.length != sources.length) {
351           targetInfo.mustRebuild();
352         } else {
353           final Hashtable<String, File> sourceMap = new Hashtable<>(sources.length);
354           for (final File source : sources) {
355             try {
356               sourceMap.put(source.getCanonicalPath(), source);
357             } catch (final IOException ex) {
358               sourceMap.put(source.getAbsolutePath(), source);
359             }
360           }
361           for (final SourceHistory sourceHistorie : sourceHistories) {
362             //
363             // relative file name, must absolutize it on output
364             // directory
365             //
366             final String absPath = sourceHistorie.getAbsolutePath(this.outputDir);
367             File match = sourceMap.get(absPath);
368             if (match != null) {
369               try {
370                 match = sourceMap.get(new File(absPath).getCanonicalPath());
371               } catch (final IOException ex) {
372                 targetInfo.mustRebuild();
373                 break;
374               }
375             }
376             if (match == null || match.lastModified() != sourceHistorie.getLastModified()) {
377               targetInfo.mustRebuild();
378               break;
379             }
380           }
381         }
382       }
383     }
384   }
385 
386   public void update(final ProcessorConfiguration config, final String[] sources, final VersionInfo versionInfo) {
387     final String configId = config.getIdentifier();
388     final String[] onesource = new String[1];
389     String[] outputNames;
390     for (final String source : sources) {
391       onesource[0] = source;
392       outputNames = config.getOutputFileNames(source, versionInfo);
393       for (final String outputName : outputNames) {
394         update(configId, outputName, onesource);
395       }
396     }
397   }
398 
399   // FREEHEP added synchronized
400   private synchronized void update(final String configId, final String outputName, final String[] sources) {
401     final File outputFile = new File(this.outputDir, outputName);
402     //
403     // if output file doesn't exist or predates the start of the
404     // compile step (most likely a compilation error) then
405     // do not write add a history entry
406     //
407     if (outputFile.exists() && !CUtil.isSignificantlyBefore(outputFile.lastModified(), this.historyFile.lastModified())) {
408       this.dirty = true;
409       this.history.remove(outputName);
410       final SourceHistory[] sourceHistories = new SourceHistory[sources.length];
411       for (int i = 0; i < sources.length; i++) {
412         final File sourceFile = new File(sources[i]);
413         final long lastModified = sourceFile.lastModified();
414         final String relativePath = CUtil.getRelativePath(this.outputDirPath, sourceFile);
415         sourceHistories[i] = new SourceHistory(relativePath, lastModified);
416       }
417       final TargetHistory newHistory = new TargetHistory(configId, outputName, outputFile.lastModified(),
418           sourceHistories);
419       this.history.put(outputName, newHistory);
420     }
421   }
422 
423   // FREEHEP added synchronized
424   public synchronized void update(final TargetInfo linkTarget) {
425     final File outputFile = linkTarget.getOutput();
426     final String outputName = outputFile.getName();
427     //
428     // if output file doesn't exist or predates the start of the
429     // compile or link step (most likely a compilation error) then
430     // do not write add a history entry
431     //
432     if (outputFile.exists() && !CUtil.isSignificantlyBefore(outputFile.lastModified(), this.historyFile.lastModified())) {
433       this.dirty = true;
434       this.history.remove(outputName);
435       final SourceHistory[] sourceHistories = linkTarget.getSourceHistories(this.outputDirPath);
436       final TargetHistory newHistory = new TargetHistory(linkTarget.getConfiguration().getIdentifier(), outputName,
437           outputFile.lastModified(), sourceHistories);
438       this.history.put(outputName, newHistory);
439     }
440   }
441 }