Repository: porscheinformatik/sonarqube-licensecheck
Pull request #76 (2019-07-05 12:06:14)

Code cleanup - use new hashCode and equals features


Patch:
diff --git a/README.md b/README.md
index e3b3083..abf7c21 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,7 @@ SonarQube License-Check
 ===================
 
 [![Build Status](https://travis-ci.org/porscheinformatik/sonarqube-licensecheck.png?branch=master)](https://travis-ci.org/porscheinformatik/sonarqube-licensecheck)
+[![Maintainability](https://api.codeclimate.com/v1/badges/6ac787bb79b43e39c367/maintainability)](https://codeclimate.com/github/porscheinformatik/sonarqube-licensecheck/maintainability)
 
 This [SonarQube](http://www.sonarqube.org/) plugin ensures that projects in an organization adhere to a set of
 standard libraries and versions. This enables the governance of the used libraries and licences.
@@ -13,7 +14,7 @@ This software is licensed under the [Apache Software License, Version 2.0](http:
 ## Compatibility
 
 This plugin is compatible:
- 
+
  * 1.x versions with SonarQube >= 5.3 and < 6.
  * 2.x version with SonarQube >= 6.5 and < 7.
  * 3.x version with SonarQube >= 6.7 LTS and < 8.
@@ -33,8 +34,8 @@ After booting the SonarQube Server with the License-Check Plugin two new options
 <b>Administration</b>.
 
 * Within the general settings the plugin can be manually enabled or disabled. By default it is enabled.
-* All other configuration is under License-Check specific settings (Configuration > License Check). 
-  * Under "Licenses" you can allow or disallow licenses globally and add/edit the list of known licenses. 
+* All other configuration is under License-Check specific settings (Configuration > License Check).
+  * Under "Licenses" you can allow or disallow licenses globally and add/edit the list of known licenses.
   * Under "Project Licenses" you can allow and disallow licenses for a specific project.
   * Under "Maven Dependencies" you can map the Maven key (groupId:artifactId) to licenses using regular expressions. E.g. `^asm:asm$` to "BSD-3-Clause"
   * Under "Maven Licenses" you can map Maven license texts to licenses using regular expressions, e.g. `.*Apache.*2.*` to "Apache-2.0".
@@ -46,15 +47,16 @@ configuration](docs/licensecheck_configuration.jpg)
 
 ### Analysis
 
-The plugin scans for dependencies defined in your project including all transitive dependencies. 
+The plugin scans for dependencies defined in your project including all transitive dependencies.
 
 Currently supported formats are:
 * Maven POM files - all dependencies with scope "compile" and "runtime" are checked
 * NPM package.json files - all dependencies (except "devDependencies") are checked
+  * Note that transitive dependencies are _not_ scanned unless `licensecheck.npm.resolvetransitive` is set to `true`.
 
 ### Project Dashboard
 
-The plugin contains a project dashboard showing a list of dependencies with version and a list of all used licences. Each table shows the status of the license 
+The plugin contains a project dashboard showing a list of dependencies with version and a list of all used licences. Each table shows the status of the license
 (allowed, not allowed, not found). You can also export the data to Excel.
 
 Example for "Dependencies" table:
diff --git a/src/main/java/at/porscheinformatik/sonarqube/licensecheck/Dependency.java b/src/main/java/at/porscheinformatik/sonarqube/licensecheck/Dependency.java
index ddc50ab..48c35ee 100644
--- a/src/main/java/at/porscheinformatik/sonarqube/licensecheck/Dependency.java
+++ b/src/main/java/at/porscheinformatik/sonarqube/licensecheck/Dependency.java
@@ -5,6 +5,7 @@ import java.io.StringWriter;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
+import java.util.Objects;
 import java.util.TreeSet;
 
 import javax.json.Json;
@@ -80,66 +81,26 @@ public class Dependency implements Comparable<Dependency>
     }
 
     @Override
-    public int hashCode()
-    {
-        final int prime = 31;
-        int result = 1;
-        result = (prime * result) + ((license == null) ? 0 : license.hashCode());
-        result = (prime * result) + ((name == null) ? 0 : name.hashCode());
-        result = (prime * result) + ((version == null) ? 0 : version.hashCode());
-        return result;
-    }
-
-    @Override
-    public boolean equals(Object obj)
+    public boolean equals(Object o)
     {
-        if (this == obj)
+        if (this == o)
         {
             return true;
         }
-        if (obj == null)
-        {
-            return false;
-        }
-        if (getClass() != obj.getClass())
-        {
-            return false;
-        }
-        Dependency other = (Dependency) obj;
-        if (license == null)
-        {
-            if (other.license != null)
-            {
-                return false;
-            }
-        }
-        else if (!license.equals(other.license))
-        {
-            return false;
-        }
-        if (name == null)
-        {
-            if (other.name != null)
-            {
-                return false;
-            }
-        }
-        else if (!name.equals(other.name))
+        if (o == null || getClass() != o.getClass())
         {
             return false;
         }
-        if (version == null)
-        {
-            if (other.version != null)
-            {
-                return false;
-            }
-        }
-        else if (!version.equals(other.version))
-        {
-            return false;
-        }
-        return true;
+        Dependency that = (Dependency) o;
+        return Objects.equals(name, that.name) &&
+            Objects.equals(version, that.version) &&
+            Objects.equals(license, that.license);
+    }
+
+    @Override
+    public int hashCode()
+    {
+        return Objects.hash(name, version, license);
     }
 
     @Override
diff --git a/src/main/java/at/porscheinformatik/sonarqube/licensecheck/LicenseCheckPropertyKeys.java b/src/main/java/at/porscheinformatik/sonarqube/licensecheck/LicenseCheckPropertyKeys.java
index eae2ec8..e77b657 100644
--- a/src/main/java/at/porscheinformatik/sonarqube/licensecheck/LicenseCheckPropertyKeys.java
+++ b/src/main/java/at/porscheinformatik/sonarqube/licensecheck/LicenseCheckPropertyKeys.java
@@ -9,4 +9,6 @@ public class LicenseCheckPropertyKeys
     public static final String ALLOWED_DEPENDENCIES_KEY = "licensecheck.alloweddependencies";
 
     public static final String PROJECT_LICENSE_KEY = "licensecheck.projectlicense";
+
+    public static final String NPM_RESOLVE_TRANSITVE_DEPS = "licensecheck.npm.resolvetransitive";
 }
diff --git a/src/main/java/at/porscheinformatik/sonarqube/licensecheck/LicenseCheckSensor.java b/src/main/java/at/porscheinformatik/sonarqube/licensecheck/LicenseCheckSensor.java
index 4378ad5..83d37c4 100644
--- a/src/main/java/at/porscheinformatik/sonarqube/licensecheck/LicenseCheckSensor.java
+++ b/src/main/java/at/porscheinformatik/sonarqube/licensecheck/LicenseCheckSensor.java
@@ -2,7 +2,6 @@ package at.porscheinformatik.sonarqube.licensecheck;
 
 import static java.util.Collections.newSetFromMap;
 
-import java.util.Collections;
 import java.util.Set;
 import java.util.TreeSet;
 import java.util.concurrent.ConcurrentHashMap;
@@ -44,7 +43,7 @@ public class LicenseCheckSensor implements Sensor
         this.settings = settings;
         this.validateLicenses = validateLicenses;
         this.scanners = new Scanner[]{
-            new PackageJsonDependencyScanner(),
+            new PackageJsonDependencyScanner(settings.getBoolean(LicenseCheckPropertyKeys.NPM_RESOLVE_TRANSITVE_DEPS)),
             new MavenDependencyScanner(mavenLicenseService, mavenDependencyService)};
     }
 
diff --git a/src/main/java/at/porscheinformatik/sonarqube/licensecheck/ValidateLicenses.java b/src/main/java/at/porscheinformatik/sonarqube/licensecheck/ValidateLicenses.java
index f2494ff..2850108 100644
--- a/src/main/java/at/porscheinformatik/sonarqube/licensecheck/ValidateLicenses.java
+++ b/src/main/java/at/porscheinformatik/sonarqube/licensecheck/ValidateLicenses.java
@@ -85,15 +85,15 @@ public class ValidateLicenses
             {
                 String notAllowedLicense = "";
 
-                for(License element : licensesContainingDependency) {
-                    if(element.getStatus() == "false") {
+                for (License element : licensesContainingDependency) {
+                    if (element.getStatus() == "false") {
                         notAllowedLicense += element.getName() + " ";
                     }
                 }
                 licenseNotAllowedIssue(context, dependency, notAllowedLicense);
             }
         }
-    }    
+    }
 
     private boolean checkSpdxLicense(String spdxLicenseString, List<License> licenses)
     {
@@ -115,7 +115,7 @@ public class ValidateLicenses
             .isPresent();
     }
 
-    private boolean checkSpdxLicenseWithOr(String spdxLicenseString, List<License> licenses){
+    private boolean checkSpdxLicenseWithOr(String spdxLicenseString, List<License> licenses) {
         String[] orLicenses = spdxLicenseString.replace("(", "").replace(")", "").split(" OR ");
         return licenses
             .stream()
@@ -138,7 +138,7 @@ public class ValidateLicenses
         {
             return true;
         }
-        else if(foundLicenses.size() == count)
+        else if (foundLicenses.size() == count)
         {
             // NOT ALLOWED
             return false;
@@ -177,7 +177,7 @@ public class ValidateLicenses
         issue.save();
     }
 
-    private static boolean contains(String[] items, String valueToFind) 
+    private static boolean contains(String[] items, String valueToFind)
     {
         for (String item : items)
         {
diff --git a/src/main/java/at/porscheinformatik/sonarqube/licensecheck/interfaces/Scanner.java b/src/main/java/at/porscheinformatik/sonarqube/licensecheck/interfaces/Scanner.java
index 9fdddfe..f3af084 100644
--- a/src/main/java/at/porscheinformatik/sonarqube/licensecheck/interfaces/Scanner.java
+++ b/src/main/java/at/porscheinformatik/sonarqube/licensecheck/interfaces/Scanner.java
@@ -1,11 +1,11 @@
 package at.porscheinformatik.sonarqube.licensecheck.interfaces;
 
 import java.io.File;
-import java.util.List;
+import java.util.Set;
 
 import at.porscheinformatik.sonarqube.licensecheck.Dependency;
 
 public interface Scanner
 {
-    List<Dependency> scan(File moduleDir);
+    Set<Dependency> scan(File moduleDir);
 }
diff --git a/src/main/java/at/porscheinformatik/sonarqube/licensecheck/license/License.java b/src/main/java/at/porscheinformatik/sonarqube/licensecheck/license/License.java
index 9da7132..22dc06a 100644
--- a/src/main/java/at/porscheinformatik/sonarqube/licensecheck/license/License.java
+++ b/src/main/java/at/porscheinformatik/sonarqube/licensecheck/license/License.java
@@ -6,6 +6,7 @@ import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.TreeSet;
 
 import javax.json.Json;
@@ -61,17 +62,6 @@ public class License implements Comparable<License>
         this.status = status;
     }
 
-    @Override
-    public int hashCode()
-    {
-        final int prime = 31;
-        int result = 1;
-        result = (prime * result) + ((identifier == null) ? 0 : identifier.hashCode());
-        result = (prime * result) + ((name == null) ? 0 : name.hashCode());
-        result = (prime * result) + ((status == null) ? 0 : status.hashCode());
-        return result;
-    }
-
     @Override
     public String toString()
     {
@@ -80,38 +70,46 @@ public class License implements Comparable<License>
 
     public static List<License> fromString(String serializedLicensesString)
     {
-        List<License> licenses = new ArrayList<>();
+        if (serializedLicensesString == null)
+        {
+            return new ArrayList<>();
+        }
 
-        if (serializedLicensesString != null)
+        if (serializedLicensesString.startsWith("["))
         {
-            if (serializedLicensesString.startsWith("["))
+            List<License> licenses = new ArrayList<>();
+
+            try (JsonReader jsonReader = Json.createReader(new StringReader(serializedLicensesString)))
             {
-                try (JsonReader jsonReader = Json.createReader(new StringReader(serializedLicensesString)))
+                JsonArray licensesJson = jsonReader.readArray();
+                for (JsonObject licenseJson : licensesJson.getValuesAs(JsonObject.class))
                 {
-                    JsonArray licensesJson = jsonReader.readArray();
-                    for (JsonObject licenseJson : licensesJson.getValuesAs(JsonObject.class))
-                    {
-                        licenses.add(new License(licenseJson.getString("name"), licenseJson.getString("identifier"),
-                            licenseJson.getString("status")));
-                    }
+                    licenses.add(new License(licenseJson.getString("name"), licenseJson.getString("identifier"),
+                        licenseJson.getString("status")));
                 }
             }
-            else if (serializedLicensesString.startsWith("{"))
-            {
-                readLegacyJson(serializedLicensesString, licenses);
-            }
-            else
-            {
-                readLegaySeparated(serializedLicensesString, licenses);
-            }
+            return licenses;
+        }
+        else if (serializedLicensesString.startsWith("{"))
+        {
+            return readLegacyJson(serializedLicensesString);
+        }
+        else
+        {
+            return readLegacySeparated(serializedLicensesString);
         }
-        return licenses;
     }
 
+    /**
+     * @deprecated remove with later release
+     * @param serializedLicensesString setting string
+     * @return a list with licences
+     */
     @Deprecated
-    private static void readLegaySeparated(String serializedLicensesString, List<License> licenses)
+    private static List<License> readLegacySeparated(String serializedLicensesString)
     {
-        // deprecated - remove with later release
+        List<License> licenses = new ArrayList<>();
+
         if (StringUtils.isNotEmpty(serializedLicensesString))
         {
             String[] parts = serializedLicensesString.split(";");
@@ -125,12 +123,20 @@ public class License implements Comparable<License>
                 licenses.add(new License(name, identifier, status));
             }
         }
+
+        return licenses;
     }
 
+    /**
+     * @deprecated remove with later release
+     * @param serializedLicensesString setting string
+     * @return a list with licences
+     */
     @Deprecated
-    private static void readLegacyJson(String serializedLicensesString, List<License> licenses)
+    private static List<License> readLegacyJson(String serializedLicensesString)
     {
-        // deprecated - remove with later release
+        List<License> licenses = new ArrayList<>();
+
         try (JsonReader jsonReader = Json.createReader(new StringReader(serializedLicensesString)))
         {
             JsonObject licensesJson = jsonReader.readObject();
@@ -141,6 +147,8 @@ public class License implements Comparable<License>
                     value.getString("status")));
             }
         }
+
+        return licenses;
     }
 
     public static String createString(Collection<License> licenses)
@@ -172,7 +180,8 @@ public class License implements Comparable<License>
         {
             return 1;
         }
-        else if (this.identifier.compareTo(o.identifier) == 0)
+
+        if (this.identifier.compareTo(o.identifier) == 0)
         {
             if (this.name.compareTo(o.name) == 0)
             {
@@ -190,22 +199,25 @@ public class License implements Comparable<License>
     }
 
     @Override
-    public boolean equals(Object object)
+    public boolean equals(Object o)
     {
-        if (this == object)
+        if (this == o)
         {
             return true;
         }
-        if (object == null)
-        {
-            return false;
-        }
-        if (getClass() != object.getClass())
+        if (o == null || getClass() != o.getClass())
         {
             return false;
         }
+        License license = (License) o;
+        return Objects.equals(name, license.name) &&
+            Objects.equals(identifier, license.identifier) &&
+            Objects.equals(status, license.status);
+    }
 
-        License license = (License) object;
-        return license.identifier.equals(this.identifier) && license.name.equals(this.name);
+    @Override
+    public int hashCode()
+    {
+        return Objects.hash(name, identifier, status);
     }
 }
diff --git a/src/main/java/at/porscheinformatik/sonarqube/licensecheck/maven/MavenDependencyScanner.java b/src/main/java/at/porscheinformatik/sonarqube/licensecheck/maven/MavenDependencyScanner.java
index 13ba27b..5236fdc 100644
--- a/src/main/java/at/porscheinformatik/sonarqube/licensecheck/maven/MavenDependencyScanner.java
+++ b/src/main/java/at/porscheinformatik/sonarqube/licensecheck/maven/MavenDependencyScanner.java
@@ -10,6 +10,7 @@ import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Objects;
 import java.util.Properties;
+import java.util.Set;
 import java.util.function.Function;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
@@ -55,13 +56,16 @@ public class MavenDependencyScanner implements Scanner
     }
 
     @Override
