changeset 136:d1612c9b8411

Create equivalent of mvn license:format Reviwed-by: aazores Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2017-June/023922.html Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2017-July/024005.html
author Christopher Koehler <chkoehle@redhat.com>
date Thu, 20 Jul 2017 13:49:48 -0400
parents bb3a7b0bf4c2
children c8113c0a3d5a
files integration-test/specs/license-check/license-check.spec.js integration-test/specs/license-check/license-update.spec.js integration-test/specs/license-check/resources/licenses/GOOD_LICENSE integration-test/specs/license-check/resources/source-files/updatable-files/corruptDate-broken.js integration-test/specs/license-check/resources/source-files/updatable-files/corruptDate-fixed.js integration-test/specs/license-check/resources/source-files/updatable-files/noEndingJavadoc-broken.js integration-test/specs/license-check/resources/source-files/updatable-files/noEndingJavadoc-fixed.js integration-test/specs/license-check/resources/source-files/updatable-files/offsetFromTop-broken.js integration-test/specs/license-check/resources/source-files/updatable-files/offsetFromTop-fixed.js integration-test/specs/license-check/resources/source-files/updatable-files/outOfDate-broken.js integration-test/specs/license-check/resources/source-files/updatable-files/outOfDate-fixed.js integration-test/specs/license-check/resources/source-files/updatable-files/slashComments-broken.js integration-test/specs/license-check/resources/source-files/updatable-files/slashComments-fixed.js integration-test/specs/license-check/resources/source-files/updatable-files/slightlyCorrupt-broken.js integration-test/specs/license-check/resources/source-files/updatable-files/slightlyCorrupt-fixed.js integration-test/specs/license-check/resources/source-files/updatable-files/veryCorrupt-broken.js integration-test/specs/license-check/resources/source-files/updatable-files/veryCorrupt-fixed.js license-check/license-check.js license-check/perform-license-update.js package.json
diffstat 20 files changed, 850 insertions(+), 1 deletions(-) [+]
line wrap: on
line diff
--- a/integration-test/specs/license-check/license-check.spec.js	Wed Jul 19 15:48:35 2017 -0400
+++ b/integration-test/specs/license-check/license-check.spec.js	Thu Jul 20 13:49:48 2017 -0400
@@ -119,4 +119,42 @@
     let licenseData = licenseCheck.readLicenseFile(CURRENT_LICENSE);
     licenseData.success.should.be.true(licenseData.error);
   });
