1 /**
2 * UGENE - Integrated Bioinformatics Tools.
3 * Copyright (C) 2008-2021 UniPro <ugene@unipro.ru>
4 * http://ugene.net
5 *
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU General Public License
8 * as published by the Free Software Foundation; either version 2
9 * of the License, or (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
19 * MA 02110-1301, USA.
20 */
21
22 #include "PluginDescriptor.h"
23
24 #include <QDir>
25 #include <QDomDocument>
26
27 #include <U2Core/GAutoDeleteList.h>
28 #include <U2Core/L10n.h>
29
30 namespace U2 {
31
platformFromText(const QString & text)32 static PlatformName platformFromText(const QString &text) {
33 QString trimmed = text.trimmed();
34 if (trimmed == "win") {
35 return PlatformName_Win;
36 }
37 if (trimmed == "unix") {
38 return PlatformName_UnixNotMac;
39 }
40 if (trimmed == "macx") {
41 return PlatformName_Mac;
42 }
43 return PlatformName_Unknown;
44 }
45
archFromText(const QString & text)46 static PlatformArch archFromText(const QString &text) {
47 QString trimmed = text.trimmed();
48 if (trimmed == "32") {
49 return PlatformArch_32;
50 }
51 if (trimmed == "64") {
52 return PlatformArch_64;
53 }
54 return PlatformArch_Unknown;
55 }
56
modeFromText(const QString & text)57 static PluginMode modeFromText(const QString &text) {
58 QString trimmed = text.trimmed().toLower();
59 QStringList tokens = trimmed.split(QRegExp("[\\s,]"), QString::SkipEmptyParts);
60 PluginMode result;
61 if (tokens.isEmpty()) {
62 result |= PluginMode_Malformed;
63 return result;
64 }
65 foreach (const QString &token, tokens) {
66 if (token == "ui") {
67 result |= PluginMode_UI;
68 } else if (token == "console") {
69 result |= PluginMode_Console;
70 } else {
71 result |= PluginMode_Malformed;
72 return result;
73 }
74 }
75 return result;
76 }
77
readPluginDescriptor(const QString & descUrl,QString & error)78 PluginDesc PluginDescriptorHelper::readPluginDescriptor(const QString &descUrl, QString &error) {
79 PluginDesc result;
80 PluginDesc failResult; // empty one, used if parsing is failed
81
82 QFile f(descUrl);
83 if (!f.open(QIODevice::ReadOnly)) {
84 error = L10N::errorOpeningFileRead(descUrl);
85 return failResult;
86 }
87
88 result.descriptorUrl = descUrl;
89
90 QByteArray xmlData = f.readAll();
91 f.close();
92
93 QDomDocument doc;
94 bool res = doc.setContent(xmlData);
95 if (!res) {
96 error = L10N::notValidFileFormat("XML", descUrl);
97 return failResult;
98 }
99
100 QDomElement pluginElement = doc.documentElement();
101 QString pluginElementName = pluginElement.tagName();
102 if (pluginElementName != "ugene-plugin") {
103 error = L10N::notValidFileFormat("UGENE plugin", descUrl);
104 return failResult;
105 }
106
107 result.id = pluginElement.attribute("id");
108 if (result.id.isEmpty()) {
109 error = tr("Required attribute not found %1").arg("id");
110 return failResult;
111 }
112
113 result.pluginVersion = Version::parseVersion(pluginElement.attribute("version"));
114 if (result.pluginVersion.text.isEmpty()) {
115 error = tr("Required attribute not found %1").arg("version");
116 return failResult;
117 }
118
119 result.ugeneVersion = Version::parseVersion(pluginElement.attribute("ugene-version"));
120 if (result.ugeneVersion.text.isEmpty()) {
121 error = tr("Required attribute not found %1").arg("ugene-version");
122 return failResult;
123 }
124
125 result.qtVersion = Version::parseVersion(pluginElement.attribute("qt-version"));
126 if (result.qtVersion.text.isEmpty()) {
127 error = tr("Required attribute not found %1").arg("qt-version");
128 return failResult;
129 }
130
131 QDomElement libraryElement = pluginElement.firstChildElement("library");
132 QString libraryUrlText = libraryElement.text();
133 if (!libraryUrlText.isEmpty() && QFileInfo(libraryUrlText).isRelative()) { // if path is relative, use descriptor dir as 'current folder'
134 libraryUrlText = QFileInfo(descUrl).absoluteDir().canonicalPath() + "/" + libraryUrlText;
135 }
136 result.libraryUrl = libraryUrlText;
137 if (result.libraryUrl.isEmpty()) {
138 error = tr("Required element not found %1").arg("library");
139 return failResult;
140 }
141 QString licenseUrl = QString(result.id + ".license");
142 if (QFileInfo(licenseUrl).isRelative()) { // if path is relative, use descriptor dir as 'current folder'
143 licenseUrl = QFileInfo(descUrl).absoluteDir().canonicalPath() + "/" + licenseUrl;
144 }
145 result.licenseUrl = licenseUrl;
146
147 result.name = pluginElement.firstChildElement("name").text();
148 if (result.name.isNull()) {
149 error = tr("Required element not found %1").arg("name");
150 return failResult;
151 }
152
153 result.pluginVendor = pluginElement.firstChildElement("plugin-vendor").text();
154 if (result.pluginVendor.isNull()) {
155 error = tr("Required element not found %1").arg("plugin-vendor");
156 return failResult;
157 }
158
159 QString pluginModeText = pluginElement.firstChildElement("plugin-mode").text();
160 result.mode = modeFromText(pluginModeText);
161 if (result.mode.testFlag(PluginMode_Malformed)) {
162 error = tr("Not valid value: '%1', plugin: %2").arg(pluginModeText).arg("plugin-mode");
163 return failResult;
164 }
165
166 QDomElement platformElement = pluginElement.firstChildElement("platform");
167 QString platformNameText = platformElement.attribute("name");
168 result.platform.name = platformFromText(platformNameText);
169 if (result.platform.name == PlatformName_Unknown) {
170 error = tr("Platform arch is unknown: %1").arg(platformNameText);
171 return failResult;
172 }
173
174 QString platformArchText = platformElement.attribute("arch");
175 result.platform.arch = archFromText(platformArchText);
176 if (result.platform.arch == PlatformArch_Unknown) {
177 error = tr("Platform bits is unknown: %1").arg(platformArchText);
178 return failResult;
179 }
180
181 QString debugText = pluginElement.firstChildElement("debug-build").text();
182 bool debug = debugText == "true" || debugText == "yes" || debugText.toInt() == 1;
183 result.qtVersion.debug = result.ugeneVersion.debug = result.pluginVersion.debug = debug;
184
185 QDomNodeList dependsElements = pluginElement.elementsByTagName("depends");
186 for (int i = 0; i < dependsElements.size(); i++) {
187 QDomNode dn = dependsElements.item(i);
188 if (!dn.isElement()) {
189 continue;
190 }
191 QString dependsText = dn.toElement().text();
192 QStringList dependsTokes = dependsText.split(QChar(';'), QString::SkipEmptyParts);
193 foreach (const QString &token, dependsTokes) {
194 QStringList plugAndVersion = token.split(QChar(':'), QString::KeepEmptyParts);
195 if (plugAndVersion.size() != 2) {
196 error = tr("Invalid depends token: %1").arg(token);
197 return failResult;
198 }
199 DependsInfo di;
200 di.id = plugAndVersion.at(0);
201 di.version = Version::parseVersion(plugAndVersion.at(1));
202 result.dependsList.append(di);
203 }
204 }
205
206 return result;
207 }
208
PluginDesc()209 PluginDesc::PluginDesc()
210 : mode(PluginMode_Malformed) {
211 }
212
operator ==(const PluginDesc & pd) const213 bool PluginDesc::operator==(const PluginDesc &pd) const {
214 return id == pd.id && pluginVersion == pd.pluginVersion && ugeneVersion == pd.ugeneVersion && qtVersion == pd.qtVersion && libraryUrl == pd.libraryUrl && licenseUrl == pd.licenseUrl && platform == pd.platform && mode == pd.mode;
215 }
216
217 //////////////////////////////////////////////////////////////////////////
218 // ordering
219
220 // states set used for DFS graph traversal
221 enum DepNodeState {
222 DS_Clean,
223 DS_InProcess,
224 DS_Done
225 };
226
227 class DepNode {
228 public:
DepNode()229 DepNode() {
230 state = DS_Clean;
231 root = false;
232 }
233 QList<DepNode *> parentNodes; // nodes this node depends on
234 QList<DepNode *> childNodes; // nodes that depends on this node
235 PluginDesc desc;
236
237 DepNodeState state;
238 bool root;
239 };
240
resetState(const QList<DepNode * > & nodes)241 static void resetState(const QList<DepNode *> &nodes) {
242 foreach (DepNode *node, nodes) {
243 node->state = DS_Clean;
244 }
245 }
246
findParentNodes(DepNode * node,const PluginDesc & desc,QString & err,QList<DepNode * > & result)247 static void findParentNodes(DepNode *node, const PluginDesc &desc, QString &err, QList<DepNode *> &result) {
248 assert(node->state == DS_Clean);
249 node->state = DS_InProcess;
250 foreach (DepNode *childNode, node->childNodes) {
251 if (childNode->state == DS_Done) { // check if node is already processed
252 continue;
253 }
254 if (childNode->state == DS_InProcess) { // circular dependency between plugins
255 err = PluginDescriptorHelper::tr("Plugin circular dependency detected: %1 <-> %2").arg(desc.id).arg(node->desc.id);
256 return;
257 }
258 findParentNodes(childNode, desc, err, result);
259 }
260 foreach (const DependsInfo &di, desc.dependsList) {
261 if (di.id == node->desc.id && di.version <= node->desc.pluginVersion) {
262 result.append(node);
263 break;
264 }
265 }
266 node->state = DS_Done;
267 }
268
orderPostorder(DepNode * node,QList<PluginDesc> & result)269 static void orderPostorder(DepNode *node, QList<PluginDesc> &result) {
270 assert(node->state == DS_Clean);
271 node->state = DS_InProcess;
272 foreach (DepNode *childNode, node->childNodes) {
273 if (childNode->state != DS_Clean) {
274 continue;
275 }
276 orderPostorder(childNode, result);
277 }
278 if (!node->root) {
279 result.append(node->desc);
280 }
281 node->state = DS_Done;
282 }
283
orderTopological(DepNode * node,QList<PluginDesc> & result)284 static void orderTopological(DepNode *node, QList<PluginDesc> &result) {
285 orderPostorder(node, result);
286 QList<PluginDesc> topologicalResult;
287 QListIterator<PluginDesc> it(result);
288 it.toBack();
289 while (it.hasPrevious()) {
290 topologicalResult.append(it.previous());
291 }
292 result = topologicalResult;
293 }
294
orderPlugins(const QList<PluginDesc> & unordered,QString & err)295 QList<PluginDesc> PluginDescriptorHelper::orderPlugins(const QList<PluginDesc> &unordered, QString &err) {
296 // Sort plugin using dependency graph.
297 // Root node has no dependencies. All child nodes depends on all parents.
298 QList<PluginDesc> result;
299 if (unordered.isEmpty()) {
300 return unordered;
301 }
302
303 GAutoDeleteList<DepNode> allNodes;
304 DepNode *rootNode = new DepNode();
305 rootNode->root = true;
306 allNodes.qlist.append(rootNode);
307
308 QList<PluginDesc> queue = unordered;
309
310 int iterations = 0;
311 int maxIterations = queue.size();
312
313 do {
314 maxIterations = queue.size();
315 PluginDesc desc = queue.takeFirst();
316 QList<DepNode *> nodes;
317 int nDeps = desc.dependsList.size();
318 if (nDeps == 0) {
319 nodes.append(rootNode);
320 } else {
321 resetState(allNodes.qlist);
322 findParentNodes(rootNode, desc, err, nodes);
323 }
324 if (!err.isEmpty()) {
325 return unordered;
326 }
327 if (nDeps == 0 || nodes.size() == nDeps) {
328 DepNode *descNode = new DepNode();
329 descNode->desc = desc;
330 allNodes.qlist.append(descNode);
331 // now add this node as a child to all nodes it depends on
332 foreach (DepNode *node, nodes) {
333 node->childNodes.append(descNode);
334 descNode->parentNodes.append(node);
335 }
336 iterations = 0;
337 continue;
338 }
339 queue.append(desc);
340 iterations++;
341 } while (!queue.isEmpty() && iterations <= maxIterations);
342
343 if (!queue.isEmpty()) {
344 err = tr("Can't satisfy dependencies for %1 !").arg(queue.first().id);
345 return unordered;
346 }
347
348 // traverse graph and add nodes in topological (reverse postorder) mode
349 resetState(allNodes.qlist);
350 orderTopological(rootNode, result);
351
352 foreach (const PluginDesc &desc, result) {
353 if (desc.id.contains("pcr", Qt::CaseInsensitive)) {
354 result.removeAll(desc);
355 result.prepend(desc);
356 }
357 }
358
359 #ifdef _DEBUG
360 assert(result.size() == unordered.size());
361 foreach (const PluginDesc &desc, unordered) {
362 int idx = result.indexOf(desc);
363 assert(idx >= 0);
364 }
365 #endif
366
367 return result;
368 }
369
370 } // namespace U2
371