-    public List<Dependency> scan(File moduleDir)
+    public Set<Dependency> scan(File moduleDir)
     {
-        if(!new File(moduleDir, "pom.xml").exists())
+        if (!new File(moduleDir, "pom.xml").exists())
         {
-            return Collections.emptyList();
+            LOGGER.info("No pom.xml file found in {} - skipping Maven dependency scan", moduleDir.getPath());
+            return Collections.emptySet();
         }
 
+        LOGGER.info("Scanning for Maven dependencies");
+
         String userSettings = null;
         String globalSettings = null;
         CommandLine cmd = getCommandLineArgs();
@@ -77,13 +81,13 @@ public class MavenDependencyScanner implements Scanner
             }
         }
 
-        return this.readDependecyList(moduleDir, userSettings, globalSettings)
+        return this.readDependencyList(moduleDir, userSettings, globalSettings)
             .map(this.loadLicenseFromPom(mavenLicenseService.getLicenseMap(), userSettings, globalSettings))
             .map(this::mapMavenDependencyToLicense)
-            .collect(Collectors.toList());
+            .collect(Collectors.toSet());
     }
 
-    private Stream<Dependency> readDependecyList(File moduleDir, String userSettings, String globalSettings)
+    private Stream<Dependency> readDependencyList(File moduleDir, String userSettings, String globalSettings)
     {
         Path tempFile = createTempFile();
         if (tempFile == null)
diff --git a/src/main/java/at/porscheinformatik/sonarqube/licensecheck/mavendependency/MavenDependency.java b/src/main/java/at/porscheinformatik/sonarqube/licensecheck/mavendependency/MavenDependency.java
index 6b02526..12b428b 100644
--- a/src/main/java/at/porscheinformatik/sonarqube/licensecheck/mavendependency/MavenDependency.java
+++ b/src/main/java/at/porscheinformatik/sonarqube/licensecheck/mavendependency/MavenDependency.java
@@ -1,5 +1,7 @@
 package at.porscheinformatik.sonarqube.licensecheck.mavendependency;
 
+import java.util.Objects;
+
 public class MavenDependency implements Comparable<MavenDependency>
 {
     private String key;
@@ -12,16 +14,6 @@ public class MavenDependency implements Comparable<MavenDependency>
         this.license = license;
     }
 
-    @Override
-    public int hashCode()
-    {
-        final int prime = 31;
-        int result = 1;
-        result = (prime * result) + ((key == null) ? 0 : key.hashCode());
-        result = (prime * result) + ((license == null) ? 0 : license.hashCode());
-        return result;
-    }
-
     public String getKey()
     {
         return key;
@@ -55,7 +47,8 @@ public class MavenDependency implements Comparable<MavenDependency>
         {
             return 1;
         }
-        else if (this.license.compareTo(o.license) == 0)
+
+        if (this.license.compareTo(o.license) == 0)
         {
             return this.key.compareTo(o.key);
         }
@@ -66,22 +59,24 @@ public class MavenDependency implements Comparable<MavenDependency>
     }
 
     @Override
-    public boolean equals(Object object)
+    public boolean equals(Object o)
     {
-        if (this == object)
+        if (this == o)
         {
             return true;
         }
-        if (object == null)
-        {
-            return false;
-        }
-        if (getClass() != object.getClass())
+        if (o == null || getClass() != o.getClass())
         {
             return false;
         }
+        MavenDependency that = (MavenDependency) o;
+        return Objects.equals(key, that.key) &&
+            Objects.equals(license, that.license);
+    }
 
-        MavenDependency mavenDependency = (MavenDependency) object;
-        return mavenDependency.license.equals(this.license) && mavenDependency.key.equals(this.key);
+    @Override
+    public int hashCode()
+    {
+        return Objects.hash(key, license);
     }
 }
