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