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.IOException;
26  import java.io.OutputStreamWriter;
27  import java.io.UnsupportedEncodingException;
28  import java.util.Enumeration;
29  import java.util.Hashtable;
30  import java.util.Vector;
31  
32  import javax.xml.parsers.ParserConfigurationException;
33  import javax.xml.parsers.SAXParser;
34  import javax.xml.parsers.SAXParserFactory;
35  
36  import org.apache.tools.ant.BuildException;
37  import org.apache.tools.ant.Project;
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.CompilerConfiguration;
43  
44  /**
45   * @author Curt Arnold
46   */
47  public final class DependencyTable {
48    /**
49     * This class handles populates the TargetHistory hashtable in response to
50     * SAX parse events
51     */
52    private class DependencyTableHandler extends DefaultHandler {
53      private final File baseDir;
54      private final DependencyTable dependencyTable;
55      private String includePath;
56      private final Vector includes;
57      private String source;
58      private long sourceLastModified;
59      private final Vector sysIncludes;
60  
61      /**
62       * Constructor
63       *
64       * @param history
65       *          hashtable of TargetHistory keyed by output name
66       * @param outputFiles
67       *          existing files in output directory
68       */
69      private DependencyTableHandler(final DependencyTable dependencyTable, final File baseDir) {
70        this.dependencyTable = dependencyTable;
71        this.baseDir = baseDir;
72        this.includes = new Vector();
73        this.sysIncludes = new Vector();
74        this.source = null;
75      }
76  
77      @Override
78      public void endElement(final String namespaceURI, final String localName, final String qName) throws SAXException {
79        //
80        // if </source> then
81        // create Dependency object and add to hashtable
82        // if corresponding source file exists and
83        // has the same timestamp
84        //
85        if (qName.equals("source")) {
86          if (this.source != null && this.includePath != null) {
87            final File existingFile = new File(this.baseDir, this.source);
88            //
89            // if the file exists and the time stamp is right
90            // preserve the dependency info
91            if (existingFile.exists()) {
92              //
93              // would have expected exact matches
94              // but was seeing some unexpected difference by
95              // a few tens of milliseconds, as long
96              // as the times are within a second
97              final long existingLastModified = existingFile.lastModified();
98              if (!CUtil.isSignificantlyAfter(existingLastModified, this.sourceLastModified)
99                  && !CUtil.isSignificantlyBefore(existingLastModified, this.sourceLastModified)) {
100               final DependencyInfo dependInfo = new DependencyInfo(this.includePath, this.source,
101                   this.sourceLastModified, this.includes, this.sysIncludes);
102               this.dependencyTable.putDependencyInfo(this.source, dependInfo);
103             }
104           }
105           this.source = null;
106           this.includes.setSize(0);
107         }
108       } else {
109         //
110         // this causes any <source> elements outside the
111         // scope of an <includePath> to be discarded
112         //
113         if (qName.equals("includePath")) {
114           this.includePath = null;
115         }
116       }
117     }
118 
119     /**
120      * startElement handler
121      */
122     @Override
123     public void startElement(final String namespaceURI, final String localName, final String qName,
124         final Attributes atts) throws SAXException {
125       //
126       // if includes, then add relative file name to vector
127       //
128       if (qName.equals("include")) {
129         this.includes.addElement(atts.getValue("file"));
130       } else {
131         if (qName.equals("sysinclude")) {
132           this.sysIncludes.addElement(atts.getValue("file"));
133         } else {
134           //
135           // if source then
136           // capture source file name,
137           // modification time and reset includes vector
138           //
139           if (qName.equals("source")) {
140             this.source = atts.getValue("file");
141             this.sourceLastModified = Long.parseLong(atts.getValue("lastModified"), 16);
142             this.includes.setSize(0);
143             this.sysIncludes.setSize(0);
144           } else {
145             if (qName.equals("includePath")) {
146               this.includePath = atts.getValue("signature");
147             }
148           }
149         }
150       }
151     }
152   }
153 
154   public abstract class DependencyVisitor {
155     /**
156      * Previews all the children of this source file.
157      *
158      * May be called multiple times as DependencyInfo's for children are
159      * filled in.
160      *
161      * @return true to continue towards recursion into included files
162      */
163     public abstract boolean preview(DependencyInfo parent, DependencyInfo[] children);
164 
165     /**
166      * Called if the dependency depth exhausted the stack.
167      */
168     public abstract void stackExhausted();
169 
170     /**
171      * Visits the dependency info.
172      *
173      * @return true to continue towards recursion into included files
174      */
175     public abstract boolean visit(DependencyInfo dependInfo);
176   }
177 
178   public class TimestampChecker extends DependencyVisitor {
179     private boolean noNeedToRebuild;
180     private final long outputLastModified;
181     private final boolean rebuildOnStackExhaustion;
182 
183     public TimestampChecker(final long outputLastModified, final boolean rebuildOnStackExhaustion) {
184       this.outputLastModified = outputLastModified;
185       this.noNeedToRebuild = true;
186       this.rebuildOnStackExhaustion = rebuildOnStackExhaustion;
187     }
188 
189     public boolean getMustRebuild() {
190       return !this.noNeedToRebuild;
191     }
192 
193     @Override
194     public boolean preview(final DependencyInfo parent, final DependencyInfo[] children) {
195       // BEGINFREEHEP
196       // int withCompositeTimes = 0;
197       // long parentCompositeLastModified = parent.getSourceLastModified();
198       // ENDFREEHEP
199       for (final DependencyInfo element : children) {
200         if (element != null) {
201           //
202           // expedient way to determine if a child forces us to
203           // rebuild
204           //
205           visit(element);
206           // BEGINFREEHEP
207           // long childCompositeLastModified = children[i]
208           // .getCompositeLastModified();
209           // if (childCompositeLastModified != Long.MIN_VALUE) {
210           // withCompositeTimes++;
211           // if (childCompositeLastModified > parentCompositeLastModified) {
212           // parentCompositeLastModified = childCompositeLastModified;
213           // }
214           // }
215           // ENDFREEHEP
216         }
217       }
218       // BEGINFREEHEP
219       // if (withCompositeTimes == children.length) {
220       // parent.setCompositeLastModified(parentCompositeLastModified);
221       // }
222       // ENDFREEHEP
223       //
224       // may have been changed by an earlier call to visit()
225       //
226       return this.noNeedToRebuild;
227     }
228 
229     @Override
230     public void stackExhausted() {
231       if (this.rebuildOnStackExhaustion) {
232         this.noNeedToRebuild = false;
233       }
234     }
235 
236     @Override
237     public boolean visit(final DependencyInfo dependInfo) {
238       if (this.noNeedToRebuild) {
239         if (CUtil.isSignificantlyAfter(dependInfo.getSourceLastModified(), this.outputLastModified)) {
240           // FREEHEP
241           // ||
242           // CUtil.isSignificantlyAfter(dependInfo.getCompositeLastModified(),
243           // outputLastModified)) {
244           this.noNeedToRebuild = false;
245         }
246       }
247       //
248       // only need to process the children if
249       // it has not yet been determined whether
250       // we need to rebuild and the composite modified time
251       // has not been determined for this file
252       return this.noNeedToRebuild;
253       // FREEHEP
254       // && dependInfo.getCompositeLastModified() == Long.MIN_VALUE;
255     }
256   }
257 
258   private final/* final */File baseDir;
259   private String baseDirPath;
260   /**
261    * a hashtable of DependencyInfo[] keyed by output file name
262    */
263   private final Hashtable dependencies = new Hashtable();
264   /** The file the cache was loaded from. */
265   private final/* final */File dependenciesFile;
266   /** Flag indicating whether the cache should be written back to file. */
267   private boolean dirty;
268 
269   /**
270    * Creates a target history table from dependencies.xml in the prject
271    * directory, if it exists. Otherwise, initializes the dependencies empty.
272    *
273    * @param baseDir
274    *          output directory for task
275    */
276   public DependencyTable(final File baseDir) {
277     if (baseDir == null) {
278       throw new NullPointerException("baseDir");
279     }
280     this.baseDir = baseDir;
281     try {
282       this.baseDirPath = baseDir.getCanonicalPath();
283     } catch (final IOException ex) {
284       this.baseDirPath = baseDir.toString();
285     }
286     this.dirty = false;
287     //
288     // load any existing dependencies from file
289     this.dependenciesFile = new File(baseDir, "dependencies.xml");
290   }
291 
292   public void commit(final CCTask task) {
293     //
294     // if not dirty, no need to update file
295     //
296     if (this.dirty) {
297       //
298       // walk through dependencies to get vector of include paths
299       // identifiers
300       //
301       final Vector includePaths = getIncludePaths();
302       //
303       //
304       // write dependency file
305       //
306       try {
307         final FileOutputStream outStream = new FileOutputStream(this.dependenciesFile);
308         OutputStreamWriter streamWriter;
309         //
310         // Early VM's may not have UTF-8 support
311         // fallback to default code page which
312         // "should" be okay unless there are
313         // non ASCII file names
314         String encodingName = "UTF-8";
315         try {
316           streamWriter = new OutputStreamWriter(outStream, "UTF-8");
317         } catch (final UnsupportedEncodingException ex) {
318           streamWriter = new OutputStreamWriter(outStream);
319           encodingName = streamWriter.getEncoding();
320         }
321         final BufferedWriter writer = new BufferedWriter(streamWriter);
322         writer.write("<?xml version='1.0' encoding='");
323         writer.write(encodingName);
324         writer.write("'?>\n");
325         writer.write("<dependencies>\n");
326         final StringBuffer buf = new StringBuffer();
327         final Enumeration includePathEnum = includePaths.elements();
328         while (includePathEnum.hasMoreElements()) {
329           writeIncludePathDependencies((String) includePathEnum.nextElement(), writer, buf);
330         }
331         writer.write("</dependencies>\n");
332         writer.close();
333         this.dirty = false;
334       } catch (final IOException ex) {
335         task.log("Error writing " + this.dependenciesFile.toString() + ":" + ex.toString());
336       }
337     }
338   }
339 
340   /**
341    * Returns an enumerator of DependencyInfo's
342    */
343   public Enumeration elements() {
344     return this.dependencies.elements();
345   }
346 
347   /**
348    * This method returns a DependencyInfo for the specific source file and
349    * include path identifier
350    *
351    */
352   public DependencyInfo getDependencyInfo(final String sourceRelativeName, final String includePathIdentifier) {
353     DependencyInfo dependInfo = null;
354     final DependencyInfo[] dependInfos = (DependencyInfo[]) this.dependencies.get(sourceRelativeName);
355     if (dependInfos != null) {
356       for (final DependencyInfo dependInfo2 : dependInfos) {
357         dependInfo = dependInfo2;
358         if (dependInfo.getIncludePathIdentifier().equals(includePathIdentifier)) {
359           return dependInfo;
360         }
361       }
362     }
363     return null;
364   }
365 
366   private Vector getIncludePaths() {
367     final Vector includePaths = new Vector();
368     DependencyInfo[] dependInfos;
369     final Enumeration dependenciesEnum = this.dependencies.elements();
370     while (dependenciesEnum.hasMoreElements()) {
371       dependInfos = (DependencyInfo[]) dependenciesEnum.nextElement();
372       for (final DependencyInfo dependInfo : dependInfos) {
373         boolean matchesExisting = false;
374         final String dependIncludePath = dependInfo.getIncludePathIdentifier();
375         final Enumeration includePathEnum = includePaths.elements();
376         while (includePathEnum.hasMoreElements()) {
377           if (dependIncludePath.equals(includePathEnum.nextElement())) {
378             matchesExisting = true;
379             break;
380           }
381         }
382         if (!matchesExisting) {
383           includePaths.addElement(dependIncludePath);
384         }
385       }
386     }
387     return includePaths;
388   }
389 
390   public void load() throws IOException, ParserConfigurationException, SAXException {
391     this.dependencies.clear();
392     if (this.dependenciesFile.exists()) {
393       final SAXParserFactory factory = SAXParserFactory.newInstance();
394       factory.setValidating(false);
395       final SAXParser parser = factory.newSAXParser();
396       parser.parse(this.dependenciesFile, new DependencyTableHandler(this, this.baseDir));
397       this.dirty = false;
398     }
399   }
400 
401   /**
402    * Determines if the specified target needs to be rebuilt.
403    *
404    * This task may result in substantial IO as files are parsed to determine
405    * their dependencies
406    */
407   public boolean needsRebuild(final CCTask task, final TargetInfo target, final int dependencyDepth) {
408     // look at any files where the compositeLastModified
409     // is not known, but the includes are known
410     //
411     boolean mustRebuild = false;
412     final CompilerConfiguration compiler = (CompilerConfiguration) target.getConfiguration();
413     final String includePathIdentifier = compiler.getIncludePathIdentifier();
414     final File[] sources = target.getSources();
415     final DependencyInfo[] dependInfos = new DependencyInfo[sources.length];
416     final long outputLastModified = target.getOutput().lastModified();
417     //
418     // try to solve problem using existing dependency info
419     // (not parsing any new files)
420     //
421     DependencyInfo[] stack = new DependencyInfo[50];
422     boolean rebuildOnStackExhaustion = true;
423     if (dependencyDepth >= 0) {
424       if (dependencyDepth < 50) {
425         stack = new DependencyInfo[dependencyDepth];
426       }
427       rebuildOnStackExhaustion = false;
428     }
429     final TimestampChecker checker = new TimestampChecker(outputLastModified, rebuildOnStackExhaustion);
430     for (int i = 0; i < sources.length && !mustRebuild; i++) {
431       final File source = sources[i];
432       final String relative = CUtil.getRelativePath(this.baseDirPath, source);
433       DependencyInfo dependInfo = getDependencyInfo(relative, includePathIdentifier);
434       if (dependInfo == null) {
435         task.log("Parsing " + relative, Project.MSG_VERBOSE);
436         dependInfo = parseIncludes(task, compiler, source);
437       }
438       walkDependencies(task, dependInfo, compiler, stack, checker);
439       mustRebuild = checker.getMustRebuild();
440     }
441     return mustRebuild;
442   }
443 
444   public DependencyInfo parseIncludes(final CCTask task, final CompilerConfiguration compiler, final File source) {
445     final DependencyInfo dependInfo = compiler.parseIncludes(task, this.baseDir, source);
446     final String relativeSource = CUtil.getRelativePath(this.baseDirPath, source);
447     putDependencyInfo(relativeSource, dependInfo);
448     return dependInfo;
449   }
450 
451   private void putDependencyInfo(final String key, final DependencyInfo dependInfo) {
452     //
453     // optimistic, add new value
454     //
455     final DependencyInfo[] old = (DependencyInfo[]) this.dependencies.put(key, new DependencyInfo[] {
456       dependInfo
457     });
458     this.dirty = true;
459     //
460     // something was already there
461     //
462     if (old != null) {
463       //
464       // see if the include path matches a previous entry
465       // if so replace it
466       final String includePathIdentifier = dependInfo.getIncludePathIdentifier();
467       for (int i = 0; i < old.length; i++) {
468         final DependencyInfo oldDepend = old[i];
469         if (oldDepend.getIncludePathIdentifier().equals(includePathIdentifier)) {
470           old[i] = dependInfo;
471           this.dependencies.put(key, old);
472           return;
473         }
474       }
475       //
476       // no match prepend the new entry to the array
477       // of dependencies for the file
478       final DependencyInfo[] combined = new DependencyInfo[old.length + 1];
479       combined[0] = dependInfo;
480       System.arraycopy(old, 0, combined, 1, old.length);
481       this.dependencies.put(key, combined);
482     }
483     return;
484   }
485 
486   public void walkDependencies(final CCTask task, final DependencyInfo dependInfo,
487       final CompilerConfiguration compiler, final DependencyInfo[] stack, final DependencyVisitor visitor)
488       throws BuildException {
489     // BEGINFREEHEP
490     if (dependInfo.hasTag(visitor)) {
491       return;
492     }
493     dependInfo.setTag(visitor);
494     // ENDFREEHEP
495     //
496     // visit this node
497     // if visit returns true then
498     // visit the referenced include and sysInclude dependencies
499     //
500     if (visitor.visit(dependInfo)) {
501       // BEGINFREEHEP
502       // //
503       // // find first null entry on stack
504       // //
505       // int stackPosition = -1;
506       // for (int i = 0; i < stack.length; i++) {
507       // if (stack[i] == null) {
508       // stackPosition = i;
509       // stack[i] = dependInfo;
510       // break;
511       // } else {
512       // //
513       // // if we have appeared early in the calling history
514       // // then we didn't exceed the criteria
515       // if (stack[i] == dependInfo) {
516       // return;
517       // }
518       // }
519       // }
520       // if (stackPosition == -1) {
521       // visitor.stackExhausted();
522       // return;
523       // }
524       // ENDFREEHEP
525       //
526       // locate dependency infos
527       //
528       final String[] includes = dependInfo.getIncludes();
529       final String includePathIdentifier = compiler.getIncludePathIdentifier();
530       final DependencyInfo[] includeInfos = new DependencyInfo[includes.length];
531       for (int i = 0; i < includes.length; i++) {
532         final DependencyInfo includeInfo = getDependencyInfo(includes[i], includePathIdentifier);
533         includeInfos[i] = includeInfo;
534       }
535       //
536       // preview with only the already available dependency infos
537       //
538       if (visitor.preview(dependInfo, includeInfos)) {
539         //
540         // now need to fill in the missing DependencyInfos
541         //
542         int missingCount = 0;
543         for (int i = 0; i < includes.length; i++) {
544           if (includeInfos[i] == null) {
545             missingCount++;
546             task.log("Parsing " + includes[i], Project.MSG_VERBOSE);
547             //
548             // If the include filepath is relative
549             // then anchor it the base directory
550             File src = new File(includes[i]);
551             if (!src.isAbsolute()) {
552               src = new File(this.baseDir, includes[i]);
553             }
554             final DependencyInfo includeInfo = parseIncludes(task, compiler, src);
555             includeInfos[i] = includeInfo;
556           }
557         }
558         //
559         // if it passes a review the second time
560         // then recurse into all the children
561         if (missingCount == 0 || visitor.preview(dependInfo, includeInfos)) {
562           //
563           // recurse into
564           //
565           for (final DependencyInfo includeInfo : includeInfos) {
566             // Darren Sargent 23Oct2008
567             // only recurse for direct includes of current source
568             // file
569             if (includeInfo.getSource().contains(File.separatorChar + "src" + File.separatorChar + "main")
570                 || includeInfo.getSource().contains(File.separatorChar + "src" + File.separatorChar + "test")) {
571               task.log("Walking dependencies for " + includeInfo.getSource(), Project.MSG_VERBOSE);
572               walkDependencies(task, includeInfo, compiler, stack, visitor);
573             }
574           }
575         }
576       }
577       // FREEHEP
578       // stack[stackPosition] = null;
579     }
580   }
581 
582   private void
583       writeDependencyInfo(final BufferedWriter writer, final StringBuffer buf, final DependencyInfo dependInfo)
584           throws IOException {
585     final String[] includes = dependInfo.getIncludes();
586     final String[] sysIncludes = dependInfo.getSysIncludes();
587     //
588     // if the includes have not been evaluted then
589     // it is not worth our time saving it
590     // and trying to distiguish between files with
591     // no dependencies and those with undetermined dependencies
592     buf.setLength(0);
593     buf.append("      <source file=\"");
594     buf.append(CUtil.xmlAttribEncode(dependInfo.getSource()));
595     buf.append("\" lastModified=\"");
596     buf.append(Long.toHexString(dependInfo.getSourceLastModified()));
597     buf.append("\">\n");
598     writer.write(buf.toString());
599     for (final String include : includes) {
600       buf.setLength(0);
601       buf.append("         <include file=\"");
602       buf.append(CUtil.xmlAttribEncode(include));
603       buf.append("\"/>\n");
604       writer.write(buf.toString());
605     }
606     for (final String sysInclude : sysIncludes) {
607       buf.setLength(0);
608       buf.append("         <sysinclude file=\"");
609       buf.append(CUtil.xmlAttribEncode(sysInclude));
610       buf.append("\"/>\n");
611       writer.write(buf.toString());
612     }
613     writer.write("      </source>\n");
614     return;
615   }
616 
617   private void writeIncludePathDependencies(final String includePathIdentifier, final BufferedWriter writer,
618       final StringBuffer buf) throws IOException {
619     //
620     // include path element
621     //
622     buf.setLength(0);
623     buf.append("   <includePath signature=\"");
624     buf.append(CUtil.xmlAttribEncode(includePathIdentifier));
625     buf.append("\">\n");
626     writer.write(buf.toString());
627     final Enumeration dependenciesEnum = this.dependencies.elements();
628     while (dependenciesEnum.hasMoreElements()) {
629       final DependencyInfo[] dependInfos = (DependencyInfo[]) dependenciesEnum.nextElement();
630       for (final DependencyInfo dependInfo : dependInfos) {
631         //
632         // if this is for the same include path
633         // then output the info
634         if (dependInfo.getIncludePathIdentifier().equals(includePathIdentifier)) {
635           writeDependencyInfo(writer, buf, dependInfo);
636         }
637       }
638     }
639     writer.write("   </includePath>\n");
640   }
641 }