diff --git a/src/main/java/at/porscheinformatik/sonarqube/licensecheck/mavenlicense/MavenLicense.java b/src/main/java/at/porscheinformatik/sonarqube/licensecheck/mavenlicense/MavenLicense.java
index 6506f70..39f0d2a 100644
--- a/src/main/java/at/porscheinformatik/sonarqube/licensecheck/mavenlicense/MavenLicense.java
+++ b/src/main/java/at/porscheinformatik/sonarqube/licensecheck/mavenlicense/MavenLicense.java
@@ -5,6 +5,7 @@ import java.io.StringWriter;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
+import java.util.Objects;
 import java.util.TreeSet;
 import java.util.regex.Pattern;
 
@@ -55,37 +56,6 @@ public class MavenLicense implements Comparable<MavenLicense>
         }
     }
 
-    @Override
-    public boolean equals(Object object)
-    {
-        if (this == object)
-        {
-            return true;
-        }
-        if (object == null)
-        {
-            return false;
-        }
-        if (getClass() != object.getClass())
-        {
-            return false;
-        }
-
-        MavenLicense mavenLicense = (MavenLicense) object;
-        return mavenLicense.license.equals(this.license)
-            && mavenLicense.regex.toString().equals(this.regex.toString());
-    }
-
-    @Override
-    public int hashCode()
-    {
-        final int prime = 31;
-        int result = 1;
-        result = (prime * result) + ((license == null) ? 0 : license.hashCode());
-        result = (prime * result) + ((regex == null) ? 0 : regex.hashCode());
-        return result;
-    }
-
     public static List<MavenLicense> fromString(String mavenLicenseString)
     {
         List<MavenLicense> mavenLicenses = new ArrayList<>();
@@ -99,7 +69,7 @@ public class MavenLicense implements Comparable<MavenLicense>
                 {
                     JsonObject licenseJson = licensesJson.getJsonObject(i);
                     String regex = null;
-                    try 
+                    try
                     {
                         regex = licenseJson.getString("regex");
                     }
@@ -149,4 +119,26 @@ public class MavenLicense implements Comparable<MavenLicense>
 
         return jsonString.toString();
     }
+
+    @Override
+    public boolean equals(Object o)
+    {
+        if (this == o)
+        {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass())
+        {
+            return false;
+        }
+        MavenLicense that = (MavenLicense) o;
+        return Objects.equals(regex, that.regex) &&
+            Objects.equals(license, that.license);
+    }
+
+    @Override
+    public int hashCode()
+    {
+        return Objects.hash(regex, license);
+    }
 }
