1 /**
2 * \file jsoncliformatter.cpp
3 * CLI formatter with JSON input and output.
4 *
5 * \b Project: Kid3
6 * \author Urs Fleisch
7 * \date 28 Jul 2019
8 *
9 * Copyright (C) 2019 Urs Fleisch
10 *
11 * This file is part of Kid3.
12 *
13 * Kid3 is free software; you can redistribute it and/or modify
14 * it under the terms of the GNU General Public License as published by
15 * the Free Software Foundation; either version 2 of the License, or
16 * (at your option) any later version.
17 *
18 * Kid3 is distributed in the hope that it will be useful,
19 * but WITHOUT ANY WARRANTY; without even the implied warranty of
20 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 * GNU General Public License for more details.
22 *
23 * You should have received a copy of the GNU General Public License
24 * along with this program. If not, see <http://www.gnu.org/licenses/>.
25 */
26
27 #include "jsoncliformatter.h"
28 #include <climits>
29 #include <QJsonArray>
30 #include <QJsonDocument>
31 #include <QVariantMap>
32 #include <QStringBuilder>
33 #include "clierror.h"
34 #include "abstractcli.h"
35 #include "frame.h"
36
37 namespace {
38
jsonRpcErrorCode(CliError errorCode)39 int jsonRpcErrorCode(CliError errorCode)
40 {
41 // See error codes at https://www.jsonrpc.org/specification
42 int code = -1;
43 switch (errorCode) {
44 case CliError::Ok:
45 code = 0;
46 break;
47 case CliError::ApplicationError:
48 code = -1;
49 break;
50 case CliError::ParseError:
51 code = -32700;
52 break;
53 case CliError::InvalidRequest:
54 case CliError::Usage:
55 code = -32600;
56 break;
57 case CliError::MethodNotFound:
58 code = -32601;
59 break;
60 case CliError::InvalidParams:
61 code = -32602;
62 break;
63 case CliError::InternalError:
64 code = -32603;
65 break;
66 }
67 return code;
68 }
69
70 }
71
72
JsonCliFormatter(AbstractCliIO * io)73 JsonCliFormatter::JsonCliFormatter(AbstractCliIO* io)
74 : AbstractCliFormatter(io), m_compact(false)
75 {
76 }
77
~JsonCliFormatter()78 JsonCliFormatter::~JsonCliFormatter()
79 {
80 }
81
clear()82 void JsonCliFormatter::clear()
83 {
84 m_jsonRequest.clear();
85 m_jsonId.clear();
86 m_errorMessage.clear();
87 m_args.clear();
88 m_response = QJsonObject();
89 m_compact = false;
90 }
91
parseArguments(const QString & line)92 QStringList JsonCliFormatter::parseArguments(const QString& line)
93 {
94 m_errorMessage.clear();
95 m_args.clear();
96 if (m_jsonRequest.isEmpty()) {
97 m_jsonRequest = line.trimmed();
98 if (!m_jsonRequest.startsWith(QLatin1Char('{'))) {
99 m_jsonRequest.clear();
100 }
101 } else {
102 m_jsonRequest.append(line.trimmed());
103 }
104 if (!m_jsonRequest.isEmpty()) {
105 if (!m_jsonRequest.endsWith(QLatin1Char('}'))) {
106 // Probably partial JSON request
107 return QStringList();
108 }
109 m_compact = m_jsonRequest.contains(QLatin1String("\"method\":\""));
110 QJsonParseError error;
111 auto doc = QJsonDocument::fromJson(m_jsonRequest.toUtf8(), &error);
112 if (!doc.isNull()) {
113 QJsonObject obj = doc.object();
114 if (!obj.isEmpty()) {
115 auto method = obj.value(QLatin1String("method")).toString();
116 if (!method.isEmpty()) {
117 m_args.append(method);
118 const auto params = obj.value(QLatin1String("params")).toArray();
119 for (const auto& param : params) {
120 QString arg = param.toString();
121 if (arg.isEmpty()) {
122 if (param.isArray()) {
123 // Special handling for tags parameter of the form [1, 2]
124 const auto elements = param.toArray();
125 for (const auto& element : elements) {
126 int tagNr = element.toInt();
127 if (tagNr > 0 && tagNr <= Frame::Tag_NumValues) {
128 arg += QLatin1Char('0' + static_cast<char>(tagNr));
129 } else {
130 arg.clear();
131 break;
132 }
133 }
134 } else if (param.isDouble()) {
135 // Allow integer numbers, for example for track numbers
136 int argInt = param.toInt(INT_MIN);
137 if (argInt != INT_MIN) {
138 arg = QString::number(argInt);
139 }
140 } else if (param.isBool()) {
141 arg = QLatin1String(param.toBool() ? "true" : "false");
142 }
143 }
144 m_args.append(arg);
145 }
146 // A JSON-RPC ID is used in the response and to store that a JSON
147 // request is running.
148 m_jsonId = obj.value(QLatin1String("id"))
149 .toString(QLatin1String(""));
150 }
151 }
152 }
153 if (m_args.isEmpty()) {
154 auto errStr = error.error != QJsonParseError::NoError
155 ? error.errorString() : QLatin1String("missing method");
156 if (!errStr.isEmpty()) {
157 m_errorMessage = errStr + QLatin1String(": ") + m_jsonRequest;
158 }
159 m_jsonRequest.clear();
160 return QStringList();
161 }
162 m_jsonRequest.clear();
163 } else {
164 m_jsonId.clear();
165 }
166 return m_args;
167 }
168
getErrorMessage() const169 QString JsonCliFormatter::getErrorMessage() const
170 {
171 return m_errorMessage;
172 }
173
isIncomplete() const174 bool JsonCliFormatter::isIncomplete() const
175 {
176 return !m_jsonRequest.isEmpty();
177 }
178
isFormatRecognized() const179 bool JsonCliFormatter::isFormatRecognized() const
180 {
181 return !m_jsonId.isNull() || !m_jsonRequest.isEmpty() ||
182 !m_errorMessage.isEmpty();
183 }
184
writeError(CliError errorCode)185 void JsonCliFormatter::writeError(CliError errorCode)
186 {
187 QString msg;
188 if (errorCode == CliError::MethodNotFound) {
189 #if QT_VERSION >= 0x050600
190 msg = tr("Unknown command '%1'")
191 .arg(m_args.isEmpty() ? QLatin1String("") : m_args.constFirst());
192 #else
193 msg = tr("Unknown command '%1'")
194 .arg(m_args.isEmpty() ? QLatin1String("") : m_args.first());
195 #endif
196 }
197 writeErrorMessage(msg, jsonRpcErrorCode(errorCode));
198 }
199
writeError(const QString & msg)200 void JsonCliFormatter::writeError(const QString& msg)
201 {
202 writeErrorMessage(msg, -1);
203 }
204
writeError(const QString & msg,CliError errorCode)205 void JsonCliFormatter::writeError(const QString& msg, CliError errorCode)
206 {
207 QString errorMsg = msg;
208 if (errorCode == CliError::Usage) {
209 errorMsg = tr("Usage:") % QLatin1Char(' ') % errorMsg;
210 }
211 writeErrorMessage(errorMsg, jsonRpcErrorCode(errorCode));
212 }
213
writeErrorMessage(const QString & msg,int code)214 void JsonCliFormatter::writeErrorMessage(const QString& msg, int code)
215 {
216 QJsonObject error;
217 error.insert(QLatin1String("code"), code);
218 error.insert(QLatin1String("message"), msg);
219 m_response.insert(QLatin1String("error"), error);
220 }
221
writeResult(const QString & str)222 void JsonCliFormatter::writeResult(const QString& str)
223 {
224 m_response.insert(QLatin1String("result"), str);
225 }
226
writeResult(const QStringList & strs)227 void JsonCliFormatter::writeResult(const QStringList& strs)
228 {
229 m_response.insert(QLatin1String("result"), QJsonArray::fromStringList(strs));
230 }
231
writeResult(const QVariantMap & map)232 void JsonCliFormatter::writeResult(const QVariantMap& map)
233 {
234 QJsonObject result;
235 if (map.size() == 1 && map.contains(QLatin1String("event"))) {
236 result = m_response.value(QLatin1String("result")).toObject();
237 auto events = result.value(QLatin1String("events")).toArray();
238 events.append(QJsonValue::fromVariant(map.value(QLatin1String("event"))));
239 result.insert(QLatin1String("events"), events);
240 } else {
241 result = QJsonObject::fromVariantMap(map);
242 }
243 m_response.insert(QLatin1String("result"), result);
244 }
245
writeResult(bool result)246 void JsonCliFormatter::writeResult(bool result)
247 {
248 m_response.insert(QLatin1String("result"), result);
249 }
250
finishWriting()251 void JsonCliFormatter::finishWriting()
252 {
253 if (m_response.isEmpty()) {
254 m_response.insert(QLatin1String("result"), QJsonValue::Null);
255 }
256 if (!m_jsonId.isEmpty()) {
257 m_response.insert(QLatin1String("jsonrpc"), QLatin1String("2.0"));
258 m_response.insert(QLatin1String("id"), m_jsonId);
259 }
260 io()->writeLine(QString::fromUtf8(
261 QJsonDocument(m_response).toJson(
262 m_compact ? QJsonDocument::Compact
263 : QJsonDocument::Indented)));
264 }
265