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