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 032/** Interactive command-line interface for a running Q2 instance. */ 033public class CLI implements Runnable { 034 final private static String DEFAULT_PROMPT = "q2> "; 035 final private static String ESCAPED_SEMICOLON = "__semicolon__"; 036 private Thread t; 037 private String line = null; 038 private boolean keepRunning = false; 039 private boolean interactive = false; 040 /** The context for this CLI session. */ 041 protected CLIContext ctx; 042 private CLICommandInterface cmdInterface; 043 private Terminal terminal; 044 private LineReader reader; 045 private Q2 q2; 046 private String prompt = DEFAULT_PROMPT; 047 private History mainHistory; 048 049 /** 050 * Creates a simple CLI with a single command and no streams. 051 * @param q2 the Q2 instance 052 * @param line the initial command line 053 * @param keepRunning true to keep running after the first command 054 * @throws IOException on I/O failure 055 */ 056 public CLI(Q2 q2, String line, boolean keepRunning) throws IOException { 057 this(q2, System.in, System.out, line, keepRunning, true); 058 } 059 060 /** 061 * Creates a full CLI with explicit I/O streams. 062 * @param q2 the Q2 instance 063 * @param in input stream 064 * @param rawout output stream 065 * @param line initial command (may be null for interactive mode) 066 * @param keepRunning true to keep running after commands 067 * @param interactive true for interactive (line-edit) mode 068 * @throws IOException on I/O failure 069 */ 070 public CLI(Q2 q2, InputStream in, OutputStream rawout, String line, boolean keepRunning, boolean interactive) throws IOException { 071 Logger.getLogger("org.jline").setLevel(Level.SEVERE); 072 this.q2 = q2; 073 PrintStream out = rawout instanceof PrintStream ? (PrintStream) rawout : new PrintStream(rawout); 074 ctx = buildCLIContext(in, out); 075 this.line = line; 076 this.keepRunning = keepRunning; 077 this.interactive = interactive; 078 this.mainHistory = new DefaultHistory(); 079 if (interactive) { 080 terminal = terminal = buildTerminal(in, out); 081 } 082 initCmdInterface(getCompletionPrefixes(), mainHistory); 083 } 084 085 /** 086 * Returns true if this CLI is still running. 087 * @return true if running 088 */ 089 protected boolean running() { 090 return getQ2() == null || getQ2().running(); 091 } 092 093 /** Called when the CLI is stopping; subclasses may override. */ 094 protected void markStopped() { } 095 096 /** Called when the CLI is starting; subclasses may override. */ 097 protected void markStarted() { } 098 099 /** 100 * Returns command prefixes registered for tab-completion. 101 * @return array of command prefixes 102 */ 103 protected String[] getCompletionPrefixes() { 104 return new String[] {"org.jpos.q2.cli." }; 105 } 106 107 /** Called on normal exit; subclasses may override. */ 108 protected void handleExit() { } 109 110 void setPrompt(String prompt, String[] completionPrefixes) throws IOException { 111 this.prompt = prompt != null ? prompt : DEFAULT_PROMPT; 112 initCmdInterface(completionPrefixes, completionPrefixes == null ? mainHistory : new DefaultHistory()); 113 } 114 115 private void initCmdInterface(String[] completionPrefixes, History history) throws IOException { 116 completionPrefixes = completionPrefixes == null ? getCompletionPrefixes() : completionPrefixes; 117 cmdInterface = new CLICommandInterface(ctx); 118 for (String s : completionPrefixes) { 119 cmdInterface.addPrefix(s); 120 } 121 cmdInterface.addPrefix("org.jpos.q2.cli.builtin."); 122 if (terminal != null) { 123 reader = buildReader(terminal, completionPrefixes, history); 124 ctx.setReader(reader); 125 } 126 } 127 128 /** Starts the CLI session. 129 * @throws Exception on startup failure 130 */ 131 public void start() throws Exception { 132 markStarted(); 133 t = new Thread(this); 134 t.setName("Q2-CLI"); 135 t.start(); 136 } 137 138 /** Stops the CLI session. */ 139 public void stop() { 140 markStopped(); 141 try { 142 t.join(); 143 } 144 catch (InterruptedException ignored) { } 145 } 146 147 public void run() { 148 while (running()) { 149 try { 150 LineReader reader = getReader(); 151 String p = prompt; 152 if (line == null) { 153 String s; 154 while ((s = reader.readLine(p)) != null) { 155 if (s.endsWith("\\")) { 156 s = s.substring(0, s.length() -1); 157 p = ""; 158 line = line == null ? s : line + s; 159 continue; 160 } 161 line = line == null ? s : line + s; 162 break; 163 } 164 } 165 if (line != null) { 166 line = line.replace("\\;", ESCAPED_SEMICOLON); 167 StringTokenizer st = new StringTokenizer(line, ";"); 168 boolean exit = false; 169 while (st.hasMoreTokens()) { 170 String n = st.nextToken().replace (ESCAPED_SEMICOLON, ";"); 171 try { 172 String[] args = cmdInterface.parseCommand(n); 173 if (args.length > 0 && args[0].contains(":")) { 174 String prefixCommand = args[0].substring(0, args[0].indexOf(":")); 175 cmdInterface.execCommand(prefixCommand); 176 n = n.substring(prefixCommand.length() + 1); 177 exit = true; 178 } 179 cmdInterface.execCommand(n); 180 } catch (IOException e) { 181 ctx.printThrowable(e); 182 } 183 } 184 line = null; 185 if (exit) { 186 try { 187 cmdInterface.execCommand("exit"); 188 } catch (IOException e) { 189 ctx.printThrowable(e); 190 } 191 } 192 } 193 if (!keepRunning) { 194 break; 195 } 196 197 } catch (UserInterruptException | EndOfFileException e) { 198 break; 199 } 200 } 201 try { 202 if (keepRunning) 203 getReader().getTerminal().close(); 204 } catch (IOException e) { 205 ctx.printThrowable(e); 206 } 207 handleExit(); 208 } 209 210 /** 211 * Returns the Q2 instance this CLI is attached to. 212 * @return the Q2 instance 213 */ 214 public Q2 getQ2() { 215 return q2; 216 } 217 218 /** 219 * Returns true if this CLI session is interactive. 220 * @return true if interactive 221 */ 222 public boolean isInteractive() { 223 return interactive; 224 } 225 226 /** 227 * Returns the JLine3 LineReader for this session. 228 * @return the LineReader 229 */ 230 public LineReader getReader() { 231 return reader; 232 } 233 234 /** 235 * Executes a CLI command with the given I/O streams. 236 * @param in input stream 237 * @param out output stream 238 * @param command command to execute 239 * @throws Exception on execution failure 240 */ 241 public static void exec (InputStream in, OutputStream out, String command) throws Exception { 242 CLI cli = new CLI(Q2.getQ2(), in, out, command, false, false); 243 cli.start(); 244 cli.stop(); 245 } 246 247 /** 248 * Executes a CLI command and captures its output as a string. 249 * @param command command string to execute 250 * @return captured output 251 * @throws Exception on execution failure 252 */ 253 public static String exec (String command) throws Exception { 254 ByteArrayOutputStream out = new ByteArrayOutputStream(); 255 exec (null, out, command); 256 return out.toString(); 257 } 258 259 /** 260 * Builds a JLine3 Terminal for this session. 261 * @param in input stream 262 * @param out output stream 263 * @return the Terminal 264 * @throws IOException on I/O failure 265 */ 266 protected Terminal buildTerminal (InputStream in, OutputStream out) throws IOException { 267 TerminalBuilder builder = TerminalBuilder.builder() 268 .streams(in,out) 269 .system(System.in == in); 270 Terminal t = builder.build(); 271 Attributes attr = t.getAttributes(); 272 attr.getOutputFlags().addAll( 273 EnumSet.of(Attributes.OutputFlag.ONLCR, Attributes.OutputFlag.OPOST) 274 ); 275 t.setAttributes(attr); 276 return t; 277 } 278 279 280 private LineReader buildReader(Terminal terminal, String[] completionPrefixes, History history) throws IOException { 281 LineReader reader = LineReaderBuilder.builder() 282 .terminal(terminal) 283 .history(history) 284 .completer(new CLIPrefixedClassNameCompleter(Arrays.asList(completionPrefixes))) 285 .build(); 286 reader.unsetOpt(LineReader.Option.INSERT_TAB); 287 reader.setOpt(LineReader.Option.DISABLE_EVENT_EXPANSION); 288 return reader; 289 } 290 291 private CLIContext buildCLIContext (InputStream in, OutputStream out) { 292 return CLIContext.builder() 293 .cli(this) 294 .in(in) 295 .out(out) 296 .build(); 297 } 298}