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