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}