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