1 /******************************************************************************
2
3 This source file is part of the Avogadro project.
4
5 Copyright 2013 Kitware, Inc.
6
7 This source code is released under the New BSD License, (the "License").
8
9 Unless required by applicable law or agreed to in writing, software
10 distributed under the License is distributed on an "AS IS" BASIS,
11 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 See the License for the specific language governing permissions and
13 limitations under the License.
14
15 ******************************************************************************/
16
17 #include "obfileformat.h"
18
19 #include "obprocess.h"
20
21 #include <avogadro/io/cmlformat.h>
22
23 #include <nlohmann/json.hpp>
24
25 #include <QtCore/QCoreApplication>
26 #include <QtCore/QFileInfo>
27 #include <QtCore/QTimer>
28
29 using json = nlohmann::json;
30
31 namespace Avogadro {
32 namespace QtPlugins {
33
34 /**
35 * @brief The ProcessListener class allows synchronous use of OBProcess.
36 */
37 class OBFileFormat::ProcessListener : public QObject
38 {
39 Q_OBJECT
40 public:
ProcessListener()41 ProcessListener() : QObject(), m_finished(false) {}
42
waitForOutput(QByteArray & output,int msTimeout=120000)43 bool waitForOutput(QByteArray& output, int msTimeout = 120000)
44 {
45 if (!wait(msTimeout))
46 return false;
47
48 // success!
49 output = m_output;
50 return true;
51 }
52
53 public slots:
responseReceived(const QByteArray & output)54 void responseReceived(const QByteArray& output)
55 {
56 m_finished = true;
57 m_output = output;
58 }
59
60 private:
wait(int msTimeout)61 bool wait(int msTimeout)
62 {
63 QTimer timer;
64 timer.start(msTimeout);
65
66 while (timer.isActive() && !m_finished)
67 qApp->processEvents(QEventLoop::AllEvents, 500);
68
69 return m_finished;
70 }
71
72 OBProcess* m_process;
73 bool m_finished;
74 QByteArray m_output;
75 };
76
OBFileFormat(const std::string & name_,const std::string & identifier_,const std::string & description_,const std::string & specificationUrl_,const std::vector<std::string> fileExtensions_,const std::vector<std::string> mimeTypes_,bool fileOnly_)77 OBFileFormat::OBFileFormat(const std::string& name_,
78 const std::string& identifier_,
79 const std::string& description_,
80 const std::string& specificationUrl_,
81 const std::vector<std::string> fileExtensions_,
82 const std::vector<std::string> mimeTypes_,
83 bool fileOnly_)
84 : Io::FileFormat(), m_description(description_),
85 m_fileExtensions(fileExtensions_), m_mimeTypes(mimeTypes_),
86 m_identifier(identifier_), m_name(name_),
87 m_specificationUrl(specificationUrl_), m_fileOnly(fileOnly_)
88 {
89 }
90
~OBFileFormat()91 OBFileFormat::~OBFileFormat()
92 {
93 }
94
read(std::istream & in,Core::Molecule & molecule)95 bool OBFileFormat::read(std::istream& in, Core::Molecule& molecule)
96 {
97 json opts;
98 if (!options().empty())
99 opts = json::parse(options(), nullptr, false);
100 else
101 opts = json::object();
102
103 // Allow blocking until the read is completed.
104 OBProcess proc;
105 ProcessListener listener;
106 QObject::connect(&proc, SIGNAL(convertFinished(QByteArray)), &listener,
107 SLOT(responseReceived(QByteArray)));
108
109 // Just grab the first file extension from the list -- all extensions for a
110 // given format map to the same parsers in OB.
111 if (m_fileExtensions.empty()) {
112 appendError("Internal error: No file extensions set.");
113 return false;
114 }
115
116 // If we are reading a pure-2D format, generate 3D coordinates:
117 QStringList options;
118 QStringList formats2D;
119 formats2D << "smi"
120 << "inchi"
121 << "can";
122 if (formats2D.contains(QString::fromStdString(m_fileExtensions.front())))
123 options << "--gen3d";
124
125 // Check if we have extra arguments for open babel
126 json extraArgs = opts.value("arguments", json::object());
127 if (extraArgs.is_array()) {
128 for (const auto& arg : extraArgs) {
129 if (arg.is_string())
130 options << arg.get<std::string>().c_str();
131 }
132 }
133
134 if (!m_fileOnly) {
135 // Determine length of data
136 in.seekg(0, std::ios_base::end);
137 std::istream::pos_type length = in.tellg();
138 in.seekg(0, std::ios_base::beg);
139 in.clear();
140
141 // Extract char data
142 QByteArray input;
143 input.resize(static_cast<int>(length));
144 in.read(input.data(), length);
145 if (in.gcount() != length) {
146 appendError("Error reading stream into buffer!");
147 return false;
148 }
149
150 // Perform the conversion.
151 if (!proc.convert(input, QString::fromStdString(m_fileExtensions.front()),
152 "cml", options)) {
153 appendError("OpenBabel conversion failed!");
154 return false;
155 }
156 } else {
157 // Can only read files. Need absolute path.
158 QString filename = QString::fromStdString(fileName());
159 if (!QFileInfo(filename).isAbsolute()) {
160 appendError("Internal error -- filename must be absolute! " +
161 filename.toStdString());
162 return false;
163 }
164
165 // Perform the conversion.
166 if (!proc.convert(filename,
167 QString::fromStdString(m_fileExtensions.front()), "cml",
168 options)) {
169 appendError("OpenBabel conversion failed!");
170 return false;
171 }
172 }
173
174 QByteArray cmlOutput;
175 if (!listener.waitForOutput(cmlOutput)) {
176 appendError(std::string("Conversion timed out."));
177 return false;
178 }
179
180 if (cmlOutput.isEmpty()) {
181 appendError(std::string("OpenBabel error: conversion failed."));
182 return false;
183 }
184
185 Io::CmlFormat cmlReader;
186 if (!cmlReader.readString(std::string(cmlOutput.constData()), molecule)) {
187 appendError(std::string("Error while reading OpenBabel-generated CML:"));
188 appendError(cmlReader.error());
189 return false;
190 }
191
192 return true;
193 }
194
write(std::ostream & out,const Core::Molecule & molecule)195 bool OBFileFormat::write(std::ostream& out, const Core::Molecule& molecule)
196 {
197 json opts;
198 if (!options().empty())
199 opts = json::parse(options(), nullptr, false);
200 else
201 opts = json::object();
202
203 // Check if we have extra arguments for open babel
204 QStringList options;
205 json extraArgs = opts.value("arguments", json::object());
206 if (extraArgs.is_array()) {
207 for (const auto& arg : extraArgs) {
208 if (arg.is_string())
209 options << arg.get<std::string>().c_str();
210 }
211 }
212
213 // Generate CML to give to OpenBabel
214 std::string cml;
215 Io::CmlFormat cmlWriter;
216 if (!cmlWriter.writeString(cml, molecule)) {
217 appendError(std::string("Error while writing CML:"));
218 appendError(cmlWriter.error());
219 return false;
220 }
221
222 // Block until the OpenBabel conversion finishes:
223 OBProcess proc;
224 ProcessListener listener;
225 QObject::connect(&proc, SIGNAL(convertFinished(QByteArray)), &listener,
226 SLOT(responseReceived(QByteArray)));
227
228 // Just grab the first file extension from the list -- all extensions for a
229 // given format map to the same parsers in OB.
230 if (m_fileExtensions.empty()) {
231 appendError("Internal error: No file extensions set.");
232 return false;
233 }
234 proc.convert(QByteArray(cml.c_str()), "cml",
235 QString::fromStdString(m_fileExtensions.front()), options);
236
237 QByteArray output;
238 if (!listener.waitForOutput(output)) {
239 appendError(std::string("Conversion timed out."));
240 return false;
241 }
242
243 if (output.isEmpty()) {
244 appendError(std::string("OpenBabel error: conversion failed."));
245 return false;
246 }
247
248 out.write(output.constData(), output.size());
249
250 return true;
251 }
252
clear()253 void OBFileFormat::clear()
254 {
255 Io::FileFormat::clear();
256 }
257
newInstance() const258 Io::FileFormat* OBFileFormat::newInstance() const
259 {
260 return new OBFileFormat(m_name, m_identifier, m_description,
261 m_specificationUrl, m_fileExtensions, m_mimeTypes,
262 m_fileOnly);
263 }
264
265 } // namespace QtPlugins
266 } // namespace Avogadro
267
268 #include "obfileformat.moc"
269