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 "CustomToolConfigParser.h"
23 
24 #include <QDir>
25 #include <QDomDocument>
26 #include <QFile>
27 #include <QFileInfo>
28 #include <QRegularExpression>
29 
30 #include <U2Core/CustomExternalTool.h>
31 #include <U2Core/U2OpStatus.h>
32 #include <U2Core/U2SafePoints.h>
33 
34 namespace U2 {
35 
36 const QString CustomToolConfigParser::ELEMENT_CONFIG = "ugeneExternalToolConfig";
37 const QString CustomToolConfigParser::ATTRIBUTE_VERSION = "version";
38 const QString CustomToolConfigParser::HARDCODED_EXPECTED_VERSION = "1.0";
39 
40 const QString CustomToolConfigParser::ID = "id";
41 const QString CustomToolConfigParser::NAME = "name";
42 const QString CustomToolConfigParser::PATH = "executableFullPath";
43 const QString CustomToolConfigParser::DESCRIPTION = "description";
44 const QString CustomToolConfigParser::TOOLKIT_NAME = "toolkitName";
45 const QString CustomToolConfigParser::TOOL_VERSION = "version";
46 const QString CustomToolConfigParser::LAUNCHER_ID = "launcherId";
47 const QString CustomToolConfigParser::DEPENDENCIES = "dependencies";
48 const QString CustomToolConfigParser::BINARY_NAME = "executableName";
49 
50 namespace {
compareCaseInsensetive(const QString & first,const QString & second)51 bool compareCaseInsensetive(const QString &first, const QString &second) {
52     return QString::compare(first, second, Qt::CaseInsensitive) == 0;
53 }
54 }    // namespace
55 
parse(U2OpStatus & os,const QString & url)56 CustomExternalTool *CustomToolConfigParser::parse(U2OpStatus &os, const QString &url) {
57     QFile file(url);
58     CHECK_EXT(file.open(QIODevice::ReadOnly), os.setError(tr("Invalid config file format: file %1 cann not be opened").arg(url)), nullptr);
59 
60     QDomDocument doc;
61     doc.setContent(&file);
62     file.close();
63 
64     QScopedPointer<CustomExternalTool> tool(new CustomExternalTool());
65 
66     const QDomNodeList nodesList = doc.elementsByTagName(ELEMENT_CONFIG);
67     CHECK_EXT(!nodesList.isEmpty(), os.setError(tr("Invalid config file format: custom tool description not found")), nullptr);
68     CHECK_EXT(1 == nodesList.count(), os.setError(tr("Invalid config file format: there are too many entities in the file")), nullptr);
69 
70     QDomElement configElement = nodesList.item(0).toElement();
71     CHECK_EXT(!configElement.isNull(), os.setError(tr("Can't parse the config file")), nullptr);
72 
73     const QString &version = configElement.attribute(ATTRIBUTE_VERSION);
74     CHECK_EXT(HARDCODED_EXPECTED_VERSION == version, os.setError(tr("Can't parse config with version %1").arg(version)), nullptr);
75 
76     const QDomNodeList toolConfigElements = configElement.childNodes();
77     QFileInfo urlFi(url);
78     for (int i = 0, n = toolConfigElements.count(); i < n; ++i) {
79         const QDomElement element = toolConfigElements.item(i).toElement();
80         CHECK_CONTINUE(!element.isNull());
81         const QString tagName = element.tagName();
82 
83         if (compareCaseInsensetive(ID, tagName)) {
84             tool->setId(element.text());
85         } else if (compareCaseInsensetive(NAME, tagName)) {
86             tool->setName(element.text());
87         } else if (compareCaseInsensetive(PATH, tagName)) {
88             if (!element.text().isEmpty()) {
89                 QString text = element.text();
90                 QFileInfo pathFi(element.text());
91                 QString absPath;
92                 if (pathFi.isRelative()) {
93                     QString newPath = urlFi.absoluteDir().absolutePath() + "/" + element.text();
94                     pathFi = QFileInfo(newPath);
95                 }
96                 absPath = pathFi.absoluteFilePath();
97                 tool->setPath(absPath);
98             }
99         } else if (compareCaseInsensetive(DESCRIPTION, tagName)) {
100             tool->setDescription(element.text().replace(QRegularExpression("\\r?\\n"), "<br>"));
101         } else if (compareCaseInsensetive(TOOLKIT_NAME, tagName)) {
102             tool->setToolkitName(element.text());
103         } else if (compareCaseInsensetive(TOOL_VERSION, tagName)) {
104             tool->setPredefinedVersion(element.text());
105         } else if (compareCaseInsensetive(LAUNCHER_ID, tagName)) {
106             tool->setLauncher(element.text());
107         } else if (compareCaseInsensetive(DEPENDENCIES, tagName)) {
108             QStringList dependencies;
109             foreach (const QString &dependency, element.text().split(",", QString::SkipEmptyParts)) {
110                 dependencies << dependency.trimmed();
111             }
112             tool->setDependencies(dependencies);
113         } else if (compareCaseInsensetive(BINARY_NAME, tagName)) {
114             tool->setBinaryName(element.text());
115         } else {
116             os.addWarning(tr("Unknown element: '%1', skipping").arg(tagName));
117         }
118     }
119 
120     if (tool->getPath().isEmpty()) {
121         QString expectedExecutableUrl = urlFi.absoluteDir().absolutePath() + "/" + tool->getExecutableFileName();
122         QFile expectedExecutable(expectedExecutableUrl);
123         if (expectedExecutable.exists()) {
124             tool->setPath(expectedExecutableUrl);
125         }
126     }
127 
128     if (tool->getToolKitName().isEmpty()) {
129         tool->setToolkitName(tool->getName());
130     }
131 
132     const bool valid = validate(os, tool.data());
133     CHECK(valid, nullptr);
134 
135     return tool.take();
136 }
137 
serialize(CustomExternalTool * tool)138 QDomDocument CustomToolConfigParser::serialize(CustomExternalTool *tool) {
139     QDomDocument doc;
140     QDomProcessingInstruction xmlHeader = doc.createProcessingInstruction("xml", "version = \"1.0\" encoding = \"UTF-8\"");
141     doc.appendChild(xmlHeader);
142 
143     QDomElement configElement = doc.createElement(ELEMENT_CONFIG);
144     configElement.setAttribute(ATTRIBUTE_VERSION, HARDCODED_EXPECTED_VERSION);
145     configElement.appendChild(addChildElement(doc, ID, tool->getId()));
146     configElement.appendChild(addChildElement(doc, NAME, tool->getName()));
147     configElement.appendChild(addChildElement(doc, PATH, tool->getPath()));
148     configElement.appendChild(addChildElement(doc, DESCRIPTION, tool->getDescription()));
149     configElement.appendChild(addChildElement(doc, TOOLKIT_NAME, tool->getToolKitName()));
150     configElement.appendChild(addChildElement(doc, TOOL_VERSION, tool->getPredefinedVersion()));
151     configElement.appendChild(addChildElement(doc, LAUNCHER_ID, tool->getToolRunnerProgramId()));
152     configElement.appendChild(addChildElement(doc, DEPENDENCIES, tool->getDependencies().join(",")));
153     configElement.appendChild(addChildElement(doc, BINARY_NAME, tool->getExecutableFileName()));
154     doc.appendChild(configElement);
155     return doc;
156 }
157 
validate(U2OpStatus & os,CustomExternalTool * tool)158 bool CustomToolConfigParser::validate(U2OpStatus &os, CustomExternalTool *tool) {
159     CHECK(nullptr != tool, false);
160     CHECK_EXT(!tool->getId().isEmpty(), os.setError(tr("The tool id is not specified in the config file.")), false);
161     CHECK_EXT(!tool->getId().contains(QRegularExpression("[^A-Za-z0-9_\\-]")), os.setError(tr("The tool id contains unexpected characters, the only letters, numbers, underlines and dashes are allowed.")), false);
162     CHECK_EXT(!tool->getId().startsWith("USUPP_"), os.setError(tr("The custom tool's ID shouldn't start with \"USUPP_\", this is a distinguishing feature of the supported tools.")), false);
163     CHECK_EXT(!tool->getId().startsWith("UCUST_"), os.setError(tr("The custom tool's ID shouldn't start with \"UCUST_\", this is a distinguishing feature of the supported tools.")), false);
164     CHECK_EXT(!tool->getName().isEmpty(), os.setError(tr("The tool name is not specified in the config file.")), false);
165 
166     CHECK_EXT(!tool->getExecutableFileName().isEmpty(), os.setError(tr("The imported custom tool \"%1\" does not have an executable file. Make sure to set up a valid executable file before you use the tool.").arg(tool->getName())), false)
167     if (tool->getPath().isEmpty()) {
168         os.addWarning(tr("The imported custom tool \"%1\" does not have an executable file. Make sure to set up a valid executable file before you use the tool.").arg(tool->getName()));
169     } else {
170         QFileInfo pathFi(tool->getPath());
171         if (!pathFi.exists()) {
172             os.addWarning(tr("The executable file \"%1\" specified for the imported custom tool \"%2\" doesn't exist. Make sure to set up a valid executable file before you use the tool.").arg(tool->getPath()).arg(tool->getName()));
173         }
174     }
175 
176     return true;
177 }
178 
addChildElement(QDomDocument & doc,const QString & elementName,const QString & elementData)179 QDomElement CustomToolConfigParser::addChildElement(QDomDocument &doc, const QString &elementName, const QString &elementData) {
180     QDomElement element = doc.createElement(elementName);
181     QDomText elementDataNode = doc.createTextNode(elementData);
182     element.appendChild(elementDataNode);
183     return element;
184 }
185 
186 }    // namespace U2
187