Adobe-Consulting-Services/acs-aem-commons

View on GitHub
oakpal-checks/src/main/java/com/adobe/acs/commons/oakpal/checks/ImportedPackages.java

Summary

Maintainability
A
0 mins
Test Coverage
/*
 * ACS AEM Commons
 *
 * Copyright (C) 2013 - 2023 Adobe
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.adobe.acs.commons.oakpal.checks;

import java.io.IOException;
import java.io.InputStream;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import java.util.jar.Manifest;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.json.Json;
import javax.json.JsonArray;
import javax.json.JsonObject;
import javax.json.JsonReader;
import javax.json.JsonString;

import org.apache.jackrabbit.JcrConstants;
import org.apache.jackrabbit.commons.JcrUtils;
import org.apache.jackrabbit.vault.fs.config.MetaInf;
import org.apache.jackrabbit.vault.packaging.PackageId;
import org.apache.jackrabbit.vault.packaging.PackageProperties;
import org.jetbrains.annotations.NotNull;
import org.osgi.framework.Version;
import org.osgi.framework.VersionRange;

import net.adamcin.oakpal.api.ProgressCheck;
import net.adamcin.oakpal.api.ProgressCheckFactory;
import net.adamcin.oakpal.api.Severity;
import net.adamcin.oakpal.api.SimpleProgressCheckFactoryCheck;

public final class ImportedPackages implements ProgressCheckFactory {

    private static final String CONFIG_VERSION = "aemVersion";
    private static final List<String> DEFAULT_VERSIONS = Arrays.asList("6.5");


    @Override
    public ProgressCheck newInstance(JsonObject config) throws Exception {
        final List<String> versions;
        JsonArray versionsFromConfig = config.getJsonArray(CONFIG_VERSION);
        if (versionsFromConfig != null) {
            versions = versionsFromConfig.stream().map(v -> (JsonString) v).map(JsonString::getString).collect(Collectors.toList());
        } else {
            versions = DEFAULT_VERSIONS;
        }
        Map<String, Map<String, Set<Version>>> exportedPackagesByVersion = versions.stream()
                .map(version -> {
                    InputStream inputStream = getClass().getResourceAsStream(String.format("/bundleinfo/%s.json", version));
                    if (inputStream == null) {
                        throw new IllegalArgumentException(String.format("Unknown version %s", version));
                    }
                    try (JsonReader reader = Json.createReader(inputStream)) {
                        JsonObject packageDefinitions = reader.readObject();
                        Map<String, Set<Version>> exportedPackages = new HashMap<>();
                        packageDefinitions.keySet().forEach(key -> {
                            exportedPackages.put(key, packageDefinitions.getJsonArray(key).stream().map(v -> {
                                String str = ((JsonString) v).getString();
                                return new Version(str);
                            }).collect(Collectors.toSet()));
                        });
                        return new AbstractMap.SimpleEntry<>(version, exportedPackages);
                    }
                }).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

        return new Check(exportedPackagesByVersion);

    }

    static final class Check extends SimpleProgressCheckFactoryCheck<ImportedPackages> {

        private final Map<String, Map<String, Set<Version>>> exportedPackagesByVersion;

        private Map<PackageId, List<Set<ImportedPackage>>> importedPackages;

        Check(Map<String, Map<String, Set<Version>>> exportedPackagesByVersion) {
            super(ImportedPackages.class);
            this.exportedPackagesByVersion = new HashMap<>();
            exportedPackagesByVersion.forEach((version, exportedPackages) -> {
                Map<String, Set<Version>> mutableExports = new HashMap<>();
                exportedPackages.forEach((packageName, versions) -> {
                    mutableExports.put(packageName, new HashSet<>(versions));
                });
                this.exportedPackagesByVersion.put(version, mutableExports);
            });

        }

        @Override
        public void startedScan() {
            this.importedPackages = new HashMap<>();
        }

        @Override
        public void beforeExtract(PackageId packageId, Session inspectSession, PackageProperties packageProperties, MetaInf metaInf, List<PackageId> subpackages) throws RepositoryException {
            this.importedPackages.put(packageId, new ArrayList<>());
        }

        @Override
        public void importedPath(PackageId packageId, String path, Node node) throws RepositoryException {
            if (node.isNodeType(JcrConstants.NT_FILE) && path.endsWith(".jar")) {
                try (InputStream stream = JcrUtils.readFile(node)) {
                    ZipInputStream zipInputStream = new ZipInputStream(stream);
                    ZipEntry entry = zipInputStream.getNextEntry();
                    while (entry != null) {
                        if (entry.getName().equals("META-INF/MANIFEST.MF")) {
                            Manifest manifest = new Manifest(zipInputStream);
                            String exportPackageHeader = manifest.getMainAttributes().getValue("Export-Package");
                            parseExportPackage(exportPackageHeader, this::parseExportPackageClause);

                            String importedPackageHeader = manifest.getMainAttributes().getValue("Import-Package");
                            Set<ImportedPackage> importedPackagesForBundle = parseImportPackageHeader(importedPackageHeader);

                            this.importedPackages.get(packageId).add(importedPackagesForBundle);
                            break;
                        }

                        entry = zipInputStream.getNextEntry();
                    }
                } catch (IOException e) {
                    throw new RepositoryException(e);
                }
            }
        }

        @Override
        public void finishedScan() {
            exportedPackagesByVersion.forEach((version, exportedPackages) -> {
                importedPackages.forEach((packageId, importedPackagesForPackage) -> {
                    importedPackagesForPackage.forEach(importedPackagesForBundle -> {
                        importedPackagesForBundle.forEach(importedPackage -> {
                            Result result = importedPackage.satisfied(exportedPackages);
                            if (!result.satisfied) {
                                if (result.availableVersions.isEmpty()) {
                                    reporting(violation -> violation
                                            .withSeverity(Severity.SEVERE)
                                            .withPackage(packageId)
                                            .withDescription("Package import {0} cannot be satisified by AEM Version {1}. Package is not exported.")
                                            .withArgument(importedPackage, version));
                                } else {
                                    reporting(violation -> violation
                                            .withSeverity(Severity.SEVERE)
                                            .withPackage(packageId)
                                            .withDescription("Package import {0} cannot be satisified by AEM Version {1}. Available package versions are ({2}).")
                                            .withArgument(importedPackage, version, result.availableVersions.stream()
                                                    .map(Version::toString).collect(Collectors.joining(", "))));
                                }
                            }
                        });
                    });
                });
            });
        }

        static void parseExportPackage(String header, Consumer<String> callback) {
            if (header == null) {
                return;
            }
            header = header.replaceAll(";uses:=\"[^\"]+\"", "");
            String[] parts = header.split(",");
            Arrays.stream(parts).forEach(callback);
        }

        private void parseExportPackageClause(String clause) {
            String[] parts = clause.split(";");
            String packageName = null;
            String version = "0.0.0";
            for (String part : parts) {
                if (part.startsWith("version")) {
                    version = part.substring(9, part.length() - 1);
                } else if (!part.contains(":=")) {
                    packageName = part;
                }
            }
            if (packageName != null) {
                for (Map<String, Set<Version>> exportedPackages : exportedPackagesByVersion.values()) {
                    Set<Version> versions = exportedPackages.computeIfAbsent(packageName, s -> new HashSet<>());
                    versions.add(new Version(version));
                }
            }
        }
    }

    static Set<ImportedPackage> parseImportPackageHeader(String header) {
        if (header == null) {
            return Collections.emptySet();
        }
        Set<ImportedPackage> result = new LinkedHashSet<>();
        String[] parts = header.split(";");
        int currentPartIndex = 0;
        ImportedPackage currentPackage = new ImportedPackage();
        while (currentPartIndex < parts.length) {
            String currentPart = parts[currentPartIndex];
            if (currentPart.startsWith("resolution:=optional")) {
                currentPackage.optional = true;
                // might be resolution:=optional or resolution:=optional,org.apache.sling.xss
                int commaIndex = currentPart.indexOf(',');
                if (commaIndex > -1) {
                    result.add(currentPackage);
                    currentPackage = new ImportedPackage();
                    currentPackage = parsePartAsPackageName(currentPackage, currentPart.substring(commaIndex + 1), result);
                }
            } else if (currentPart.startsWith("version")) {
                // might be version="[1.0,2)",com.amazonaws.auth or might just be version="[1.0,2)"
                int firstQuoteIndex = currentPart.indexOf('"') + 1;
                int secondQuoteIndex = currentPart.indexOf('"', firstQuoteIndex);

                String versionPart = currentPart.substring(firstQuoteIndex, secondQuoteIndex);

                currentPackage.versionRange = new VersionRange(versionPart);

                int commaAfterSecondQuote = currentPart.indexOf(',', secondQuoteIndex);
                if (commaAfterSecondQuote > -1) {
                    result.add(currentPackage);
                    currentPackage = new ImportedPackage();
                    currentPackage = parsePartAsPackageName(currentPackage, currentPart.substring(commaAfterSecondQuote + 1), result);
                }
            } else {
                currentPackage = parsePartAsPackageName(currentPackage, currentPart, result);
            }
            currentPartIndex++;
        }
        if (currentPackage.packageName != null) {
            result.add(currentPackage);
        }
        return result;
    }

    @NotNull
    private static ImportedPackages.ImportedPackage parsePartAsPackageName(ImportedPackage currentPackage, String part, Set<ImportedPackage> result) {
        // could be just a bare package name or even a comma-delimited list of package names
        String[] subParts = part.split(",");
        for (int i = 0; i < subParts.length - 1; i++) {
            currentPackage.packageName = subParts[i];
            result.add(currentPackage);
            currentPackage = new ImportedPackage();
        }
        currentPackage.packageName = subParts[subParts.length - 1];
        return currentPackage;
    }

    static class ImportedPackage {
        private String packageName;
        private boolean optional;
        private VersionRange versionRange;

        static final Result OK = new Result();
        static final Result NO_EXPORTS = new Result(Collections.emptySet());

        Result satisfied(Map<String, Set<Version>> availablePackages) {
            if (optional || versionRange == null) {
                return OK;
            }
            Set<Version> availableVersions = availablePackages.get(packageName);
            if (availableVersions == null) {
                return NO_EXPORTS;
            }
            for (Version availableVersion : availableVersions) {
                if (versionRange.includes(availableVersion)) {
                    return OK;
                }
            }
            return new Result(availableVersions);
        }

        @Override
        public String toString() {
            StringBuilder builder = new StringBuilder();
            if (packageName != null) {
                builder.append(packageName);
            }
            if (optional) {
                builder.append(";resolution:=optional");
            }
            if (versionRange != null) {
                builder.append(";version=\"").append(versionRange.toString()).append('"');
            }
            return builder.toString();
        }
    }

    static class Result {
        final boolean satisfied;
        final Set<Version> availableVersions;

        Result() {
            this.satisfied = true;
            this.availableVersions = Collections.emptySet();
        }

        Result(Set<Version> availableVersions) {
            this.satisfied = false;
            this.availableVersions = availableVersions;
        }
    }

}