diff --git a/src/main/java/at/porscheinformatik/sonarqube/licensecheck/npm/PackageJsonDependencyScanner.java b/src/main/java/at/porscheinformatik/sonarqube/licensecheck/npm/PackageJsonDependencyScanner.java
index bb2e3de..785fc01 100644
--- a/src/main/java/at/porscheinformatik/sonarqube/licensecheck/npm/PackageJsonDependencyScanner.java
+++ b/src/main/java/at/porscheinformatik/sonarqube/licensecheck/npm/PackageJsonDependencyScanner.java
@@ -5,9 +5,9 @@ import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
-import java.util.ArrayList;
 import java.util.Collections;
-import java.util.List;
+import java.util.HashSet;
+import java.util.Set;
 
 import javax.json.Json;
 import javax.json.JsonArray;
@@ -24,89 +24,111 @@ public class PackageJsonDependencyScanner implements Scanner
 {
     private static final Logger LOGGER = LoggerFactory.getLogger(PackageJsonDependencyScanner.class);
 
+    private boolean resolveTransitiveDeps;
+
+    public PackageJsonDependencyScanner(boolean resolveTransitiveDeps)
+    {
+        this.resolveTransitiveDeps = resolveTransitiveDeps;
+    }
+
     @Override
-    public List<Dependency> scan(File file)
+    public Set<Dependency> scan(File moduleDir)
     {
-        File packageJsonFile = new File(file, "package.json");
+        File packageJsonFile = new File(moduleDir, "package.json");
 
-        if (packageJsonFile.exists())
+        if (!packageJsonFile.exists())
         {
-            try (InputStream fis = new FileInputStream(packageJsonFile);
-                JsonReader jsonReader = Json.createReader(fis))
-            {
-                JsonObject jsonObject = jsonReader.readObject();
-                JsonObject jsonObjectDependencies = jsonObject.getJsonObject("dependencies");
-                if (jsonObjectDependencies != null)
-                {
-                    return dependencyParser(jsonObjectDependencies, packageJsonFile);
-                }
-                jsonReader.close();
-            }
-            catch (IOException e)
-            {
-                LOGGER.error("Error reading package.json", e);
-            }
+            LOGGER.info("No package.json file found in {} - skipping NPM dependency scan", moduleDir.getPath());
+            return Collections.emptySet();
         }
-        return Collections.emptyList();
+
+        LOGGER.info("Scanning for NPM dependencies");
+
+        return dependencyParser(moduleDir, packageJsonFile);
     }
 
-    private List<Dependency> dependencyParser(JsonObject jsonDependencies, File packageJsonFile)
+    private Set<Dependency> dependencyParser(File baseDir, File packageJsonFile)
     {
-        List<Dependency> dependencies = new ArrayList<>();
+        Set<Dependency> dependencies = new HashSet<>();
 
-        File nodeModulesFolder = new File(packageJsonFile.getParentFile(), "node_modules");
-        if (nodeModulesFolder.exists() && nodeModulesFolder.isDirectory())
+        try (InputStream fis = new FileInputStream(packageJsonFile);
+            JsonReader jsonReader = Json.createReader(fis))
         {
-            for (String key : jsonDependencies.keySet())
+            JsonObject packageJson = jsonReader.readObject();
+
+            JsonObject packageJsonDependencies = packageJson.getJsonObject("dependencies");
+            if (packageJsonDependencies != null)
             {
-                moduleCheck(nodeModulesFolder, key, dependencies);
+                scanDependencies(baseDir, packageJsonDependencies.keySet(), dependencies);
             }
         }
+        catch (IOException e)
+        {
+            LOGGER.error("Error reading package.json", e);
+        }
 
         return dependencies;
     }
 
-    private static void moduleCheck(File nodeModulesFolder, String identifier, List<Dependency> dependencies)
+    private void scanDependencies(File baseDir, Set<String> packageNames, Set<Dependency> dependencies)
     {
-        File moduleFolder = new File(nodeModulesFolder, identifier);
+        LOGGER.info("Scanning NPM packages " + packageNames);
 
-        if (moduleFolder.exists() && moduleFolder.isDirectory())
+        for (String packageName : packageNames)
         {
-            File packageFile = new File(moduleFolder, "package.json");
+            if (dependencies.stream().anyMatch(d -> packageName.equals(d.getName())))
+            {
+                LOGGER.debug("Package {} has already been encountered and will not be scanned again", packageName);
+                continue;
+            }
 
-            try (InputStream fis = new FileInputStream(packageFile); JsonReader jsonReader = Json.createReader(fis))
+            File packageJsonFile = new File(baseDir, "node_modules/" + packageName + "/package.json");
+            if (!packageJsonFile.exists())
             {
-                JsonObject jsonObject = jsonReader.readObject();
-                if (jsonObject != null)
+                LOGGER.warn("No package.json file found for package {} in node_modules - skipping dependency",
+                        packageName);
+                continue;
+            }
+
+            try (InputStream fis = new FileInputStream(packageJsonFile);
+                JsonReader jsonReader = Json.createReader(fis))
+            {
+                JsonObject packageJson = jsonReader.readObject();
+                if (packageJson != null)
                 {
-                    String license = null;
-                    if (jsonObject.containsKey("license"))
+                    String license = "";
+                    if (packageJson.containsKey("license"))
                     {
-                        license = jsonObject.getString("license");
+                        license = packageJson.getString("license");
                     }
-                    else if (jsonObject.containsKey("licenses"))
+                    else if (packageJson.containsKey("licenses"))
                     {
-                        JsonArray licenses = jsonObject.getJsonArray("licenses");
+                        JsonArray licenses = packageJson.getJsonArray("licenses");
                         if (licenses.size() > 0)
                         {
                             license = licenses.getJsonObject(0).getString("type");
                         }
                     }
 
-                    if (license != null)
+                    dependencies.add(new Dependency(packageName, packageJson.getString("version"), license));
+
+                    if (resolveTransitiveDeps)
                     {
-                        dependencies.add(new Dependency(identifier, jsonObject.getString("version"), license));
+                        JsonObject packageJsonDependencies = packageJson.getJsonObject("dependencies");
+                        if (packageJsonDependencies != null)
+                        {
+                            scanDependencies(baseDir, packageJsonDependencies.keySet(), dependencies);
+                        }
                     }
                 }
-                jsonReader.close();
             }
             catch (FileNotFoundException e)
             {
-                LOGGER.error("Could not find package.json", e);
+                LOGGER.error("Could not load package.json", e);
             }
             catch (Exception e)
             {
-                LOGGER.error("Error adding dependency " + identifier, e);
+                LOGGER.error("Could not check NPM package " + packageName, e);
             }
         }
     }
diff --git a/src/main/java/at/porscheinformatik/sonarqube/licensecheck/projectLicense/ProjectLicense.java b/src/main/java/at/porscheinformatik/sonarqube/licensecheck/projectLicense/ProjectLicense.java
index 3708583..296aeed 100644
--- a/src/main/java/at/porscheinformatik/sonarqube/licensecheck/projectLicense/ProjectLicense.java
+++ b/src/main/java/at/porscheinformatik/sonarqube/licensecheck/projectLicense/ProjectLicense.java
@@ -5,6 +5,7 @@ import java.io.StringWriter;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
+import java.util.Objects;
 import java.util.TreeSet;
 
 import javax.json.Json;
@@ -90,7 +91,8 @@ public class ProjectLicense implements Comparable<ProjectLicense>
         {
             return 1;
         }
-        else if (this.getProjectKey().compareTo(o.getProjectKey()) == 0)
+
+        if (this.getProjectKey().compareTo(o.getProjectKey()) == 0)
         {
             if (this.getLicense().compareTo(o.getLicense()) == 0)
             {
@@ -108,35 +110,25 @@ public class ProjectLicense implements Comparable<ProjectLicense>
     }
 
     @Override
-    public boolean equals(Object object)
+    public boolean equals(Object o)
     {
-        if (this == object)
+        if (this == o)
         {
             return true;
         }
-        if (object == null)
-        {
-            return false;
-        }
-        if (getClass() != object.getClass())
+        if (o == null || getClass() != o.getClass())
         {
             return false;
         }
-
-        ProjectLicense projectLicense = (ProjectLicense) object;
-
-        return projectLicense.getLicense().equals(this.getLicense())
-            && projectLicense.getProjectKey().equals(this.getProjectKey());
+        ProjectLicense that = (ProjectLicense) o;
+        return Objects.equals(projectKey, that.projectKey) &&
+            Objects.equals(license, that.license) &&
+            Objects.equals(status, that.status);
     }
 
     @Override
     public int hashCode()
     {
-        final int prime = 31;
-        int result = 1;
-        result = (prime * result) + ((getLicense() == null) ? 0 : getLicense().hashCode());
-        result = (prime * result) + ((getProjectKey() == null) ? 0 : getProjectKey().hashCode());
-        result = (prime * result) + ((getStatus() == null) ? 0 : getStatus().hashCode());
-        return result;
+        return Objects.hash(projectKey, license, status);
     }
 }


Test patch:
diff --git a/src/test/java/at/porscheinformatik/sonarqube/licensecheck/PackageJsonDependencyScannerTest.java b/src/test/java/at/porscheinformatik/sonarqube/licensecheck/PackageJsonDependencyScannerTest.java
index c01c268..99f929d 100644
--- a/src/test/java/at/porscheinformatik/sonarqube/licensecheck/PackageJsonDependencyScannerTest.java
+++ b/src/test/java/at/porscheinformatik/sonarqube/licensecheck/PackageJsonDependencyScannerTest.java
@@ -1,26 +1,66 @@
 package at.porscheinformatik.sonarqube.licensecheck;
 
 import java.io.File;
-import java.util.List;
+import java.util.Set;
 
-import org.hamcrest.CoreMatchers;
-import org.junit.Assert;
 import org.junit.Test;
 
 import at.porscheinformatik.sonarqube.licensecheck.interfaces.Scanner;
 import at.porscheinformatik.sonarqube.licensecheck.npm.PackageJsonDependencyScanner;
 
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.hamcrest.Matchers.hasSize;
+import static org.junit.Assert.assertThat;
+
 public class PackageJsonDependencyScannerTest
 {
-    final File folder = new File("src/test/resources");
+    private final File folder = new File("src/test/resources");
 
     @Test
     public void testHappyPath()
     {
-        Scanner scanner = new PackageJsonDependencyScanner();
+        Scanner scanner = new PackageJsonDependencyScanner(false);
+
+        Set<Dependency> dependencies = scanner.scan(folder);
+
+        assertThat(dependencies, hasSize(2));
+        assertThat(dependencies, containsInAnyOrder(
+            new Dependency("angular", "1.5.0", "MIT"),
+            new Dependency("arangojs", "5.6.0", "Apache-2.0")));
+    }
+
+    @Test
+    public void testTransitive()
+    {
+        Scanner scanner = new PackageJsonDependencyScanner(true);
+
+        Set<Dependency> dependencies = scanner.scan(folder);
+
+        assertThat(dependencies, hasSize(4));
+        assertThat(dependencies, containsInAnyOrder(
+            new Dependency("angular", "1.5.0", "MIT"),
+            new Dependency("arangojs", "5.6.0", "Apache-2.0"),
+            new Dependency("linkedlist", "1.0.1", "LGPLv3"),
+            new Dependency("retry", "0.10.1", "MIT")));
+    }
+
+    @Test
+    public void testNoPackageJson()
+    {
+        Scanner scanner = new PackageJsonDependencyScanner(false);
+
+        Set<Dependency> dependencies = scanner.scan(new File("src"));
+
+        assertThat(dependencies, hasSize(0));
+    }
+
+    @Test
+    public void testNoNodeModules()
+    {
+        Scanner scanner = new PackageJsonDependencyScanner(false);
 
-        List<Dependency> dependencies = scanner.scan(folder);
+        Set<Dependency> dependencies = scanner.scan(new File(folder, "node_modules/arangojs"));
 
-        Assert.assertThat(dependencies, CoreMatchers.hasItem(new Dependency("angular", "1.5.0", "MIT")));
+        assertThat(dependencies, hasSize(0));
     }
 }
diff --git a/src/test/java/at/porscheinformatik/sonarqube/licensecheck/maven/MavenDependencyScannerTest.java b/src/test/java/at/porscheinformatik/sonarqube/licensecheck/maven/MavenDependencyScannerTest.java
index cb89015..6894084 100644
--- a/src/test/java/at/porscheinformatik/sonarqube/licensecheck/maven/MavenDependencyScannerTest.java
+++ b/src/test/java/at/porscheinformatik/sonarqube/licensecheck/maven/MavenDependencyScannerTest.java
@@ -9,8 +9,8 @@ import java.io.IOException;
 import java.nio.file.Files;
 import java.util.Arrays;
 import java.util.HashMap;
-import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.regex.Pattern;
 
 import org.hamcrest.Matchers;
@@ -39,7 +39,7 @@ public class MavenDependencyScannerTest
         Scanner scanner = new MavenDependencyScanner(licenseService, dependencyService);
 
         // -
-        List<Dependency> dependencies = scanner.scan(moduleDir);
+        Set<Dependency> dependencies = scanner.scan(moduleDir);
 
         assertThat(dependencies.size(), Matchers.greaterThan(0));
 
@@ -66,7 +66,7 @@ public class MavenDependencyScannerTest
 
         File moduleDir = Files.createTempDirectory("lala").toFile();
         moduleDir.deleteOnExit();
-        List<Dependency> dependencies = scanner.scan(moduleDir);
+        Set<Dependency> dependencies = scanner.scan(moduleDir);
 
         assertThat(dependencies.size(), is(0));
     }
