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}