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