diff --git a/src/test/resources/node_modules/arangojs/package.json b/src/test/resources/node_modules/arangojs/package.json
new file mode 100644
index 0000000..0a4db0d
--- /dev/null
+++ b/src/test/resources/node_modules/arangojs/package.json
@@ -0,0 +1,88 @@
+{
+  "name": "arangojs",
+  "version": "5.6.0",
+  "license": "Apache-2.0",
+  "description": "The official ArangoDB JavaScript driver.",
+  "homepage": "https://github.com/arangodb/arangojs",
+  "author": "ArangoDB GmbH",
+  "keywords": [
+    "arango",
+    "arangodb",
+    "aql",
+    "nosql",
+    "client",
+    "driver",
+    "api",
+    "http",
+    "rest"
+  ],
+  "main": "lib/index.js",
+  "browser": {
+    "./lib/util/btoa.js": "./lib/util/btoa.web.js",
+    "./lib/util/bytelength.js": "./lib/util/bytelength.web.js",
+    "./lib/util/multipart.js": "./lib/util/multipart.web.js",
+    "./lib/util/request.js": "./lib/util/request.web.js"
+  },
+  "directories": {
+    "lib": "lib"
+  },
+  "files": [
+    "lib/",
+    "package.json",
+    "arangojs.d.ts",
+    "README.md",
+    "LICENSE"
+  ],
+  "typings": "arangojs.d.ts",
+  "scripts": {
+    "test": "mocha --growl",
+    "lint": "snazzy --verbose src/**/*.js test/**/*.js",
+    "ci": "mocha",
+    "watch": "npm-run-all -p watch:*",
+    "watch:browser": "npm run dist:browser -- --watch",
+    "watch:node": "watch 'npm run dist:node' ./src ./test",
+    "watch:test": "watch 'npm run lint && npm run test' ./src ./test",
+    "dist": "npm-run-all -p dist:*",
+    "dist:browser": "webpack",
+    "dist:node": "babel --compact false -d lib src",
+    "prepublish": "npm run lint && npm run dist && node -e 'require(\"./\");'"
+  },
+  "dependencies": {
+    "linkedlist": "^1.0.1",
+    "retry": "^0.10.0"
+  },
+  "devDependencies": {
+    "babel-cli": "6.22.2",
+    "babel-core": "6.22.1",
+    "babel-loader": "6.2.9",
+    "babel-plugin-add-module-exports": "0.2.1",
+    "babel-plugin-transform-builtin-extend": "1.1.0",
+    "babel-plugin-transform-es2015-modules-commonjs": "6.22.0",
+    "babel-preset-es2015": "6.22.0",
+    "babel-preset-stage-1": "6.22.0",
+    "chai": "3.5.0",
+    "core-js": "2.4.1",
+    "coveralls": "2.11.15",
+    "istanbul": "0.4.5",
+    "json-loader": "0.5.4",
+    "mocha": "3.2.0",
+    "npm-run-all": "4.0.1",
+    "snazzy": "6.0.0",
+    "standard": "8.6.0",
+    "watch": "1.0.1",
+    "webpack": "1.14.0"
+  },
+  "contributors": [
+    {
+      "name": "Alan Plum",
+      "email": "me@pluma.io"
+    }
+  ],
+  "bugs": {
+    "url": "https://github.com/arangodb/arangojs/issues"
+  },
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/arangodb/arangojs.git"
+  }
+}
diff --git a/src/test/resources/node_modules/linkedlist/package.json b/src/test/resources/node_modules/linkedlist/package.json
new file mode 100644
index 0000000..fed11ff
--- /dev/null
+++ b/src/test/resources/node_modules/linkedlist/package.json
@@ -0,0 +1,28 @@
+{
+  "author": "Kilian Ciuffolo <me@nailik.org> (http://nailik.org)",
+  "name": "linkedlist",
+  "licenses": [{
+    "type": "LGPLv3"
+  }],
+  "description": "Array like linked list with iterator",
+  "keywords": ["array", "linked", "list", "linkedlist", "iterator"],
+  "version": "1.0.1",
+  "repository": {
+    "type": "git",
+    "url": "http://github.com/kilianc/node-linkedlist.git"
+  },
+  "main" : "./",
+  "scripts": {
+    "test": "make test"
+  },
+  "engines": {
+    "node": ">= v0.6.x"
+  },
+  "dependencies": {
+    "retry": "^1.2.3"
+  },
+  "devDependencies": {
+    "mocha": "1.0.x",
+    "should": "0.6.1"
+  }
+}
diff --git a/src/test/resources/node_modules/retry/package.json b/src/test/resources/node_modules/retry/package.json
new file mode 100644
index 0000000..77a7d12
--- /dev/null
+++ b/src/test/resources/node_modules/retry/package.json
@@ -0,0 +1,26 @@
+{
+  "author": "Tim Koschützki <tim@debuggable.com> (http://debuggable.com/)",
+  "name": "retry",
+  "description": "Abstraction for exponential and custom retry strategies for failed operations.",
+  "license": "MIT",
+  "version": "0.10.1",
+  "homepage": "https://github.com/tim-kos/node-retry",
+  "repository": {
+    "type": "git",
+    "url": "git://github.com/tim-kos/node-retry.git"
+  },
+  "directories": {
+    "lib": "./lib"
+  },
+  "main": "index",
+  "engines": {
+    "node": "*"
+  },
+  "dependencies": {
+    "linkedlist": "^1.0.1"
+  },
+  "devDependencies": {
+    "fake": "0.2.0",
+    "far": "0.0.1"
+  }
+}
diff --git a/src/test/resources/package.json b/src/test/resources/package.json
index 909f0c9..af1d969 100644
--- a/src/test/resources/package.json
+++ b/src/test/resources/package.json
@@ -6,10 +6,11 @@
   "dependencies": {
     "angular": "^1.4.3",
     "angular-ui-router" : "~0.2.18",
-    "angular-ui-bootstrap" : "1.1.2"
+    "angular-ui-bootstrap" : "1.1.2",
+    "arangojs" : "5.6.0"
   },
   "devDependencies": {
     "gulp": "^3.9.1"
   },
   "license": "MIT"
-}
\ No newline at end of file
+}