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}