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.*; 022import org.jline.reader.impl.history.DefaultHistory; 023import org.jline.terminal.Attributes; 024import org.jline.terminal.Terminal; 025import org.jline.terminal.TerminalBuilder; 026 027import java.io.*; 028import java.util.*; 029import java.util.logging.Level; 030import java.util.logging.Logger; 031 032public class CLI implements Runnable { 033 final private static String DEFAULT_PROMPT = "q2> "; 034 final private static String ESCAPED_SEMICOLON = "__semicolon__"; 035 private Thread t; 036 private String line = null; 037 private boolean keepRunning = false; 038 private boolean interactive = false; 039 protected CLIContext ctx; 040 private CLICommandInterface cmdInterface; 041 private Terminal terminal; 042 private LineReader reader; 043 private Q2 q2; 044 private String prompt = DEFAULT_PROMPT; 045 private History mainHistory; 046 047 public CLI(Q2 q2, String line, boolean keepRunning) throws IOException { 048 this(q2, System.in, System.out, line, keepRunning, true); 049 } 050 051 public CLI(Q2 q2, InputStream in, OutputStream rawout, String line, boolean keepRunning, boolean interactive) throws IOException { 052 Logger.getLogger("org.jline").setLevel(Level.SEVERE); 053 this.q2 = q2; 054 PrintStream out = rawout instanceof PrintStream ? (PrintStream) rawout : new PrintStream(rawout); 055 ctx = buildCLIContext(in, out); 056 this.line = line; 057 this.keepRunning = keepRunning; 058 this.interactive = interactive; 059 this.mainHistory = new DefaultHistory(); 060 if (interactive) { 061 terminal = terminal = buildTerminal(in, out); 062 } 063 initCmdInterface(getCompletionPrefixes(), mainHistory); 064 } 065 066 protected boolean running() { 067 return getQ2() == null || getQ2().running(); 068 } 069 070 protected void markStopped() { } 071 072 protected void markStarted() { } 073 074 protected String[] getCompletionPrefixes() { 075 return new String[] {"org.jpos.q2.cli." }; 076 } 077 078 protected void handleExit() { } 079 080 void setPrompt(String prompt, String[] completionPrefixes) throws IOException { 081 this.prompt = prompt != null ? prompt : DEFAULT_PROMPT; 082 initCmdInterface(completionPrefixes, completionPrefixes == null ? mainHistory : new DefaultHistory()); 083 } 084 085 private void initCmdInterface(String[] completionPrefixes, History history) throws IOException { 086 completionPrefixes = completionPrefixes == null ? getCompletionPrefixes() : completionPrefixes; 087 cmdInterface = new CLICommandInterface(ctx); 088 for (String s : completionPrefixes) { 089 cmdInterface.addPrefix(s); 090 } 091 cmdInterface.addPrefix("org.jpos.q2.cli.builtin."); 092 if (terminal != null) { 093 reader = buildReader(terminal, completionPrefixes, history); 094 ctx.setReader(reader); 095 } 096 } 097 098 public void start() throws Exception { 099 markStarted(); 100 t = new Thread(this); 101 t.setName("Q2-CLI"); 102 t.start(); 103 } 104 105 public void stop() { 106 markStopped(); 107 try { 108 t.join(); 109 } 110 catch (InterruptedException ignored) { } 111 } 112 113 public void run() { 114 while (running()) { 115 try { 116 LineReader reader = getReader(); 117 String p = prompt; 118 if (line == null) { 119 String s; 120 while ((s = reader.readLine(p)) != null) { 121 if (s.endsWith("\\")) { 122 s = s.substring(0, s.length() -1); 123 p = ""; 124 line = line == null ? s : line + s; 125 continue; 126 } 127 line = line == null ? s : line + s; 128 break; 129 } 130 } 131 if (line != null) { 132 line = line.replace("\\;", ESCAPED_SEMICOLON); 133 StringTokenizer st = new StringTokenizer(line, ";"); 134 boolean exit = false; 135 while (st.hasMoreTokens()) { 136 String n = st.nextToken().replace (ESCAPED_SEMICOLON, ";"); 137 try { 138 String[] args = cmdInterface.parseCommand(n); 139 if (args.length > 0 && args[0].contains(":")) { 140 String prefixCommand = args[0].substring(0, args[0].indexOf(":")); 141 cmdInterface.execCommand(prefixCommand); 142 n = n.substring(prefixCommand.length() + 1); 143 exit = true; 144 } 145 cmdInterface.execCommand(n); 146 } catch (IOException e) { 147 ctx.printThrowable(e); 148 } 149 } 150 line = null; 151 if (exit) { 152 try { 153 cmdInterface.execCommand("exit"); 154 } catch (IOException e) { 155 ctx.printThrowable(e); 156 } 157 } 158 } 159 if (!keepRunning) { 160 break; 161 } 162 163 } catch (UserInterruptException | EndOfFileException e) { 164 break; 165 } 166 } 167 try { 168 if (keepRunning) 169 getReader().getTerminal().close(); 170 } catch (IOException e) { 171 ctx.printThrowable(e); 172 } 173 handleExit(); 174 } 175 176 public Q2 getQ2() { 177 return q2; 178 } 179 180 public boolean isInteractive() { 181 return interactive; 182 } 183 184 public LineReader getReader() { 185 return reader; 186 } 187 188 public static void exec (InputStream in, OutputStream out, String command) throws Exception { 189 CLI cli = new CLI(Q2.getQ2(), in, out, command, false, false); 190 cli.start(); 191 cli.stop(); 192 } 193 194 public static String exec (String command) throws Exception { 195 ByteArrayOutputStream out = new ByteArrayOutputStream(); 196 exec (null, out, command); 197 return out.toString(); 198 } 199 200 protected Terminal buildTerminal (InputStream in, OutputStream out) throws IOException { 201 TerminalBuilder builder = TerminalBuilder.builder() 202 .streams(in,out) 203 .system(System.in == in); 204 Terminal t = builder.build(); 205 Attributes attr = t.getAttributes(); 206 attr.getOutputFlags().addAll( 207 EnumSet.of(Attributes.OutputFlag.ONLCR, Attributes.OutputFlag.OPOST) 208 ); 209 t.setAttributes(attr); 210 return t; 211 } 212 213 214 private LineReader buildReader(Terminal terminal, String[] completionPrefixes, History history) throws IOException { 215 LineReader reader = LineReaderBuilder.builder() 216 .terminal(terminal) 217 .history(history) 218 .completer(new CLIPrefixedClassNameCompleter(Arrays.asList(completionPrefixes))) 219 .build(); 220 reader.unsetOpt(LineReader.Option.INSERT_TAB); 221 reader.setOpt(LineReader.Option.DISABLE_EVENT_EXPANSION); 222 return reader; 223 } 224 225 private CLIContext buildCLIContext (InputStream in, OutputStream out) { 226 return CLIContext.builder() 227 .cli(this) 228 .in(in) 229 .out(out) 230 .build(); 231 } 232}