001/*
002 * jPOS Project [http://jpos.org]
003 * Copyright (C) 2000-2026 jPOS Software SRL
004 *
005 * This program is free software: you can redistribute it and/or modify
006 * it under the terms of the GNU Affero General Public License as
007 * published by the Free Software Foundation, either version 3 of the
008 * License, or (at your option) any later version.
009 *
010 * This program is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
013 * GNU Affero General Public License for more details.
014 *
015 * You should have received a copy of the GNU Affero General Public License
016 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
017 */
018
019package org.jpos.q2.install;
020
021import java.io.File;
022import java.io.IOException;
023import java.net.JarURLConnection;
024import java.net.URI;
025import java.net.URISyntaxException;
026import java.net.URL;
027import java.security.MessageDigest;
028import java.security.NoSuchAlgorithmException;
029import java.util.*;
030import java.util.jar.JarEntry;
031import java.util.jar.JarFile;
032import java.util.stream.Collectors;
033
034/**
035 * Helpers that enumerate jPOS module metadata embedded under
036 * {@code META-INF/modules/} on the runtime classpath.
037 *
038 * @author vsalaman
039 */
040public class ModuleUtils
041{
042    /** Default constructor; no instance state to initialise. */
043    public ModuleUtils() {}
044    private static final String MODULES_UUID_DIR = "META-INF/modules/uuids/";
045    private static final String MODULES_RKEYS_DIR = "META-INF/modules/rkeys/";
046
047    /**
048     * Enumerates every classpath resource located under {@code prefix}, traversing
049     * both directory- and JAR-based class-loader entries.
050     *
051     * @param prefix classpath prefix to scan (e.g. {@code "META-INF/modules/uuids/"})
052     * @return resource paths relative to the classpath root
053     * @throws IOException if classpath enumeration fails
054     */
055    public static List<String> getModuleEntries(String prefix) throws IOException {
056        List<String> result = new ArrayList<>();
057
058        Enumeration<URL> urls = ModuleUtils.class.getClassLoader().getResources(prefix);
059        while (urls.hasMoreElements()) {
060            URL url = urls.nextElement();
061            if (url == null) continue;
062
063            try {
064                List<String> entries;
065                String protocol = url.getProtocol();
066                if ("jar".equals(protocol)) {
067                    entries = resolveModuleEntriesFromJar(url, prefix);
068                } else if ("file".equals(protocol)) {
069                    entries = resolveModuleEntriesFromFiles(url, prefix);
070                } else {
071                    // Unsupported protocol, skip with optional logging
072                    continue;
073                }
074                result.addAll(entries);
075            } catch (URISyntaxException e) {
076                throw new IOException("Bad URL: " + url, e);
077            }
078        }
079        return result;
080    }
081
082    /**
083     * Returns the sorted list of module UUIDs discovered under
084     * {@code META-INF/modules/uuids/}.
085     *
086     * @return module UUIDs (one per registered module)
087     * @throws IOException if classpath enumeration fails
088     */
089    public static List<String> getModulesUUIDs() throws IOException {
090        return getModuleEntries(MODULES_UUID_DIR).stream()
091          .sorted()
092          .map(p -> p.substring(MODULES_UUID_DIR.length()))
093          .collect(Collectors.toList());
094    }
095
096    /**
097     * Returns the sorted list of revocation-key identifiers discovered under
098     * {@code META-INF/modules/rkeys/}.
099     *
100     * @return revocation key identifiers
101     * @throws IOException if classpath enumeration fails
102     */
103    public static List<String> getRKeys () throws IOException {
104        return ModuleUtils.getModuleEntries(MODULES_RKEYS_DIR)
105          .stream()
106          .sorted()
107          .map(p -> p.substring(MODULES_RKEYS_DIR.length()))
108          .collect(Collectors.toList());
109    }
110
111    /**
112     * Returns the Base64-encoded SHA-256 hash of the concatenated module UUIDs,
113     * used as a per-installation fingerprint by license verification.
114     *
115     * @return Base64 SHA-256 hash, or empty string when no UUIDs are registered
116     * @throws IOException if classpath enumeration fails
117     * @throws NoSuchAlgorithmException if SHA-256 is unavailable
118     */
119    public static String getSystemHash() throws IOException, NoSuchAlgorithmException {
120        MessageDigest digest = MessageDigest.getInstance("SHA-256");
121        List<String> uuids = getModulesUUIDs();
122        if (uuids.isEmpty()) return "";
123
124        uuids.forEach(uuid -> digest.update(uuid.getBytes()));
125        return Base64.getEncoder().encodeToString(digest.digest());
126    }
127
128    private static List<String> resolveModuleEntriesFromFiles(URL url, String prefix)
129      throws URISyntaxException {
130        String normalizedPrefix = prefix.endsWith("/") ? prefix : prefix + "/";
131        List<String> resourceList = new ArrayList<>();
132        File dir = new File(url.toURI());
133        addFiles(dir, normalizedPrefix, resourceList);
134        return resourceList;
135    }
136
137    private static void addFiles(File dir, String prefix, List<String> resourceList) {
138        File[] files = dir.listFiles();
139        if (files == null) return;
140
141        for (File file : files) {
142            if (file.isDirectory()) {
143                addFiles(file, prefix + file.getName() + "/", resourceList);
144            } else {
145                resourceList.add(prefix + file.getName());
146            }
147        }
148    }
149
150    private static List<String> resolveModuleEntriesFromJar(URL url, String prefix)
151      throws IOException {
152        String normalizedPrefix = prefix.endsWith("/") ? prefix : prefix + "/";
153        List<String> resourceList = new ArrayList<>();
154
155        JarURLConnection conn = (JarURLConnection) url.openConnection();
156        try (JarFile jarFile = conn.getJarFile()) {
157            Enumeration<JarEntry> entries = jarFile.entries();
158            while (entries.hasMoreElements()) {
159                JarEntry entry = entries.nextElement();
160                String name = entry.getName();
161                if (name.startsWith(normalizedPrefix) && !entry.isDirectory()) {
162                    resourceList.add(name);
163                }
164            }
165        }
166        return resourceList;
167    }
168}