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;
020
021import org.jline.reader.Candidate;
022import org.jline.reader.Completer;
023import org.jline.reader.LineReader;
024import org.jline.reader.ParsedLine;
025import org.jline.utils.AttributedString;
026
027import java.io.File;
028import java.io.IOException;
029import java.net.JarURLConnection;
030import java.net.URI;
031import java.net.URISyntaxException;
032import java.net.URL;
033import java.util.*;
034import java.util.jar.JarEntry;
035
036/** JLine3 {@link Completer} that provides tab-completion for class names with given prefixes. */
037public class CLIPrefixedClassNameCompleter implements Completer {
038    /** Pre-computed candidate list for tab completion. */
039    protected final Collection<Candidate> candidates = new ArrayList<>();
040
041    /**
042     * Creates a completer that includes class names with the given prefixes.
043     * @param prefixes the class name prefixes to include as candidates
044     * @throws IOException if class scanning fails
045     */
046    public CLIPrefixedClassNameCompleter(Collection<String> prefixes) throws IOException {
047        for (String s : getClassNames(prefixes)) {
048            candidates.add(new Candidate(AttributedString.stripAnsi(s), s, null, null, null, null, true));
049        }
050    }
051
052    private static String[] getClassNames(Collection<String> prefixes) throws IOException {
053        Set<String> classes = new HashSet<>();
054        for (String prefix : prefixes) {
055            classes.addAll(getClassEntries(prefix));
056        }
057        Set<String> classNames = new TreeSet<String>();
058        for (String name : classes) {
059            if (name.endsWith(".class")) {
060                classNames.add(name.replace('/', '.').substring(0, name.length() - 6));
061            }
062        }
063        return classNames.toArray(new String[classNames.size()]);
064    }
065
066    private static List<String> getClassEntries(String prefix) throws IOException {
067        final String p = prefix.replaceAll("\\.", "\\/");
068        List<String> result = new ArrayList<String>();
069
070        Enumeration<URL> urls = CLIPrefixedClassNameCompleter.class.getClassLoader().getResources(p);
071        while (urls.hasMoreElements()) {
072            URL url = urls.nextElement();
073            if (url == null) {
074                return Collections.emptyList();
075            }
076
077            try {
078                final List<String> lst = url.getProtocol().equals("jar") ?
079                  resolveModuleEntriesFromJar(url, p) :
080                  resolveModuleEntriesFromFiles(url, p);
081                result.addAll(lst);
082            } catch (URISyntaxException e) {
083                throw new IOException("Bad URL", e);
084            }
085        }
086        return result;
087    }
088
089    private static List<String> resolveModuleEntriesFromFiles(URL url, String _prefix) throws IOException, URISyntaxException {
090        final String prefix = _prefix.endsWith("/") ? _prefix : _prefix + "/";
091        List<String> resourceList = new ArrayList<String>();
092        final URI uri = url.toURI();
093        File f = new File(uri);
094        addFiles(f, prefix, resourceList);
095        return resourceList;
096    }
097
098    private static void addFiles(File f, String prefix, List<String> resourceList) {
099        File files[] = f.listFiles();
100        if (files == null) {
101            return;
102        }
103        for (File file : files) {
104            if (file.isDirectory()) {
105                addFiles(file, prefix + file.getName() + System.getProperty("file.separator"), resourceList);
106            } else {
107                resourceList.add(file.getName());
108            }
109        }
110    }
111
112    private static List<String> resolveModuleEntriesFromJar(URL url, String _prefix) throws IOException {
113        final String prefix = _prefix.endsWith("/") ? _prefix : _prefix + "/";
114        List<String> resourceList = new ArrayList<String>();
115        JarURLConnection conn = (JarURLConnection) url.openConnection();
116        Enumeration entries = conn.getJarFile().entries();
117        while (entries.hasMoreElements()) {
118            JarEntry entry = (JarEntry) entries.nextElement();
119            String name = entry.getName();
120            if (name.startsWith(prefix) && !name.contains("$") && !entry.isDirectory()) {
121                name = name.substring(prefix.length()).toLowerCase();
122                if (!name.contains("/")) {
123                    resourceList.add(name);
124                }
125            }
126        }
127        return resourceList;
128    }
129
130    @Override
131    public void complete(LineReader reader, ParsedLine line, List<Candidate> cand) {
132        if (line.wordIndex() == 0 && cand != null)
133            cand.addAll(this.candidates);
134    }
135
136    public String toString () {
137        StringBuilder sb = new StringBuilder ("CLIPrefixedClassNameCompletor[");
138        sb.append(hashCode());
139        for (Candidate c : candidates) {
140            sb.append (System.getProperty("line.separator"));
141            sb.append (" ");
142            sb.append (c.value());
143        }
144        sb.append(']');
145        return sb.toString();
146    }
147}