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