001/*
002 * Copyright (c) 2004-2020, Oracle and/or its affiliates.
003 *
004 * Licensed under the 2-clause BSD license.
005 *
006 * Redistribution and use in source and binary forms, with or without
007 * modification, are permitted provided that the following conditions are met:
008 *
009 * 1. Redistributions of source code must retain the above copyright notice,
010 *    this list of conditions and the following disclaimer.
011 *
012 * 2. Redistributions in binary form must reproduce the above copyright notice,
013 *    this list of conditions and the following disclaimer in the documentation
014 *    and/or other materials provided with the distribution.
015 *
016 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
017 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
018 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
019 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
020 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
021 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
022 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
023 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
024 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
025 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
026 * POSSIBILITY OF SUCH DAMAGE.
027 */
028
029package com.oracle.labs.mlrg.olcut.config.json;
030
031import com.fasterxml.jackson.core.JsonFactory;
032import com.fasterxml.jackson.core.JsonParser;
033import com.fasterxml.jackson.databind.JsonNode;
034import com.fasterxml.jackson.databind.ObjectMapper;
035import com.fasterxml.jackson.databind.node.ArrayNode;
036import com.fasterxml.jackson.databind.node.ObjectNode;
037import com.oracle.labs.mlrg.olcut.config.io.ConfigLoader;
038import com.oracle.labs.mlrg.olcut.config.io.ConfigLoaderException;
039import com.oracle.labs.mlrg.olcut.config.ConfigurationData;
040import com.oracle.labs.mlrg.olcut.config.ConfigurationManager;
041import com.oracle.labs.mlrg.olcut.config.property.GlobalProperties;
042import com.oracle.labs.mlrg.olcut.config.property.ListProperty;
043import com.oracle.labs.mlrg.olcut.config.property.MapProperty;
044import com.oracle.labs.mlrg.olcut.config.PropertyException;
045import com.oracle.labs.mlrg.olcut.config.property.SimpleProperty;
046import com.oracle.labs.mlrg.olcut.config.io.URLLoader;
047import com.oracle.labs.mlrg.olcut.config.SerializedObject;
048import com.oracle.labs.mlrg.olcut.util.IOUtil;
049
050import java.io.File;
051import java.io.IOException;
052import java.io.InputStream;
053import java.net.MalformedURLException;
054import java.net.URL;
055import java.security.AccessController;
056import java.security.PrivilegedAction;
057import java.util.ArrayList;
058import java.util.HashMap;
059import java.util.Iterator;
060import java.util.Map;
061import java.util.Map.Entry;
062import java.util.logging.Level;
063import java.util.logging.Logger;
064
065/**
066 *
067 */
068public class JsonLoader implements ConfigLoader {
069
070    private static final Logger logger = Logger.getLogger(JsonLoader.class.getName());
071
072    private final JsonFactory factory;
073
074    private final URLLoader parent;
075
076    private final Map<String, ConfigurationData> rpdMap;
077
078    private final Map<String, ConfigurationData> existingRPD;
079
080    private final Map<String, SerializedObject> serializedObjects;
081
082    private final GlobalProperties globalProperties;
083
084    private String workingDir;
085
086    public JsonLoader(JsonFactory factory, URLLoader parent, Map<String, ConfigurationData> rpdMap, Map<String, ConfigurationData> existingRPD,
087                      Map<String, SerializedObject> serializedObjects, GlobalProperties globalProperties) {
088        this.factory = factory;
089        this.parent = parent;
090        this.rpdMap = rpdMap;
091        this.existingRPD = existingRPD;
092        this.serializedObjects = serializedObjects;
093        this.globalProperties = globalProperties;
094    }
095
096    /**
097     * Loads json configuration data from the location
098     */
099    @Override
100    public final void load(URL url) throws ConfigLoaderException {
101        AccessController.doPrivileged((PrivilegedAction<Void>)
102                () -> {
103                    if (url.getProtocol().equals("file")) {
104                        workingDir = new File(url.getFile()).getParent();
105                    } else if (IOUtil.isDisallowedProtocol(url)) {
106                        throw new ConfigLoaderException("Unable to load configurations from URLs with protocol: " + url.getProtocol());
107                    } else {
108                        workingDir = "";
109                    }
110                    try (JsonParser parser = factory.createParser(url)) {
111                        parser.configure(JsonParser.Feature.STRICT_DUPLICATE_DETECTION, true);
112                        parseJson(parser);
113                    } catch (IOException e) {
114                        String msg = "Error while parsing " + url.toString() + ": " + e.getMessage();
115                        throw new ConfigLoaderException(e, msg);
116                    }
117                    return null;
118                }
119        );
120    }
121
122    /**
123     * Loads json configuration data from the stream
124     */
125    @Override
126    public void load(InputStream stream) throws ConfigLoaderException {
127        try (JsonParser parser = factory.createParser(stream)) {
128            parser.configure(JsonParser.Feature.STRICT_DUPLICATE_DETECTION, true);
129            parseJson(parser);
130        } catch (IOException e) {
131            String msg = "Error while parsing input: " + e.getMessage();
132            throw new ConfigLoaderException(e, msg);
133        }
134    }
135
136    public Map<String, ConfigurationData> getPropertyMap() {
137        return rpdMap;
138    }
139
140    public Map<String, SerializedObject> getSerializedObjects() {
141        return serializedObjects;
142    }
143
144    public GlobalProperties getGlobalProperties() {
145        return globalProperties;
146    }
147
148    protected void parseJson(JsonParser parser) {
149        ObjectMapper mapper = new ObjectMapper();
150        try {
151            parser.nextToken(); // now currentToken == START_OBJECT
152            if (parser.nextToken() == null) {
153                throw new ConfigLoaderException("Failed to parse JSON, did not start with " + ConfigLoader.CONFIG + " object.");
154            } // now currentToken == CONFIG
155            if (parser.currentName().equals(ConfigLoader.CONFIG)) {
156                parser.nextToken();
157                ObjectNode node = mapper.readTree(parser);
158                ObjectNode globalPropertiesNode = (ObjectNode) node.get(ConfigLoader.GLOBALPROPERTIES);
159                ArrayNode filesNode = (ArrayNode) node.get(ConfigLoader.FILES);
160                ArrayNode serializedObjectsNode = (ArrayNode) node.get(ConfigLoader.SERIALIZEDOBJECTS);
161                ArrayNode componentsNode = (ArrayNode) node.get(ConfigLoader.COMPONENTS);
162                if (globalPropertiesNode != null) {
163                    Iterator<Entry<String,JsonNode>> itr = globalPropertiesNode.fields();
164                    while (itr.hasNext()) {
165                        Entry<String,JsonNode> e = itr.next();
166                        try {
167                            globalProperties.setValue(e.getKey(), e.getValue().textValue());
168                        } catch (PropertyException ex) {
169                            throw new ConfigLoaderException("Invalid global property name: " + e.getKey());
170                        }
171                    }
172                }
173                if (filesNode != null) {
174                    for (JsonNode file : filesNode) {
175                        parseFile((ObjectNode)file);
176                    }
177                }
178                if (serializedObjectsNode != null) {
179                    for (JsonNode serialized : serializedObjectsNode) {
180                        parseSerializedObject((ObjectNode)serialized);
181                    }
182                }
183                if (componentsNode != null) {
184                    for (JsonNode component : componentsNode) {
185                        parseComponent((ObjectNode)component);
186                    }
187                }
188
189            } else {
190                throw new ConfigLoaderException("Did not start with " + ConfigLoader.CONFIG + " object.");
191            }
192        } catch (IOException | ClassCastException e) {
193            throw new ConfigLoaderException(e);
194        }
195    }
196
197    protected void parseComponent(ObjectNode node) {
198        boolean overriding = false;
199        JsonNode curComponentNode = node.get(ConfigLoader.NAME);
200        JsonNode curTypeNode = node.get(ConfigLoader.TYPE);
201        JsonNode overrideNode = node.get(ConfigLoader.INHERIT);
202        //
203        // Check for a badly formed component tag.
204        if (curComponentNode == null || (curTypeNode == null && overrideNode == null)) {
205            throw new ConfigLoaderException("Component element must specify "
206                    + "'name' and either 'type' or 'inherit' attributes, found " + node.toString());
207        }
208        String curComponent = curComponentNode.textValue();
209        String curType = curTypeNode != null ? curTypeNode.textValue() : null;
210        String override = overrideNode != null ? overrideNode.textValue() : null;
211
212        JsonNode export = node.get(ConfigLoader.EXPORT);
213        boolean exportable = export != null && Boolean.parseBoolean(export.textValue());
214        JsonNode impNode = node.get(ConfigLoader.IMPORT);
215        boolean importable = impNode != null && Boolean.parseBoolean(impNode.textValue());
216        JsonNode lt = node.get(ConfigLoader.LEASETIME);
217        if (export == null && lt != null) {
218            throw new ConfigLoaderException("lease timeout " + lt +
219                    " specified for component that does not have export set, at node " + node.toString());
220        }
221        long leaseTime = ConfigurationData.DEFAULT_LEASE_TIME;
222        if (lt != null) {
223            try {
224                leaseTime = Long.parseLong(lt.textValue());
225                if (leaseTime < 0) {
226                    throw new ConfigLoaderException("lease timeout "
227                            + lt + " must be greater than 0, for component " + curComponent);
228                }
229            } catch (NumberFormatException nfe) {
230                throw new ConfigLoaderException("lease timeout "
231                        + lt + " must be a long, for component " + curComponent);
232            }
233        }
234        JsonNode entriesNameNode = node.get(ConfigLoader.ENTRIES);
235        String entriesName = entriesNameNode != null ? entriesNameNode.textValue() : null;
236        JsonNode serializedFormNode = node.get(ConfigLoader.SERIALIZED);
237        String serializedForm = serializedFormNode != null ? serializedFormNode.textValue() : null;
238
239        ConfigurationData rpd;
240        if (override != null) {
241            //
242            // If we're overriding an existing type, then we should pull
243            // its property set, copy it and override it. Note that we're
244            // not doing any type checking here, so it's possible to specify
245            // a type for override that is incompatible with the specified
246            // properties. If that's the case, then things might get
247            // really weird. We'll log an override with a specified type
248            // just in case.
249            ConfigurationData spd = rpdMap.get(override);
250            if (spd == null) {
251                spd = existingRPD.get(override);
252                if (spd == null) {
253                    throw new ConfigLoaderException("Override for undefined component: "
254                            + override + ", with name " + curComponent);
255                }
256            }
257            if (curType != null && !curType.equals(spd.getClassName())) {
258                logger.log(Level.FINE, String.format("Overriding component %s with component %s, new type is %s overridden type was %s",
259                        spd.getName(), curComponent, curType, spd.getClassName()));
260            }
261            if (curType == null) {
262                curType = spd.getClassName();
263            }
264            rpd = new ConfigurationData(curComponent, curType, spd.getProperties(), serializedForm, entriesName, exportable, importable, leaseTime);
265            overriding = true;
266        } else {
267            if (rpdMap.get(curComponent) != null) {
268                throw new ConfigLoaderException("duplicate definition for "
269                        + curComponent);
270            }
271            rpd = new ConfigurationData(curComponent, curType, serializedForm, entriesName, exportable, importable, leaseTime);
272        }
273
274        ObjectNode properties = (ObjectNode) node.get(ConfigLoader.PROPERTIES);
275        // properties is null if there are no properties specified in the json
276        if (properties != null) {
277            Iterator<Entry<String, JsonNode>> fieldsItr = properties.fields();
278            while (fieldsItr.hasNext()) {
279                Entry<String, JsonNode> e = fieldsItr.next();
280                String propName = e.getKey();
281                if (e.getValue() instanceof ArrayNode) {
282                    // Must be list
283                    ArrayList<SimpleProperty> listOutput = new ArrayList<>();
284                    ArrayList<Class<?>> classListOutput = new ArrayList<>();
285                    ArrayNode listNode = (ArrayNode) e.getValue();
286                    for (JsonNode element : listNode) {
287                        if (element.size() > 1) {
288                            throw new ConfigLoaderException("Too many elements in a propertylist item, found " + element);
289                        }
290                        Iterator<Entry<String, JsonNode>> listElementItr = element.fields();
291                        while (listElementItr.hasNext()) {
292                            Entry<String, JsonNode> elementEntry = listElementItr.next();
293                            String elementName = elementEntry.getKey();
294                            switch (elementName) {
295                                case ConfigLoader.ITEM:
296                                    listOutput.add(new SimpleProperty(elementEntry.getValue().textValue()));
297                                    break;
298                                case ConfigLoader.TYPE:
299                                    try {
300                                        classListOutput.add(Class.forName(elementEntry.getValue().textValue()));
301                                    } catch (ClassNotFoundException cnfe) {
302                                        throw new ConfigLoaderException("Unable to find class "
303                                                + elementEntry.getValue().textValue() + " in component " + curComponent + ", propertylist " + propName);
304                                    }
305                                    break;
306                                default:
307                                    throw new ConfigLoaderException("Unknown node in component " + curComponent + ", propertylist " + propName + ", node = " + e.getValue().toString());
308                            }
309                        }
310                    }
311                    ListProperty listProp;
312                    if (classListOutput.isEmpty()) {
313                        listProp = new ListProperty(listOutput);
314                    } else {
315                        listProp = new ListProperty(listOutput,classListOutput);
316                    }
317                    rpd.add(propName, listProp);
318                } else if (e.getValue() instanceof ObjectNode) {
319                    // Must be map
320                    Map<String, SimpleProperty> mapOutput = new HashMap<>();
321                    Iterator<Entry<String, JsonNode>> mapElementItr = e.getValue().fields();
322                    while (mapElementItr.hasNext()) {
323                        Entry<String, JsonNode> mapEntry = mapElementItr.next();
324                        if (mapEntry.getValue().isTextual()) {
325                            mapOutput.put(mapEntry.getKey(), new SimpleProperty(mapEntry.getValue().textValue()));
326                        } else {
327                            throw new ConfigLoaderException("Unknown node in component " + curComponent + ", propertymap " + propName + ", node = " + e.getValue().toString());
328                        }
329                    }
330                    rpd.add(propName, new MapProperty(mapOutput));
331                } else {
332                    // Generic property.
333                    rpd.add(propName, new SimpleProperty(e.getValue().textValue()));
334                }
335            }
336        }
337        rpdMap.put(rpd.getName(),rpd);
338    }
339
340    protected void parseFile(ObjectNode node) {
341        JsonNode name = node.get(ConfigLoader.NAME);
342        JsonNode value = node.get(ConfigLoader.VALUE);
343        if (name == null || value == null) {
344            throw new ConfigLoaderException("File element must have "
345                    + "'name' and 'value' attributes, found " + node.toString());
346        }
347        try {
348            String path = value.textValue();
349            URL newURL = ConfigurationManager.class.getResource(path);
350            if (newURL == null) {
351                File newFile = new File(path);
352                if (!newFile.isAbsolute()) {
353                    newFile = new File(workingDir,path);
354                }
355                newURL = newFile.toURI().toURL();
356            }
357            parent.addURL(newURL);
358        } catch (MalformedURLException ex) {
359            throw new ConfigLoaderException(ex, "Incorrectly formatted file element " + name.textValue() + " with value " + value.textValue());
360        }
361    }
362
363    protected void parseSerializedObject(ObjectNode node) {
364        JsonNode name = node.get(ConfigLoader.NAME);
365        JsonNode type = node.get(ConfigLoader.TYPE);
366        JsonNode location = node.get(ConfigLoader.LOCATION);
367        if ((name == null) || (type == null) || (location == null)) {
368            throw new ConfigLoaderException("Serialized element must have 'name', 'type' and 'location' elements, found " + node.toString());
369        }
370        serializedObjects.put(name.textValue(), new SerializedObject(name.textValue(), location.textValue(), type.textValue()));
371    }
372}