View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one   *
3    * or more contributor license agreements.  See the NOTICE file *
4    * distributed with this work for additional information        *
5    * regarding copyright ownership.  The ASF licenses this file   *
6    * to you under the Apache License, Version 2.0 (the            *
7    * "License"); you may not use this file except in compliance   *
8    * with the License.  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,   *
13   * software distributed under the License is distributed on an  *
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
15   * KIND, either express or implied.  See the License for the    *
16   * specific language governing permissions and limitations      *
17   * under the License.                                           *
18   */
19  package org.apache.rat.annotation;
20  
21  import org.apache.commons.io.IOUtils;
22  
23  import java.io.BufferedReader;
24  import java.io.File;
25  import java.io.FileInputStream;
26  import java.io.FileWriter;
27  import java.io.FilterInputStream;
28  import java.io.IOException;
29  import java.io.InputStream;
30  import java.io.InputStreamReader;
31  import java.io.Writer;
32  import java.util.Arrays;
33  import java.util.HashMap;
34  import java.util.Map;
35  
36  /**
37   * Add a license header to a document. This appender does not check for the
38   * existence of an existing license header, it is assumed that either a second
39   * license header is intentional or that there is no license header present
40   * already.
41   */
42  public abstract class AbstractLicenseAppender {
43      private static final String DOT = ".";
44      private static final int TYPE_UNKNOWN = 0;
45      private static final int TYPE_JAVA = 1;
46      private static final int TYPE_XML = 2;
47      private static final int TYPE_HTML = 3;
48      private static final int TYPE_CSS = 4;
49      private static final int TYPE_JAVASCRIPT = 5;
50      private static final int TYPE_APT = 6;
51      private static final int TYPE_PROPERTIES = 7;
52      private static final int TYPE_PYTHON = 8;
53      private static final int TYPE_C = 9;
54      private static final int TYPE_H = 10;
55      private static final int TYPE_SH = 11;
56      private static final int TYPE_BAT = 12;
57      private static final int TYPE_VM = 13;
58      private static final int TYPE_SCALA = 14;
59      private static final int TYPE_RUBY = 15;
60      private static final int TYPE_PERL = 16;
61      private static final int TYPE_TCL = 17;
62      private static final int TYPE_CPP = 18;
63      private static final int TYPE_CSHARP = 19;
64      private static final int TYPE_PHP = 20;
65      private static final int TYPE_GROOVY = 21;
66      private static final int TYPE_VISUAL_STUDIO_SOLUTION = 22;
67      private static final int TYPE_BEANSHELL = 23;
68      private static final int TYPE_JSP = 24;
69      private static final int TYPE_FML = 25;
70      private static final int TYPE_GO = 26;
71      private static final int TYPE_PM = 27;    
72  
73      /**
74       * the line separator for this OS
75       */
76      private static final String LINE_SEP = System.getProperty("line.separator");
77  
78      private static final int[] FAMILY_C = new int[]{
79              TYPE_JAVA, TYPE_JAVASCRIPT, TYPE_C, TYPE_H, TYPE_SCALA,
80              TYPE_CSS, TYPE_CPP, TYPE_CSHARP, TYPE_PHP, TYPE_GROOVY,
81              TYPE_BEANSHELL, TYPE_GO,
82      };
83      private static final int[] FAMILY_SGML = new int[]{
84              TYPE_XML, TYPE_HTML, TYPE_JSP, TYPE_FML,
85      };
86      private static final int[] FAMILY_SH = new int[]{
87              TYPE_PROPERTIES, TYPE_PYTHON, TYPE_SH, TYPE_RUBY, TYPE_PERL,
88              TYPE_TCL, TYPE_VISUAL_STUDIO_SOLUTION, TYPE_PM,
89      };
90      private static final int[] FAMILY_BAT = new int[]{
91              TYPE_BAT,
92      };
93      private static final int[] FAMILY_APT = new int[]{
94              TYPE_APT,
95      };
96      private static final int[] FAMILY_VELOCITY = new int[]{
97              TYPE_VM,
98      };
99      private static final int[] EXPECTS_HASH_PLING = new int[]{
100             TYPE_PYTHON, TYPE_SH, TYPE_RUBY, TYPE_PERL, TYPE_TCL,
101     };
102     private static final int[] EXPECTS_AT_ECHO = new int[]{
103             TYPE_BAT,
104     };
105     private static final int[] EXPECTS_PACKAGE = new int[]{
106             TYPE_JAVA, TYPE_GO, TYPE_PM,
107     };
108     private static final int[] EXPECTS_XML_DECL = new int[]{
109             TYPE_XML,
110     };
111     private static final int[] EXPECTS_PHP_PI = new int[]{
112             TYPE_PHP,
113     };
114     private static final int[] EXPECTS_MSVSSF_HEADER = new int[]{
115             TYPE_VISUAL_STUDIO_SOLUTION,
116     };
117 
118     private static final Map<String, Integer> EXT2TYPE = new HashMap<String, Integer>();
119 
120     static {
121         // these arrays are used in Arrays.binarySearch so they must
122         // be sorted
123         Arrays.sort(FAMILY_C);
124         Arrays.sort(FAMILY_SGML);
125         Arrays.sort(FAMILY_SH);
126         Arrays.sort(FAMILY_BAT);
127         Arrays.sort(FAMILY_APT);
128         Arrays.sort(FAMILY_VELOCITY);
129 
130         Arrays.sort(EXPECTS_HASH_PLING);
131         Arrays.sort(EXPECTS_AT_ECHO);
132         Arrays.sort(EXPECTS_PACKAGE);
133         Arrays.sort(EXPECTS_XML_DECL);
134         Arrays.sort(EXPECTS_MSVSSF_HEADER);
135 
136         EXT2TYPE.put("apt", TYPE_APT);
137         EXT2TYPE.put("asax", TYPE_HTML);
138         EXT2TYPE.put("ascx", TYPE_HTML);
139         EXT2TYPE.put("aspx", TYPE_HTML);
140         EXT2TYPE.put("bat", TYPE_BAT);
141         EXT2TYPE.put("bsh", TYPE_BEANSHELL);
142         EXT2TYPE.put("c", TYPE_C);
143         EXT2TYPE.put("cc", TYPE_CPP);
144         EXT2TYPE.put("cmd", TYPE_BAT);
145         EXT2TYPE.put("config", TYPE_XML);
146         EXT2TYPE.put("cpp", TYPE_CPP);
147         EXT2TYPE.put("cs", TYPE_CSHARP);
148         EXT2TYPE.put("csdproj", TYPE_XML);
149         EXT2TYPE.put("csproj", TYPE_XML);
150         EXT2TYPE.put("css", TYPE_CSS);
151         EXT2TYPE.put("fxcop", TYPE_XML);
152         EXT2TYPE.put("fml", TYPE_FML);
153         EXT2TYPE.put("groovy", TYPE_GROOVY);
154         EXT2TYPE.put("go", TYPE_GO);
155         EXT2TYPE.put("h", TYPE_H);
156         EXT2TYPE.put("hh", TYPE_H);
157         EXT2TYPE.put("hpp", TYPE_H);
158         EXT2TYPE.put("htm", TYPE_HTML);
159         EXT2TYPE.put("html", TYPE_HTML);
160         EXT2TYPE.put("java", TYPE_JAVA);
161         EXT2TYPE.put("js", TYPE_JAVASCRIPT);
162         EXT2TYPE.put("jsp", TYPE_JSP);
163         EXT2TYPE.put("ndoc", TYPE_XML);
164         EXT2TYPE.put("nunit", TYPE_XML);
165         EXT2TYPE.put("php", TYPE_PHP);
166         EXT2TYPE.put("pl", TYPE_PERL);
167         EXT2TYPE.put("pm", TYPE_PM);
168         EXT2TYPE.put("properties", TYPE_PROPERTIES);
169         EXT2TYPE.put("py", TYPE_PYTHON);
170         EXT2TYPE.put("rb", TYPE_RUBY);
171         EXT2TYPE.put("rdf", TYPE_XML);
172         EXT2TYPE.put("resx", TYPE_XML);
173         EXT2TYPE.put("scala", TYPE_SCALA);
174         EXT2TYPE.put("sh", TYPE_SH);
175         EXT2TYPE.put("shfbproj", TYPE_XML);
176         EXT2TYPE.put("sln", TYPE_VISUAL_STUDIO_SOLUTION);
177         EXT2TYPE.put("stylecop", TYPE_XML);
178         EXT2TYPE.put("svg", TYPE_XML);
179         EXT2TYPE.put("tcl", TYPE_TCL);
180         EXT2TYPE.put("vbdproj", TYPE_XML);
181         EXT2TYPE.put("vbproj", TYPE_XML);
182         EXT2TYPE.put("vcproj", TYPE_XML);
183         EXT2TYPE.put("vm", TYPE_VM);
184         EXT2TYPE.put("vsdisco", TYPE_XML);
185         EXT2TYPE.put("webinfo", TYPE_XML);
186         EXT2TYPE.put("xml", TYPE_XML);
187         EXT2TYPE.put("xproj", TYPE_XML);
188         EXT2TYPE.put("xsl", TYPE_XML);
189     }
190 
191     private boolean isForced;
192 
193     public AbstractLicenseAppender() {
194         super();
195     }
196 
197     /**
198      * Append the default license header to the supplied document.
199      *
200      * @param document document to append to.
201      * @throws IOException if there is a problem while reading or writing the file
202      */
203     public void append(File document) throws IOException {
204         int type = getType(document);
205         if (type == TYPE_UNKNOWN) {
206             return;
207         }
208 
209         boolean expectsHashPling = expectsHashPling(type);
210         boolean expectsAtEcho = expectsAtEcho(type);
211         boolean expectsPackage = expectsPackage(type);
212         boolean expectsXMLDecl = expectsXMLDecl(type);
213         boolean expectsPhpPI = expectsPhpPI(type);
214         boolean expectsMSVSSF = expectsMSVisualStudioSolutionFileHeader(type);
215 
216         File newDocument = new File(document.getAbsolutePath() + ".new");
217         FileWriter writer = new FileWriter(newDocument);
218         try {
219             if (!attachLicense(writer, document,
220                     expectsHashPling, expectsAtEcho, expectsPackage,
221                     expectsXMLDecl, expectsPhpPI, expectsMSVSSF)) {
222                 // Java File without package, XML file without decl or PHP
223                 // file without PI
224                 // for Java just place the license at the front, for XML add
225                 // an XML decl first - don't know how to handle PHP
226                 if (expectsPackage || expectsXMLDecl) {
227                     writer = new FileWriter(newDocument);
228                     if (expectsXMLDecl) {
229                         writer.write("<?xml version='1.0'?>");
230                         writer.write(LINE_SEP);
231                     }
232                     attachLicense(writer, document,
233                             false, false, false, false, false, false);
234                 }
235             }
236         } finally {
237             IOUtils.closeQuietly(writer);
238         }
239 
240         if (isForced) {
241             boolean deleted = document.delete();
242             if (!deleted) {
243                 System.err.println("Could not delete original file to prepare renaming.");
244             }
245             boolean renamed = newDocument.renameTo(document.getAbsoluteFile());
246             if (!renamed) {
247                 System.err.println("Failed to rename new file, original file remains unchanged.");
248             }
249         }
250     }
251 
252     /**
253      * Write document's content to writer attaching the license using
254      * the given flags as hints for where to put it.
255      *
256      * @return whether the license has actually been written
257      */
258     private boolean attachLicense(Writer writer, File document,
259                                   boolean expectsHashPling,
260                                   boolean expectsAtEcho,
261                                   boolean expectsPackage,
262                                   boolean expectsXMLDecl,
263                                   boolean expectsPhpPI,
264                                   boolean expectsMSVSSF)
265             throws IOException {
266         boolean written = false;
267         FileInputStream fis = null;
268         BufferedReader br = null;
269         try {
270             fis = new FileInputStream(document);
271             br = new BufferedReader(new InputStreamReader(new BOMInputStream(fis)));
272 
273             if (!expectsHashPling
274                     && !expectsAtEcho
275                     && !expectsPackage
276                     && !expectsXMLDecl
277                     && !expectsPhpPI
278                     && !expectsMSVSSF) {
279                 written = true;
280                 writer.write(getLicenseHeader(document));
281                 writer.write(LINE_SEP);
282             }
283 
284             String line;
285             boolean first = true;
286             while ((line = br.readLine()) != null) {
287                 if (first && expectsHashPling) {
288                     written = true;
289                     doFirstLine(document, writer, line, "#!");
290                 } else if (first && expectsAtEcho) {
291                     written = true;
292                     doFirstLine(document, writer, line, "@echo");
293                 } else if (first && expectsMSVSSF) {
294                     written = true;
295                     if ("".equals(line)) {
296                         line = passThroughReadNext(writer, line, br);
297                     }
298                     if (line.startsWith("Microsoft Visual Studio Solution"
299                             + " File")) {
300                         line = passThroughReadNext(writer, line, br);
301                     }
302                     doFirstLine(document, writer, line, "# Visual ");
303                 } else {
304                     writer.write(line);
305                     writer.write(LINE_SEP);
306                 }
307 
308                 if (expectsPackage && line.startsWith("package ")) {
309                     written = true;
310                     writer.write(LINE_SEP);
311                     writer.write(getLicenseHeader(document));
312                     writer.write(LINE_SEP);
313                 } else if (expectsXMLDecl && line.startsWith("<?xml ")) {
314                     written = true;
315                     writer.write(LINE_SEP);
316                     writer.write(getLicenseHeader(document));
317                     writer.write(LINE_SEP);
318                 } else if (expectsPhpPI && line.startsWith("<?php")) {
319                     written = true;
320                     writer.write(LINE_SEP);
321                     writer.write(getLicenseHeader(document));
322                     writer.write(LINE_SEP);
323                 }
324                 first = false;
325             }
326         } finally {
327             IOUtils.closeQuietly(br);
328             IOUtils.closeQuietly(fis);
329             IOUtils.closeQuietly(writer);
330         }
331         return written;
332     }
333 
334     /**
335      * Check first line for specified text and process.
336      */
337     private void doFirstLine(File document, Writer writer, String line, String lookfor) throws IOException {
338         if (line.startsWith(lookfor)) {
339             writer.write(line);
340             writer.write(LINE_SEP);
341             writer.write(getLicenseHeader(document));
342         } else {
343             writer.write(getLicenseHeader(document));
344             writer.write(line);
345             writer.write(LINE_SEP);
346         }
347     }
348 
349     /**
350      * Detect the type of document.
351      *
352      * @param document to retrieve type from.
353      * @return not null
354      * TODO use existing mechanism to detect the type of a file and record it in the report output, thus we will not need this duplication here.
355      */
356     protected int getType(File document) {
357         String path = document.getPath();
358         int lastDot = path.lastIndexOf(DOT);
359         if (lastDot >= 0 && lastDot < path.length() - 1) {
360             String ext = path.substring(lastDot + 1);
361             Integer type = EXT2TYPE.get(ext);
362             if (type != null) {
363                 return type;
364             }
365         }
366         return TYPE_UNKNOWN;
367     }
368 
369     /**
370      * Set the force flag on this appender. If this flag is set
371      * to true then files will be modified directly, otherwise
372      * new files will be created alongside the existing files.
373      *
374      * @param force force flag.
375      */
376     public void setForce(boolean force) {
377         isForced = force;
378     }
379 
380     /**
381      * @param document document to extract from.
382      * @return Get the license header of a document.
383      */
384     public abstract String getLicenseHeader(File document);
385 
386     /**
387      * Get the first line of the license header formatted
388      * for the given type of file.
389      *
390      * @param type the type of file, see the TYPE_* constants
391      * @return not null
392      */
393     protected String getFirstLine(int type) {
394         if (isFamilyC(type)) {
395             return "/*" + LINE_SEP;
396         } else if (isFamilySGML(type)) {
397             return "<!--" + LINE_SEP;
398         }
399         return "";
400     }
401 
402 
403     /**
404      * Get the last line of the license header formatted
405      * for the given type of file.
406      *
407      * @param type the type of file, see the TYPE_* constants
408      * @return not null
409      */
410     protected String getLastLine(int type) {
411         if (isFamilyC(type)) {
412             return " */" + LINE_SEP;
413         } else if (isFamilySGML(type)) {
414             return "-->" + LINE_SEP;
415         }
416         return "";
417     }
418 
419 
420     /**
421      * Get a line of the license header formatted
422      * for the given type of file.
423      *
424      * @param type    the type of file, see the TYPE_* constants
425      * @param content the content for this line
426      * @return not null
427      */
428     protected String getLine(int type, String content) {
429         if (isFamilyC(type)) {
430             return " * " + content + LINE_SEP;
431         } else if (isFamilySGML(type)) {
432             return content + LINE_SEP;
433         } else if (isFamilyAPT(type)) {
434             return "~~ " + content + LINE_SEP;
435         } else if (isFamilySH(type)) {
436             return "# " + content + LINE_SEP;
437         } else if (isFamilyBAT(type)) {
438             return "rem " + content + LINE_SEP;
439         } else if (isFamilyVelocity(type)) {
440             return "## " + content + LINE_SEP;
441         }
442         return "";
443     }
444 
445     private static boolean isFamilyC(int type) {
446         return isIn(FAMILY_C, type);
447     }
448 
449     private static boolean isFamilySGML(int type) {
450         return isIn(FAMILY_SGML, type);
451     }
452 
453     private static boolean isFamilySH(int type) {
454         return isIn(FAMILY_SH, type);
455     }
456 
457     private static boolean isFamilyAPT(int type) {
458         return isIn(FAMILY_APT, type);
459     }
460 
461     private static boolean isFamilyBAT(int type) {
462         return isIn(FAMILY_BAT, type);
463     }
464 
465     private static boolean isFamilyVelocity(int type) {
466         return isIn(FAMILY_VELOCITY, type);
467     }
468 
469     private static boolean expectsHashPling(int type) {
470         return isIn(EXPECTS_HASH_PLING, type);
471     }
472 
473     private static boolean expectsAtEcho(int type) {
474         return isIn(EXPECTS_AT_ECHO, type);
475     }
476 
477     private static boolean expectsPackage(int type) {
478         return isIn(EXPECTS_PACKAGE, type);
479     }
480 
481     private static boolean expectsXMLDecl(int type) {
482         return isIn(EXPECTS_XML_DECL, type);
483     }
484 
485     private static boolean expectsPhpPI(int type) {
486         return isIn(EXPECTS_PHP_PI, type);
487     }
488 
489     private static boolean expectsMSVisualStudioSolutionFileHeader(int type) {
490         return isIn(EXPECTS_MSVSSF_HEADER, type);
491     }
492 
493     private static boolean isIn(int[] arr, int key) {
494         return Arrays.binarySearch(arr, key) >= 0;
495     }
496 
497     private String passThroughReadNext(Writer writer, String line,
498                                        BufferedReader br) throws IOException {
499         writer.write(line);
500         writer.write(LINE_SEP);
501         String l = br.readLine();
502         return l == null ? "" : l;
503     }
504 }
505 
506 /**
507  * Stripped down version of Commons IO 2.0's BOMInputStream.
508  */
509 class BOMInputStream extends FilterInputStream {
510     private int[] firstBytes;
511     private int fbLength, fbIndex, markFbIndex;
512     private boolean markedAtStart;
513     private static final int[][] BOMS = {
514             new int[]{0xEF, 0xBB, 0xBF}, // UTF-8
515             new int[]{0xFE, 0xFF}, // UTF-16BE
516             new int[]{0xFF, 0xFE}, // UTF-16LE
517     };
518 
519     BOMInputStream(InputStream s) {
520         super(s);
521     }
522 
523     @Override
524     public int read() throws IOException {
525         int b = readFirstBytes();
526         return (b >= 0) ? b : in.read();
527     }
528 
529     @Override
530     public int read(byte[] buf, int off, int len) throws IOException {
531         int firstCount = 0;
532         int b = 0;
533         while ((len > 0) && (b >= 0)) {
534             b = readFirstBytes();
535             if (b >= 0) {
536                 buf[off++] = (byte) (b & 0xFF);
537                 len--;
538                 firstCount++;
539             }
540         }
541         int secondCount = in.read(buf, off, len);
542         return (secondCount < 0)
543                 ? (firstCount > 0 ? firstCount : -1) : firstCount + secondCount;
544     }
545 
546     @Override
547     public int read(byte[] buf) throws IOException {
548         return read(buf, 0, buf.length);
549     }
550 
551     private int readFirstBytes() throws IOException {
552         getBOM();
553         return (fbIndex < fbLength) ? firstBytes[fbIndex++] : -1;
554     }
555 
556     private void getBOM() throws IOException {
557         if (firstBytes == null) {
558             int max = 0;
559             for (int[] BOM : BOMS) {
560                 max = Math.max(max, BOM.length);
561             }
562             firstBytes = new int[max];
563             for (int i = 0; i < firstBytes.length; i++) {
564                 firstBytes[i] = in.read();
565                 fbLength++;
566                 if (firstBytes[i] < 0) {
567                     break;
568                 }
569 
570                 boolean found = find();
571                 if (found) {
572                     fbLength = 0;
573                     break;
574                 }
575             }
576         }
577     }
578 
579     @Override
580     public synchronized void mark(int readlimit) {
581         markFbIndex = fbIndex;
582         markedAtStart = (firstBytes == null);
583         in.mark(readlimit);
584     }
585 
586     @Override
587     public synchronized void reset() throws IOException {
588         fbIndex = markFbIndex;
589         if (markedAtStart) {
590             firstBytes = null;
591         }
592 
593         in.reset();
594     }
595 
596     @Override
597     public long skip(long n) throws IOException {
598         while ((n > 0) && (readFirstBytes() >= 0)) {
599             n--;
600         }
601         return in.skip(n);
602     }
603 
604     private boolean find() {
605         for (int[] BOM : BOMS) {
606             if (matches(BOM)) {
607                 return true;
608             }
609         }
610         return false;
611     }
612 
613     private boolean matches(int[] bom) {
614         if (bom.length != fbLength) {
615             return false;
616         }
617         for (int i = 0; i < bom.length; i++) {
618             if (bom[i] != firstBytes[i]) {
619                 return false;
620             }
621         }
622         return true;
623     }
624 
625 }