+
+  describe('argument year processing', () => {
+    it('should properly get the current year for no arguments', () => {
+      const CURRENT_YEAR = new Date().getFullYear();
+      let currentYear = licenseCheck.getCurrentYearFromArgs([]);
+      currentYear.should.be.equal(CURRENT_YEAR);
+    });
+
+    ['20a5', 'a', ' ', '', '2345 '].forEach(yearStr => {
+      it('should throw for a non-integer value: "' + yearStr + '"', () => {
+        licenseCheck.getCurrentYearFromArgs.bind(null, [yearStr]).should.throw();
+      });
+    });
+
+    ['1990', '1999', '1000', '1523'].forEach(yearStr => {
+      it('should throw a < 2000 year value: ' + yearStr, () => {
+        licenseCheck.getCurrentYearFromArgs.bind(null, [yearStr]).should.throw();
+      });
+    });
+
+    ['3000', '3159', '3888', '4813', '9999'].forEach(yearStr => {
+      it('should throw a >= 3000 year value: ' + yearStr, () => {
+        licenseCheck.getCurrentYearFromArgs.bind(null, [yearStr]).should.throw();
+      });
+    });
+
+    it('should throw if there are multiple arguments', () => {
+      licenseCheck.getCurrentYearFromArgs.bind(null, ['2014', '2015']).should.throw();
+      licenseCheck.getCurrentYearFromArgs.bind(null, ['2014', '2015', '2711']).should.throw();
+    });
+
+    [2014, 2017, 2017, 2050, 2345].forEach(yearInt => {
+      it('should correctly process valid year ' + yearInt.toString(), () => {
+        let year = licenseCheck.getCurrentYearFromArgs([yearInt.toString()]);
+        year.should.be.equal(yearInt);
+      });
+    });
+  });
 });
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/integration-test/specs/license-check/license-update.spec.js	Thu Jul 20 13:49:48 2017 -0400
@@ -0,0 +1,330 @@
+/**
+ * Copyright 2012-2017 Red Hat, Inc.
+ *
+ * Thermostat is distributed under the GNU General Public License,
+ * version 2 or any later version (with a special exception described
+ * below, commonly known as the "Classpath Exception").
+ *
+ * A copy of GNU General Public License (GPL) is included in this
+ * distribution, in the file COPYING.
+ *
+ * Linking Thermostat code with other modules is making a combined work
+ * based on Thermostat.  Thus, the terms and conditions of the GPL
+ * cover the whole combination.
+ *
+ * As a special exception, the copyright holders of Thermostat give you
+ * permission to link this code with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on Thermostat code.  If you modify Thermostat, you may
+ * extend this exception to your version of the software, but you are
+ * not obligated to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+describe('License updating', () => {
+  const licenseCheck = require('../../../license-check/license-check.js');
+
+  it('should have found the license check module', () => {
+    (licenseCheck === null).should.be.false();
+  });
+
+  describe('file to array reading', () => {
+    const SIMPLE_SOURCE_FILE = './integration-test/specs/license-check/resources/source-files/invalid-files/invalid-misc/no-license.js';
+    const EMPTY_FILE = './integration-test/specs/license-check/resources/licenses/EMPTY_LICENSE';
+
+    it('should read a file to lines with no \\r\\n\'s', () => {
+      let lines = licenseCheck.readFileLinesToArray(SIMPLE_SOURCE_FILE);
+      const EXPECTED_LINES = [
+        'var x = 5;',
+        '',
+        'function test (val) {',
+        '    return val;',
+        '}',
+        ''
+      ];
+      lines.should.eql(EXPECTED_LINES);
+    });
+
+    it('should read an empty file and be an array of one empty string', () => {
+      let lines = licenseCheck.readFileLinesToArray(EMPTY_FILE);
+      lines.should.eql(['']);
+    });
+  });
+
+  describe('license range', () => {
+    it('should not find anything in an empty list', () => {
+      let licenseRange = licenseCheck.determineLicenseLineRangesAtTopOfFile(['']);
+      licenseRange.foundMatch.should.be.false();
+    });
+
+    it('should find it normally at the top', () => {
+      const LICENSE_LINES = [
+        '/**',
+        ' * Some license',
+        ' * here',
+        ' */'
+      ];
+      let licenseRange = licenseCheck.determineLicenseLineRangesAtTopOfFile(LICENSE_LINES);
+      licenseRange.foundMatch.should.be.true();
+      licenseRange.start.should.equal(0);
+      licenseRange.end.should.equal(3);
+    });
+
+    it('should find it normally at the top with source code', () => {
+      const LICENSE_LINES = [
+        '/**',
+        ' * Some license',
+        ' * here',
+        ' */',
+        '',
+        'var sourceCode = "here"',
+        ''
+      ];
+      let licenseRange = licenseCheck.determineLicenseLineRangesAtTopOfFile(LICENSE_LINES);
+      licenseRange.foundMatch.should.be.true();
+      licenseRange.start.should.equal(0);
+      licenseRange.end.should.equal(3);
+    });
+
+    it('should find it at the middle due to multiple \\n\'s', () => {
+      const LICENSE_LINES = [
+        '',
+        '',
+        '',
+        '/**',
+        ' * Some license',
+        ' * here',
+        ' */',
+        '',
+        'var sourceCode = "here"',
+        ''
+      ];
+      let licenseRange = licenseCheck.determineLicenseLineRangesAtTopOfFile(LICENSE_LINES);
+      licenseRange.foundMatch.should.be.true();
+      licenseRange.start.should.equal(3);
+      licenseRange.end.should.equal(6);
+    });
+
+    it('should not find it because of source code above it', () => {
+      const LICENSE_LINES = [
+        '',
+        'var x = 5;',
+        '',
+        '/**',
+        ' * Some license',
+        ' * here',
+        ' */',
+        '',
+        'var sourceCode = "here"',
+        ''
+      ];
+      let licenseRange = licenseCheck.determineLicenseLineRangesAtTopOfFile(LICENSE_LINES);
+      licenseRange.foundMatch.should.be.false();
+    });
+
+    it('should find //\'s and at the middle due to multiple \\n\'s', () => {
+      const LICENSE_LINES = [
+        '',
+        '',
+        '// Some license',
+        '// here',
+        '',
+        'var sourceCode = "here"',
+        ''
+      ];
+      let licenseRange = licenseCheck.determineLicenseLineRangesAtTopOfFile(LICENSE_LINES);
+      licenseRange.foundMatch.should.be.true();
+      licenseRange.start.should.equal(2);
+      licenseRange.end.should.equal(3);
+    });
+
+    it('should report a non-terminated comment document', () => {
+      const LICENSE_LINES = [
+        '/**',
+        ' * Something 2012-2012',
+        ' * no terminal star/slash',
+        'var sourceCode = "here"',
+        ''
+      ];
+      let licenseRange = licenseCheck.determineLicenseLineRangesAtTopOfFile(LICENSE_LINES);
+      licenseRange.foundMatch.should.be.true();
+      licenseRange.start.should.equal(0);
+      licenseRange.end.should.equal(4);
+    });
+  });
+
+  describe('license range updating', () => {
+    it('should not find a difference between identical licenses', () => {
+      const FILE_LINES = [
+        '/**',
+        ' * Some license 2015-2017',
+        ' */'
+      ];
+      const LICENSE_DATA = {
+        text: [
+          '/**',
+          ' * Some license 2015-2017',
+          ' */'
+        ],
+        yearLineNumber: 1,
+        error: null,
+        success: true
+      };
+      const LICENSE_LINE_RANGE = { start: 0, end: 2, foundMatch: true };
+      let updateStatus = licenseCheck.licenseRangeNeedsUpdating(FILE_LINES, LICENSE_LINE_RANGE, LICENSE_DATA);
+      updateStatus.needsUpdate.should.be.false();
+    });
+
+    it('should find a difference between non-identical licenses (different year)', () => {
+      const FILE_LINES = [
+        '/**',
+        ' * Some license 2015-2016',
+        ' */'
+      ];
+      const LICENSE_DATA = {
+        text: [
+          '/**',
+          ' * Some license 2015-2017',
+          ' */'
+        ],
+        yearLineNumber: 1,
+        error: null,
+        success: true
+      };
+      const LICENSE_LINE_RANGE = { start: 0, end: 2, foundMatch: true };
+      let updateStatus = licenseCheck.licenseRangeNeedsUpdating(FILE_LINES, LICENSE_DATA, LICENSE_LINE_RANGE);
+      updateStatus.needsUpdate.should.be.true();
+    });
+
+    it('should find a difference between non-identical licenses (different text)', () => {
+      const FILE_LINES = [
+        '/**',
+        ' * Some license 2015-2016',
+        ' *',
+        ' * This is a simple test',
+        ' * of mutated text.',
+        ' */'
+      ];
+      const LICENSE_DATA = {
+        text: [
+          '/**',
+          ' * Some license 2015-2017',
+          ' *',
+          ' * This is a simple test',
+          ' * of unmutated text.',
+          ' */'
+        ],
+        yearLineNumber: 1,
+        error: null,
+        success: true
+      };
+      const LICENSE_LINE_RANGE = { start: 0, end: 5, foundMatch: true };
+      let updateStatus = licenseCheck.licenseRangeNeedsUpdating(FILE_LINES, LICENSE_DATA, LICENSE_LINE_RANGE);
+      updateStatus.needsUpdate.should.be.true();
+    });
+
+    it('should find a difference between non-identical licenses (missing lines)', () => {
+      const FILE_LINES = [
+        '/**',
+        ' * Some license 2015-2017',
+        ' *',
+        ' * This is a simple test',
+        ' */'
+      ];
+      const LICENSE_DATA = {
+        text: [
+          '/**',
+          ' * Some license 2015-2017',
+          ' *',
+          ' * This is a simple test',
+          ' * of unmutated text.',
+          ' */'
+        ],
+        yearLineNumber: 1,
+        error: null,
+        success: true
+      };
+      const LICENSE_LINE_RANGE = { start: 0, end: 4, foundMatch: true };
+      let updateStatus = licenseCheck.licenseRangeNeedsUpdating(FILE_LINES, LICENSE_DATA, LICENSE_LINE_RANGE);
+      updateStatus.needsUpdate.should.be.true();
+    });
+
+    it('should detect an update is needed for double slash comments', () => {
+      const FILE_LINES = [
+        '// Some license 2015-2016'
+      ];
+      const LICENSE_LINE_RANGE = { start: 0, end: 2, foundMatch: true };
+      const LICENSE_DATA = {
+        text: [
+          '/**',
+          ' * Some license 2015-2017',
+          ' */'
+        ],
+        yearLineNumber: 1,
+        error: null,
+        success: true
+      };
+      let updateStatus = licenseCheck.licenseRangeNeedsUpdating(FILE_LINES, LICENSE_DATA, LICENSE_LINE_RANGE);
+      updateStatus.needsUpdate.should.be.true();
+    });
+
+    it('should detect an update is needed for leaky multiline comments', () => {
+      const FILE_LINES = [
+        '/**',
+        ' * Some license 2015-2016',
+        ' */'
+      ];
+      const LICENSE_LINE_RANGE = { start: 0, end: 2, foundMatch: true };
+      const LICENSE_DATA = {
+        text: [
+          '/**',
+          ' * Some license 2015-2017',
+          ' *'
+        ],
+        yearLineNumber: 1,
+        error: null,
+        success: true
+      };
+      let updateStatus = licenseCheck.licenseRangeNeedsUpdating(FILE_LINES, LICENSE_DATA, LICENSE_LINE_RANGE);
+      updateStatus.needsUpdate.should.be.true();
+    });
+
+    it('should not want to update an empty file', () => {
+      const FILE_LINES = [''];
+      const LICENSE_LINE_RANGE = { start: 0, end: 0, foundMatch: true };
+      const LICENSE_DATA = { text: [''], yearLineNumber: 0, error: null, success: true };
+      let updateStatus = licenseCheck.licenseRangeNeedsUpdating(FILE_LINES, LICENSE_DATA, LICENSE_LINE_RANGE);
+      updateStatus.needsUpdate.should.be.false();
+    });
+  });
+
+  describe('integration test', () => {
+    const fs = require('fs');
+    const glob = require('glob');
+    const BROKEN_IDENTIFIER = '-broken.js';
+    const EXPECTED_FIXED_IDENTIFIER = '-fixed.js';
+    const UPDATABLE_FOLDER = './integration-test/specs/license-check/resources/source-files/updatable-files/';
+    const LICENSE_PATH = './integration-test/specs/license-check/resources/licenses/GOOD_LICENSE';
+    const LICENSE_DATA = licenseCheck.readLicenseFile(LICENSE_PATH);
+
+    let files = glob.sync(UPDATABLE_FOLDER + '*' + BROKEN_IDENTIFIER);
+    files.forEach((filePath) => {
+      it('should properly fix "' + filePath + '"', () => {
+        let tmpFileLines = licenseCheck.readFileLinesToArray(filePath);
+        let tmpLicenseLineRange = licenseCheck.determineLicenseLineRangesAtTopOfFile(tmpFileLines);
+        let updateStatus = licenseCheck.licenseRangeNeedsUpdating(tmpFileLines, tmpLicenseLineRange, LICENSE_DATA);
+        updateStatus.needsUpdate.should.be.true();
+
+        let outputText = licenseCheck.generateFileOutputText(tmpFileLines, tmpLicenseLineRange, LICENSE_DATA);
+
+        let fixedPath = filePath.slice(0, filePath.length - BROKEN_IDENTIFIER.length) + EXPECTED_FIXED_IDENTIFIER;
+        let expectedText = fs.readFileSync(fixedPath, 'utf8');
+        outputText.should.be.eql(expectedText);
+      });
+    });
+  });
+});
--- a/integration-test/specs/license-check/resources/licenses/GOOD_LICENSE	Wed Jul 19 15:48:35 2017 -0400
+++ b/integration-test/specs/license-check/resources/licenses/GOOD_LICENSE	Thu Jul 20 13:49:48 2017 -0400
@@ -1,4 +1,4 @@
-Copyright 2015-2016 by some company.
+Copyright 2015-2017 by some company.
 
 This is a fake license
 used in unit
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/integration-test/specs/license-check/resources/source-files/updatable-files/corruptDate-broken.js	Thu Jul 20 13:49:48 2017 -0400
@@ -0,0 +1,13 @@
+/**
+ * Copyright 201-2015 by some company.
+ *
+ * This is a fake license
+ * used in unit
+ *      testing.
+ */
+
+
+
+function f (x) {
+  return x;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/integration-test/specs/license-check/resources/source-files/updatable-files/corruptDate-fixed.js	Thu Jul 20 13:49:48 2017 -0400
@@ -0,0 +1,13 @@
+/**
+ * Copyright 2015-2017 by some company.
+ *
+ * This is a fake license
+ * used in unit
+ *       testing.
+ */
+
+
+
+function f (x) {
+  return x;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/integration-test/specs/license-check/resources/source-files/updatable-files/noEndingJavadoc-broken.js	Thu Jul 20 13:49:48 2017 -0400
@@ -0,0 +1,6 @@
+/**
+ * Copyright 2015-2017 by some company.
+ *
+ * This is a fake license
+ * used in unit
+ *       testing.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/integration-test/specs/license-check/resources/source-files/updatable-files/noEndingJavadoc-fixed.js	Thu Jul 20 13:49:48 2017 -0400
@@ -0,0 +1,7 @@
+/**
+ * Copyright 2015-2017 by some company.
+ *
+ * This is a fake license
+ * used in unit
+ *       testing.
+ */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/integration-test/specs/license-check/resources/source-files/updatable-files/offsetFromTop-broken.js	Thu Jul 20 13:49:48 2017 -0400
@@ -0,0 +1,12 @@
+
+
+
+/**
+ * Copyright 2015-2017 by some company.
+ *
+ * This is a fake license
+ * used in unit
+ *      testing.
+ */
+
+var x = 5;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/integration-test/specs/license-check/resources/source-files/updatable-files/offsetFromTop-fixed.js	Thu Jul 20 13:49:48 2017 -0400
@@ -0,0 +1,9 @@
+/**
+ * Copyright 2015-2017 by some company.
+ *
+ * This is a fake license
+ * used in unit
+ *       testing.
+ */
+
+var x = 5;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/integration-test/specs/license-check/resources/source-files/updatable-files/outOfDate-broken.js	Thu Jul 20 13:49:48 2017 -0400
@@ -0,0 +1,9 @@
+/**
+ * Copyright 2015-2016 by some company.
+ *
+ * This is a fake license
+ * used in unit
+ *      testing.
+ */
+
+var xyz = 'hi';
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/integration-test/specs/license-check/resources/source-files/updatable-files/outOfDate-fixed.js	Thu Jul 20 13:49:48 2017 -0400
@@ -0,0 +1,9 @@
+/**
+ * Copyright 2015-2017 by some company.
+ *
+ * This is a fake license
+ * used in unit
+ *       testing.
+ */
+
+var xyz = 'hi';
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/integration-test/specs/license-check/resources/source-files/updatable-files/slashComments-broken.js	Thu Jul 20 13:49:48 2017 -0400
@@ -0,0 +1,7 @@
+// Copyright 2015-2017 by some company.
+//
+// This is a fake license
+// used in unit
+//      testing.
+
+let a = null;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/integration-test/specs/license-check/resources/source-files/updatable-files/slashComments-fixed.js	Thu Jul 20 13:49:48 2017 -0400
@@ -0,0 +1,9 @@
+/**
+ * Copyright 2015-2017 by some company.
+ *
+ * This is a fake license
+ * used in unit
+ *       testing.
+ */
+
+let a = null;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/integration-test/specs/license-check/resources/source-files/updatable-files/slightlyCorrupt-broken.js	Thu Jul 20 13:49:48 2017 -0400
@@ -0,0 +1,16 @@
+/**
+ * Copyright 2015-2017 by some company.
+ *
+ * This is a fake license
+ * used in unit
+ */
+
+
+
+
+var f = () => { };
+
+
+
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/integration-test/specs/license-check/resources/source-files/updatable-files/slightlyCorrupt-fixed.js	Thu Jul 20 13:49:48 2017 -0400
@@ -0,0 +1,17 @@
+/**
+ * Copyright 2015-2017 by some company.
+ *
+ * This is a fake license
+ * used in unit
+ *       testing.
+ */
+
+
+
+
+var f = () => { };
+
+
+
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/integration-test/specs/license-check/resources/source-files/updatable-files/veryCorrupt-broken.js	Thu Jul 20 13:49:48 2017 -0400
@@ -0,0 +1,12 @@
+/**
+ * Copyright 2014-2017 by some company.
+ *
+ * a
+ *  9aid    ; 
+
+
+var x = 5;
+// 
+h 
+
+        /
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/integration-test/specs/license-check/resources/source-files/updatable-files/veryCorrupt-fixed.js	Thu Jul 20 13:49:48 2017 -0400
@@ -0,0 +1,7 @@
+/**
+ * Copyright 2015-2017 by some company.
+ *
+ * This is a fake license
+ * used in unit
+ *       testing.
+ */
--- a/license-check/license-check.js	Wed Jul 19 15:48:35 2017 -0400
+++ b/license-check/license-check.js	Thu Jul 20 13:49:48 2017 -0400
@@ -190,7 +190,212 @@
   return returnResult;
 }
 
+/**
+ * Reads the file at the path provided to an array. If there are any errors
+ * reading the file, null is returned. Note that the lines do not contain
+ * any line endings, so [\r]\n's are trimmed away.
+ * @param {string} filePath The path of the file
+ * @return An array of lines (with no \n's or \r\n's), or null on error.
+ */
+function readFileLinesToArray (filePath) {
+  try {
+    return fs.readFileSync(filePath, 'utf8').split(/\r?\n/);
+  } catch (err) {
+    return null;
+  }
+}
+
+/**
+ * A helper function to edit the license range object based on information of
+ * the javadoc starting at the provided line offset.
+ * @param {number} lineOffset The offset in the fileLineArray parameter.
+ * @param {string[]} fileLineArray A list of all the lines
+ * @return The updated licenseRange for the javadoc comment range.
+ */
+function findRangeFromJavadocComments (lineOffset, fileLineArray) {
+  let startLine = lineOffset;
+  lineOffset++;
+
+  while (lineOffset < fileLineArray.length) {
+    if (fileLineArray[lineOffset].startsWith(' */')) {
+      break;
+    }
+    lineOffset++;
+  }
+
+  let licenseRange = { start: startLine, end: lineOffset, foundMatch: true };
+
+  // In the corner case where it's a malformed header and runs off the end
+  // due to a lack of terminating */ then we want to correct the line ending
+  // since it's one ahead due to how it exited the while loop.
+  if (lineOffset >= fileLineArray.length) {
+    licenseRange.end--;
+  }
+
+  return licenseRange;
+}
+
+/**
+ * A helper function to edit the license range object based on information of
+ * the double slash comments starting at the provided line offset.
+ * @param {number} lineOffset The offset in the fileLineArray parameter.
+ * @param {string[]} fileLineArray A list of all the lines
+ * @return The updated licenseRange for the double slash comment range.
+ */
+function findRangeFromDoubleSlashComments (lineOffset, fileLineArray) {
+  let startLine = lineOffset;
+  lineOffset++;
+
+  while (lineOffset < fileLineArray.length && fileLineArray[lineOffset].startsWith('//')) {
+    lineOffset++;
+  }
+
+  return { start: startLine, end: lineOffset - 1, foundMatch: true };
+}
+
+/**
+ * Checks if the license is at the top of the file, if so then the range of the
+ * license lines are provided in the file in the returned object. If any error
+ * occurs, the 'error' field is set with a reason (as a string).
+ * @param {string[]} fileLineArray A list of all the lines, generated by
+ * the readFileLinesToArray() function.
+ * @return An object that contains three fields, 'start' and 'end' which are
+ * indices of the file lines from where the license starts and ends. The 'end'
+ * field will always be inclusive, so lines[end] will never go out of bounds.
+ * If there is an issue, 'foundMatch' is set to false (otherwise its true).
+ */
+function determineLicenseLineRangesAtTopOfFile (fileLineArray) {
+  let licenseRange = { start: 0, end: 0, foundMatch: false };
+  let lineOffset = 0;
+
+  // Skip empty lines at the top if any.
+  while (lineOffset < fileLineArray.length) {
+    let currentLine = fileLineArray[lineOffset].replace(/^\s+$/, '');
+    if (currentLine !== '') {
+      break;
+    }
+    lineOffset++;
+  }
+
+  if (lineOffset === fileLineArray.length) {
+    return licenseRange;
+  }
+
+  if (fileLineArray[lineOffset].startsWith('/**')) {
+    return findRangeFromJavadocComments(lineOffset, fileLineArray);
+  }
+
+  if (fileLineArray[lineOffset].startsWith('//')) {
+    return findRangeFromDoubleSlashComments(lineOffset, fileLineArray);
+  }
+
+  return licenseRange;
+}
+
+/**
+ * Checks if the range provided for the file is in need of fixing.
+ * @param {string[]} fileLines The lines returned from readFileLinesToArray().
+ * @param {Object} licenseLineRange The license file comment range determined
+ * from determineLicenseLineRangesAtTopOfFile(). This should not have an error
+ * set or the function will return true early.
+ * @param {Object} licenseData The license data object for the
+ * target file from the readLicenseFile() function.
+ * @return A 'updateStatus' object which has two fields 'needsUpdate' (bool)
+ * and 'updateReason' (string). If no updates are needed, then updateReason is
+ * an empty string.
+ */
+function licenseRangeNeedsUpdating (fileLines, licenseLineRange, licenseData) {
+  // We do not want to update empty files.
+  if (fileLines.length === 1 && fileLines[0] === '') {
+    return { needsUpdate: false, updateReason: '' };
+  }
+
+  if (!licenseLineRange.foundMatch ||
+      licenseLineRange.start > 0 ||
+      !fileLines[licenseLineRange.start].startsWith('/**') ||
+      !fileLines[licenseLineRange.end].startsWith(' */')) {
+    return { needsUpdate: true, updateReason: 'Missing license header' };
+  }
+
+  let licenseDataIndex = 0;
+  for (let index = licenseLineRange.start; index <= licenseLineRange.end; index++) {
+    if (fileLines[index] !== licenseData.text[licenseDataIndex]) {
+      return { needsUpdate: true, updateReason: 'Line ' + (index + 1) + ' is different to the license' };
+    }
+    licenseDataIndex++;
+  }
+
+  return { needsUpdate: false, updateReason: '' };
+}
+
+/**
+ * Generates the text output from the provided data that should be written into
+ * the target file.
+ * @param {string[]} fileLines The lines of the file at the file path,
+ * which should be what was retrieved from readFileLinesToArray(filePath).
+ * @param {Object} licenseLineRange The object with license range
+ * information from the determineLicenseLineRangesAtTopOfFile() function.
+ * @param {Object} licenseData The license data object for the
+ * target file from the readLicenseFile() function.
+ * @return The text that can be written to a file.
+ */
+function generateFileOutputText (fileLines, licenseLineRange, licenseData) {
+  let textToWriteAsLicenseHeader = licenseData.text.join('\n') + '\n';
+  let fileBodyText = '';
+
+  if (licenseLineRange.foundMatch) {
+    // Since we want to replace it, we will want to remove lines 0 -> end, as any
+    // extra \n's in there should be removed along with the license. This will
+    // also handle the case of just replacing the license at the top.
+    // We made sure that there isn't source code or anything important before the
+    // starting line, so it is okay to toss that out since it must be empty space
+    // instead of something important.
+    fileBodyText = fileLines.slice(licenseLineRange.end + 1).join('\n');
+  } else {
+    // Because we're adding it to the top, we want to put an extra line between
+    // the new license and the source code, so we add a '\n' at the beginning.
+    fileBodyText = '\n' + fileLines.join('\n');
+  }
+
+  return textToWriteAsLicenseHeader + fileBodyText;
+}
+
+/**
+ * Gets the current year from the arguments provided. If it is not in the args
+ * then it will return whatever the current year is.
+ * @param {string[]} args The runtime arguments (such as: process.argv.slice(2))
+ * of which the year will be extracted from.
+ * @return The year as an integer.
+ * @throws {string} If the year provided on the command line is not between the
+ * year 2000 and 3000. The thrown object is a string, which is the reason.
+ */
+function getCurrentYearFromArgs (args) {
+  if (args.length === 0) {
+    return new Date().getFullYear();
+  } else if (args.length > 1) {
+    throw new Error('Currently only support at most one argument, which should be the year');
+  }
+
+  let yearArg = args[0];
+  if (/^\d{4}$/.test(yearArg)) {
+    let year = parseInt(yearArg);
+    if (isNaN(year)) {
+      throw new Error('Year argument is not a number: ' + yearArg);
+    } else if (year < 2000 || year >= 3000) {
+      throw new Error('Year argument is out of range (should be a year in the 2000\'s)');
+    }
+    return year;
+  }
+
+  throw new Error('Expected argument to be a four digit year');
+}
+
 
 exports.readLicenseFile = readLicenseFile;
 exports.checkFileForLicense = checkFileForLicense;
 exports.walkDirectoryAndCheckLicenses = walkDirectoryAndCheckLicenses;
+exports.readFileLinesToArray = readFileLinesToArray;
+exports.determineLicenseLineRangesAtTopOfFile = determineLicenseLineRangesAtTopOfFile;
+exports.licenseRangeNeedsUpdating = licenseRangeNeedsUpdating;
+exports.generateFileOutputText = generateFileOutputText;
+exports.getCurrentYearFromArgs = getCurrentYearFromArgs;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/license-check/perform-license-update.js	Thu Jul 20 13:49:48 2017 -0400
@@ -0,0 +1,129 @@
+/**
+ * Copyright 2012-2017 Red Hat, Inc.
+ *
+ * Thermostat is distributed under the GNU General Public License,
+ * version 2 or any later version (with a special exception described
+ * below, commonly known as the "Classpath Exception").
+ *
+ * A copy of GNU General Public License (GPL) is included in this
+ * distribution, in the file COPYING.
+ *
+ * Linking Thermostat code with other modules is making a combined work
+ * based on Thermostat.  Thus, the terms and conditions of the GPL
+ * cover the whole combination.
+ *
+ * As a special exception, the copyright holders of Thermostat give you
+ * permission to link this code with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on Thermostat code.  If you modify Thermostat, you may
+ * extend this exception to your version of the software, but you are
+ * not obligated to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+const fs = require('fs');
+const glob = require('glob');
+const licenseChecker = require('./license-check.js');
+const SCRIPT_TERMINATION_EXIT_STATUS = 1;
+const LICENSE_FILE_PATH = 'LICENSE';
+const CURRENT_YEAR = licenseChecker.getCurrentYearFromArgs(process.argv.slice(2));
+
+function exitWithErrorMessage (msg) {
+  console.error(msg);
+  process.exit(SCRIPT_TERMINATION_EXIT_STATUS);
+}
+
+function writeTextToFile (filePath, outputText) {
+  try {
+    fs.truncateSync(filePath);
+    fs.writeFileSync(filePath, outputText);
+    return true;
+  } catch (ex) {
+    return false;
+  }
+}
+
+function updateLicenseYearIfOutOfDate (licenseText, licenseStartYear, licenseEndYear) {
+  if (CURRENT_YEAR.toString() === licenseEndYear.toString()) {
+    return;
+  }
+
+  console.info('Updating license header to year ' + CURRENT_YEAR);
+
+  let licenseYearStr = licenseStartYear + '-' + licenseEndYear;
+  let newYearStr = licenseStartYear + '-' + CURRENT_YEAR.toString();
+  licenseText = licenseText.replace(licenseYearStr, newYearStr);
+
+  try {
+    writeTextToFile(LICENSE_FILE_PATH, licenseText);
+  } catch (err) {
+    exitWithErrorMessage('Unable to update project license file to latest year: ' + err);
+  }
+}
+
+// Make sure the license is at the current year.
+(() => {
+  let yearRegex = /(2\d{3})-(2\d{3})/;
+
+  // Note that this can throw on linux/mac/windows, but won't on BSD. We let
+  // the exception be uncaught and have the script invoker read the stack trace
+  // to figure out what they did wrong (ex: bad file path, bad permissions).
+  let licenseText = fs.readFileSync(LICENSE_FILE_PATH, 'utf-8');
+  if (licenseText === null) {
+    exitWithErrorMessage('Error trying to read license file');
+  }
+
+  let matches = yearRegex.exec(licenseText);
+  if (matches === null) {
+    exitWithErrorMessage('Cannot find year line in the license');
+  }
+  let licenseStartYear = matches[1];
+  let licenseEndYear = matches[2];
+
+  updateLicenseYearIfOutOfDate(licenseText, licenseStartYear, licenseEndYear);
+})();
+
+// Scan and update the files.
+(() => {
+  let licensesInNeedOfFixing = 0;
+  let licensesFixed = 0;
+
+  let licenseData = licenseChecker.readLicenseFile(LICENSE_FILE_PATH);
+  if (!licenseData.success) {
+    exitWithErrorMessage(licenseData.error);
+  }
+
+  ['*.js', 'src/**/*.js', 'license-check/**/*.js', 'integration-test/specs/license-check/*.js'].forEach((folderPath) => {
+    glob.sync(folderPath).forEach(filePath => {
+      let fileLines = licenseChecker.readFileLinesToArray(filePath);
+      let licenseLineRange = licenseChecker.determineLicenseLineRangesAtTopOfFile(fileLines);
+
+      let updateStatus = licenseChecker.licenseRangeNeedsUpdating(fileLines, licenseLineRange, licenseData);
+      if (updateStatus.needsUpdate) {
+        licensesInNeedOfFixing++;
+        console.info('Fixing license (' + updateStatus.updateReason + ') at: ' + filePath);
+
+        let outputLines = licenseChecker.generateFileOutputText(fileLines, licenseLineRange, licenseData);
+        let writeResult = writeTextToFile(filePath, outputLines);
+        if (writeResult) {
+          licensesFixed++;
+        } else {
+          console.error('Unable to write changes to fix license at: ' + filePath);
+        }
+      }
+    });
+  });
+
+  if (licensesInNeedOfFixing === 0) {
+    console.info('All licenses are up-to-date');
+  } else if (licensesInNeedOfFixing === licensesFixed) {
+    console.info('Fixed ' + licensesFixed + ' license' + (licensesFixed !== 1 ? 's' : ''));
+  } else {
+    let licensesNotFixed = licensesInNeedOfFixing - licensesFixed;
+    exitWithErrorMessage('Unable to fix ' + licensesNotFixed + '/' + licensesInNeedOfFixing + ' licenses');
+  }
+})();
--- a/package.json	Wed Jul 19 15:48:35 2017 -0400
+++ b/package.json	Thu Jul 20 13:49:48 2017 -0400
@@ -71,6 +71,7 @@
   "scripts": {
     "clean": "\"$(npm bin)/rimraf\" checkstyle coverage dist test-reports",
     "license-check": "node license-check/perform-license-check.js",
+    "license-update": "node license-check/perform-license-update.js",
     "prelint": "\"$(npm bin)/mkdirp\" checkstyle",
     "lint-js": "\"$(npm bin)/eslint\" --ext '!.spec.js' -c .eslintrc.yaml  src || npm run lint-js-checkstyle",
     "lint-js-checkstyle": "\"$(npm bin)/eslint\" -c .eslintrc.yaml -f checkstyle -o checkstyle/checkstyle-src.xml src",