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