1 /******************************************************************************
2 * Copyright (c) 2011, Michael P. Gerlek (mpg@flaxen.com)
3 *
4 * All rights reserved.
5 *
6 * Redistribution and use in source and binary forms, with or without
7 * modification, are permitted provided that the following
8 * conditions are met:
9 *
10 * * Redistributions of source code must retain the above copyright
11 * notice, this list of conditions and the following disclaimer.
12 * * Redistributions in binary form must reproduce the above copyright
13 * notice, this list of conditions and the following disclaimer in
14 * the documentation and/or other materials provided
15 * with the distribution.
16 * * Neither the name of Hobu, Inc. or Flaxen Geo Consulting nor the
17 * names of its contributors may be used to endorse or promote
18 * products derived from this software without specific prior
19 * written permission.
20 *
21 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
22 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
23 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
24 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
25 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
26 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
27 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
28 * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
29 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
30 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
31 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
32 * OF SUCH DAMAGE.
33 ****************************************************************************/
34
35 #include <nlohmann/json.hpp>
36
37 #include <pdal/Filter.hpp>
38 #include <pdal/PipelineReaderJSON.hpp>
39 #include <pdal/PipelineManager.hpp>
40 #include <pdal/PluginManager.hpp>
41 #include <pdal/Options.hpp>
42 #include <pdal/util/FileUtils.hpp>
43 #include <pdal/util/Algorithm.hpp>
44 #include <pdal/util/Utils.hpp>
45
46 #include <memory>
47 #include <vector>
48
49 namespace pdal
50 {
51
PipelineReaderJSON(PipelineManager & manager)52 PipelineReaderJSON::PipelineReaderJSON(PipelineManager& manager) :
53 m_manager(manager)
54 {}
55
56
parsePipeline(NL::json & tree)57 void PipelineReaderJSON::parsePipeline(NL::json& tree)
58 {
59 TagMap tags;
60 std::vector<Stage*> inputs;
61
62 size_t last = tree.size() - 1;
63 for (size_t i = 0; i < tree.size(); ++i)
64 {
65 NL::json& node = tree.at(i);
66
67 std::string filename;
68 std::string tag;
69 std::string type;
70 std::vector<Stage*> specifiedInputs;
71 Options options;
72
73 // strings are assumed to be filenames
74 if (node.is_string())
75 {
76 filename = node.get<std::string>();
77 }
78 else
79 {
80 type = extractType(node);
81 filename = extractFilename(node);
82 tag = extractTag(node, tags);
83 specifiedInputs = extractInputs(node, tags);
84 if (!specifiedInputs.empty())
85 inputs = specifiedInputs;
86 options = extractOptions(node);
87 }
88
89 Stage *s = nullptr;
90
91 // The type is inferred from a filename as a reader if it's not
92 // the last stage or if there's only one.
93 if ((type.empty() && (i == 0 || i != last)) ||
94 Utils::startsWith(type, "readers."))
95 {
96 StringList files = FileUtils::glob(filename);
97 if (files.empty())
98 files.push_back(filename);
99
100 for (const std::string& path : files)
101 {
102 StageCreationOptions ops { path, type, nullptr, options, tag };
103 s = &m_manager.makeReader(ops);
104
105 if (specifiedInputs.size())
106 throw pdal_error("JSON pipeline: Inputs not permitted for "
107 " reader: '" + path + "'.");
108 inputs.push_back(s);
109 }
110 }
111 else if (type.empty() || Utils::startsWith(type, "writers."))
112 {
113 StageCreationOptions ops { filename, type, nullptr, options, tag };
114 s = &m_manager.makeWriter(ops);
115 for (Stage *ts : inputs)
116 s->setInput(*ts);
117 inputs.clear();
118 inputs.push_back(s);
119 }
120 else
121 {
122 if (filename.size())
123 options.add("filename", filename);
124 StageCreationOptions ops { "", type, nullptr, options, tag };
125 s = &m_manager.makeFilter(ops);
126 for (Stage *ts : inputs)
127 s->setInput(*ts);
128 inputs.clear();
129 inputs.push_back(s);
130 }
131 // 's' should be valid at this point. makeXXX will throw if the stage
132 // couldn't be constructed.
133 if (tag.size())
134 tags[tag] = s;
135 }
136 }
137
138
readPipeline(std::istream & input)139 void PipelineReaderJSON::readPipeline(std::istream& input)
140 {
141 NL::json root;
142
143 try
144 {
145 root = NL::json::parse(input);
146 }
147 catch (NL::json::parse_error& err)
148 {
149 // Look for a right bracket -- this indicates the start of the
150 // actual message from the parse error.
151 std::string s(err.what());
152 auto pos = s.find("]");
153 if (pos != std::string::npos)
154 s = s.substr(pos + 1);
155 throw pdal_error("Pipeline:" + s);
156 }
157
158 auto it = root.find("pipeline");
159 if (root.is_object() && it != root.end())
160 parsePipeline(*it);
161 else if (root.is_array())
162 parsePipeline(root);
163 else
164 throw pdal_error("Pipeline: root element is not a pipeline.");
165 }
166
167
readPipeline(const std::string & filename)168 void PipelineReaderJSON::readPipeline(const std::string& filename)
169 {
170 std::istream* input = Utils::openFile(filename);
171 if (!input)
172 {
173 throw pdal_error("Pipeline: Unable to open stream for "
174 "file \"" + filename + "\"");
175 }
176
177 try
178 {
179 readPipeline(*input);
180 }
181 catch (...)
182 {
183 Utils::closeFile(input);
184 throw;
185 }
186
187 Utils::closeFile(input);
188 }
189
190
extractType(NL::json & node)191 std::string PipelineReaderJSON::extractType(NL::json& node)
192 {
193 std::string type;
194
195 auto it = node.find("type");
196 if (it != node.end())
197 {
198 NL::json& val = *it;
199 if (!val.is_null())
200 {
201 if (val.is_string())
202 type = val.get<std::string>();
203 else
204 throw pdal_error("JSON pipeline: 'type' must be specified as "
205 "a string.");
206 }
207 node.erase(it);
208 }
209 return type;
210 }
211
212
extractFilename(NL::json & node)213 std::string PipelineReaderJSON::extractFilename(NL::json& node)
214 {
215 std::string filename;
216
217 auto it = node.find("filename");
218 if (it != node.end())
219 {
220 NL::json& val = *it;
221 if (!val.is_null())
222 {
223 if (val.is_string())
224 filename = val.get<std::string>();
225 else
226 throw pdal_error("JSON pipeline: 'filename' must be "
227 "specified as a string.");
228 }
229 node.erase(it);
230 }
231 return filename;
232 }
233
234
extractTag(NL::json & node,TagMap & tags)235 std::string PipelineReaderJSON::extractTag(NL::json& node, TagMap& tags)
236 {
237 std::string tag;
238
239 auto it = node.find("tag");
240 if (it != node.end())
241 {
242 NL::json& val = *it;
243 if (!val.is_null())
244 {
245 if (val.is_string())
246 {
247 tag = val.get<std::string>();
248 if (tags.find(tag) != tags.end())
249 throw pdal_error("JSON pipeline: duplicate tag '" +
250 tag + "'.");
251 }
252 else
253 throw pdal_error("JSON pipeline: tag must be "
254 "specified as a string.");
255 }
256 node.erase(it);
257 std::string::size_type pos = 0;
258 if (!Stage::parseTagName(tag, pos) || pos != tag.size())
259 throw pdal_error("JSON pipeline: Invalid tag name '" + tag + "'. "
260 "Must start with letter. Remainder can be letters, "
261 "digits or underscores.");
262 }
263 return tag;
264 }
265
266
handleInputTag(const std::string & tag,const TagMap & tags,std::vector<Stage * > & inputs)267 void PipelineReaderJSON::handleInputTag(const std::string& tag,
268 const TagMap& tags, std::vector<Stage *>& inputs)
269 {
270 auto ii = tags.find(tag);
271 if (ii == tags.end())
272 throw pdal_error("JSON pipeline: Invalid pipeline: "
273 "undefined stage tag '" + tag + "'.");
274 else
275 inputs.push_back(ii->second);
276 }
277
278
extractInputs(NL::json & node,TagMap & tags)279 std::vector<Stage *> PipelineReaderJSON::extractInputs(NL::json& node,
280 TagMap& tags)
281 {
282 std::vector<Stage *> inputs;
283 std::string filename;
284
285 auto it = node.find("inputs");
286 if (it != node.end())
287 {
288 NL::json& val = *it;
289 if (val.is_string())
290 handleInputTag(val.get<std::string>(), tags, inputs);
291 else if (val.is_array())
292 {
293 for (auto& input : val)
294 {
295 if (!input.is_string())
296 throw pdal_error("JSON pipeline: 'inputs' tag must "
297 " be specified as a string or array of strings.");
298 handleInputTag(input.get<std::string>(), tags, inputs);
299 }
300 }
301 else
302 throw pdal_error("JSON pipeline: 'inputs' tag must "
303 " be specified as a string or array of strings.");
304 node.erase(it);
305 }
306 return inputs;
307 }
308
309 namespace
310 {
311
extractOption(Options & options,const std::string & name,const NL::json & node)312 bool extractOption(Options& options, const std::string& name,
313 const NL::json& node)
314 {
315 if (node.is_string())
316 options.add(name, node.get<std::string>());
317 else if (node.is_number_unsigned())
318 options.add(name, node.get<uint64_t>());
319 else if (node.is_number_integer())
320 options.add(name, node.get<int64_t>());
321 else if (node.is_number_float())
322 options.add(name, node.get<double>());
323 else if (node.is_boolean())
324 options.add(name, node.get<bool>());
325 else if (node.is_null())
326 options.add(name, "");
327 else
328 return false;
329 return true;
330 }
331
332 } // unnamed namespace
333
extractOptions(NL::json & node)334 Options PipelineReaderJSON::extractOptions(NL::json& node)
335 {
336 Options options;
337
338 for (auto& it : node.items())
339 {
340 NL::json& subnode = it.value();
341 const std::string& name = it.key();
342
343 if (name == "plugin")
344 {
345 PluginManager<Stage>::loadPlugin(subnode.get<std::string>());
346
347 // Don't actually put a "plugin" option on
348 // any stage
349 continue;
350 }
351
352 if (subnode.is_array())
353 {
354 for (const NL::json& val : subnode)
355 if (val.is_object())
356 options.add(name, val);
357 else if (!extractOption(options, name, val))
358 throw pdal_error("JSON pipeline: Invalid value type for "
359 "option list '" + name + "'.");
360 }
361 else if (subnode.is_object())
362 options.add(name, subnode);
363 else if (!extractOption(options, name, subnode))
364 throw pdal_error("JSON pipeline: Value of stage option '" +
365 name + "' cannot be converted.");
366 }
367 node.clear();
368 return options;
369 }
370
371 } // namespace pdal
372