| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.sling.feature.io.json; |
| |
| import org.apache.felix.configurator.impl.json.JSMin; |
| import org.apache.felix.configurator.impl.json.JSONUtil; |
| import org.apache.felix.configurator.impl.json.TypeConverter; |
| import org.apache.felix.configurator.impl.model.Config; |
| import org.apache.sling.feature.Artifact; |
| import org.apache.sling.feature.ArtifactId; |
| import org.apache.sling.feature.Bundles; |
| import org.apache.sling.feature.Configuration; |
| import org.apache.sling.feature.Configurations; |
| import org.apache.sling.feature.Extension; |
| import org.apache.sling.feature.ExtensionType; |
| import org.apache.sling.feature.Extensions; |
| import org.apache.sling.feature.KeyValueMap; |
| |
| import java.io.IOException; |
| import java.io.Reader; |
| import java.io.StringWriter; |
| import java.io.Writer; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.Enumeration; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| import java.util.Set; |
| |
| import javax.json.Json; |
| import javax.json.JsonArrayBuilder; |
| import javax.json.JsonObject; |
| import javax.json.JsonObjectBuilder; |
| import javax.json.JsonStructure; |
| import javax.json.JsonWriter; |
| |
| /** |
| * Common methods for JSON reading. |
| */ |
| abstract class JSONReaderBase { |
| |
| /** The optional location. */ |
| protected final String location; |
| |
| /** Exception prefix containing the location (if set) */ |
| protected final String exceptionPrefix; |
| |
| /** |
| * Private constructor |
| * @param location Optional location |
| */ |
| JSONReaderBase(final String location) { |
| this.location = location; |
| if ( location == null ) { |
| exceptionPrefix = ""; |
| } else { |
| exceptionPrefix = location + " : "; |
| } |
| } |
| |
| protected String minify(final Reader reader) throws IOException { |
| // minify JSON (remove comments) |
| final String contents; |
| try ( final Writer out = new StringWriter()) { |
| final JSMin min = new JSMin(reader, out); |
| min.jsmin(); |
| contents = out.toString(); |
| } |
| return contents; |
| } |
| |
| /** |
| * Get the JSON object as a map, removing all comments that start with a '#' character |
| * @param json The JSON object to process |
| * @return A map representing the JSON object. |
| */ |
| protected Map<String, Object> getJsonMap(JsonObject json) { |
| @SuppressWarnings("unchecked") |
| Map<String, Object> m = (Map<String, Object>) JSONUtil.getValue(json); |
| |
| removeComments(m); |
| return m; |
| } |
| |
| private void removeComments(Map<String, Object> m) { |
| for(Iterator<Map.Entry<String, Object>> it = m.entrySet().iterator(); it.hasNext(); ) { |
| Entry<String, ?> entry = it.next(); |
| if (entry.getKey().startsWith("#")) { |
| it.remove(); |
| } else if (entry.getValue() instanceof Map) { |
| @SuppressWarnings("unchecked") |
| Map<String, Object> embedded = (Map<String, Object>) entry.getValue(); |
| removeComments(embedded); |
| } else if (entry.getValue() instanceof Collection) { |
| Collection<?> embedded = (Collection<?>) entry.getValue(); |
| removeComments(embedded); |
| } |
| } |
| } |
| |
| @SuppressWarnings("unchecked") |
| private void removeComments(Collection<?> embedded) { |
| for (Object el : embedded) { |
| if (el instanceof Collection) { |
| removeComments((Collection<?>) el); |
| } else if (el instanceof Map) { |
| removeComments((Map<String, Object>) el); |
| } |
| } |
| } |
| |
| protected String getProperty(final Map<String, Object> map, final String key) throws IOException { |
| final Object val = map.get(key); |
| if ( val != null ) { |
| checkType(key, val, String.class); |
| return val.toString(); |
| } |
| return null; |
| } |
| |
| /** |
| * Read the variables section |
| * @param map The map describing the feature or application |
| * @param kvMap The variables will be written to this Key Value Map |
| * @return The same variables as a normal map |
| * @throws IOException If the json is invalid. |
| */ |
| protected Map<String, String> readVariables(Map<String, Object> map, KeyValueMap kvMap) throws IOException { |
| HashMap<String, String> variables = new HashMap<>(); |
| |
| if (map.containsKey(JSONConstants.FEATURE_VARIABLES)) { |
| final Object variablesObj = map.get(JSONConstants.FEATURE_VARIABLES); |
| checkType(JSONConstants.FEATURE_VARIABLES, variablesObj, Map.class); |
| |
| @SuppressWarnings("unchecked") |
| final Map<String, Object> vars = (Map<String, Object>) variablesObj; |
| for (final Map.Entry<String, Object> entry : vars.entrySet()) { |
| Object val = entry.getValue(); |
| checkType("variable value", val, String.class, Boolean.class, Number.class, null); |
| |
| String key = entry.getKey(); |
| if (kvMap.get(key) != null) { |
| throw new IOException(this.exceptionPrefix + "Duplicate variable " + key); |
| } |
| |
| String value = val == null ? null : val.toString(); |
| |
| kvMap.put(key, value); |
| variables.put(key, value); |
| } |
| } |
| return variables; |
| } |
| |
| |
| /** |
| * Read the bundles / start levels section |
| * @param map The map describing the feature |
| * @param container The bundles container |
| * @param configContainer The configurations container |
| * @throws IOException If the json is invalid. |
| */ |
| protected void readBundles( |
| final Map<String, Object> map, |
| final Bundles container, |
| final Configurations configContainer) throws IOException { |
| if ( map.containsKey(JSONConstants.FEATURE_BUNDLES)) { |
| final Object bundlesObj = map.get(JSONConstants.FEATURE_BUNDLES); |
| checkType(JSONConstants.FEATURE_BUNDLES, bundlesObj, List.class); |
| |
| final List<Artifact> list = new ArrayList<>(); |
| readArtifacts(JSONConstants.FEATURE_BUNDLES, "bundle", list, bundlesObj, configContainer); |
| |
| for(final Artifact a : list) { |
| Artifact sameFound = container.getSame(a.getId()); |
| if ( sameFound != null) { |
| throw new IOException(exceptionPrefix + "Duplicate bundle " + a.getId().toMvnId()); |
| } |
| try { |
| // check start order |
| a.getStartOrder(); |
| } catch ( final IllegalArgumentException nfe) { |
| throw new IOException(exceptionPrefix + "Illegal start order '" + a.getMetadata().get(Artifact.KEY_START_ORDER) + "'"); |
| } |
| container.add(a); |
| } |
| } |
| } |
| |
| protected void readArtifacts(final String section, |
| final String artifactType, |
| final List<Artifact> artifacts, |
| final Object listObj, |
| final Configurations container) |
| throws IOException { |
| checkType(section, listObj, List.class); |
| @SuppressWarnings("unchecked") |
| final List<Object> list = (List<Object>) listObj; |
| for(final Object entry : list) { |
| final Artifact artifact; |
| checkType(artifactType, entry, Map.class, String.class); |
| if ( entry instanceof String ) { |
| // skip comments |
| if ( entry.toString().startsWith("#") ) { |
| continue; |
| } |
| artifact = new Artifact(ArtifactId.parse((String) entry)); |
| } else { |
| @SuppressWarnings("unchecked") |
| final Map<String, Object> bundleObj = (Map<String, Object>) entry; |
| if ( !bundleObj.containsKey(JSONConstants.ARTIFACT_ID) ) { |
| throw new IOException(exceptionPrefix + " " + artifactType + " is missing required artifact id"); |
| } |
| checkType(artifactType + " " + JSONConstants.ARTIFACT_ID, bundleObj.get(JSONConstants.ARTIFACT_ID), String.class); |
| final ArtifactId id = ArtifactId.parse(bundleObj.get(JSONConstants.ARTIFACT_ID).toString()); |
| |
| artifact = new Artifact(id); |
| for(final Map.Entry<String, Object> metadataEntry : bundleObj.entrySet()) { |
| final String key = metadataEntry.getKey(); |
| if ( JSONConstants.ARTIFACT_KNOWN_PROPERTIES.contains(key) ) { |
| continue; |
| } |
| checkType(artifactType + " metadata " + key, metadataEntry.getValue(), String.class, Number.class, Boolean.class); |
| artifact.getMetadata().put(key, metadataEntry.getValue().toString()); |
| } |
| if ( bundleObj.containsKey(JSONConstants.FEATURE_CONFIGURATIONS) ) { |
| checkType(artifactType + " configurations", bundleObj.get(JSONConstants.FEATURE_CONFIGURATIONS), Map.class); |
| addConfigurations(bundleObj, artifact, container); |
| } |
| } |
| artifacts.add(artifact); |
| } |
| } |
| |
| protected void addConfigurations(final Map<String, Object> map, |
| final Artifact artifact, |
| final Configurations container) throws IOException { |
| final JSONUtil.Report report = new JSONUtil.Report(); |
| @SuppressWarnings("unchecked") |
| final List<Config> configs = JSONUtil.readConfigurationsJSON(new TypeConverter(null), |
| 0, "", (Map<String, ?>)map.get(JSONConstants.FEATURE_CONFIGURATIONS), report); |
| if ( !report.errors.isEmpty() || !report.warnings.isEmpty() ) { |
| final StringBuilder builder = new StringBuilder(exceptionPrefix); |
| builder.append("Errors in configurations:"); |
| for(final String w : report.warnings) { |
| builder.append("\n"); |
| builder.append(w); |
| } |
| for(final String e : report.errors) { |
| builder.append("\n"); |
| builder.append(e); |
| } |
| throw new IOException(builder.toString()); |
| } |
| |
| for(final Config c : configs) { |
| final int pos = c.getPid().indexOf('~'); |
| final Configuration config; |
| if ( pos != -1 ) { |
| config = new Configuration(c.getPid().substring(0, pos), c.getPid().substring(pos + 1)); |
| } else { |
| config = new Configuration(c.getPid()); |
| } |
| final Enumeration<String> keyEnum = c.getProperties().keys(); |
| while ( keyEnum.hasMoreElements() ) { |
| final String key = keyEnum.nextElement(); |
| if ( key.startsWith(":configurator:") ) { |
| throw new IOException(exceptionPrefix + "Configuration must not define configurator property " + key); |
| } |
| final Object val = c.getProperties().get(key); |
| config.getProperties().put(key, val); |
| } |
| if ( config.getProperties().get(Configuration.PROP_ARTIFACT_ID) != null ) { |
| throw new IOException(exceptionPrefix + "Configuration must not define property " + Configuration.PROP_ARTIFACT_ID); |
| } |
| if ( artifact != null ) { |
| config.getProperties().put(Configuration.PROP_ARTIFACT_ID, artifact.getId().toMvnId()); |
| } |
| for(final Configuration current : container) { |
| if ( current.equals(config) ) { |
| throw new IOException(exceptionPrefix + "Duplicate configuration " + config); |
| } |
| } |
| container.add(config); |
| } |
| } |
| |
| |
| protected void readConfigurations(final Map<String, Object> map, |
| final Configurations container) throws IOException { |
| if ( map.containsKey(JSONConstants.FEATURE_CONFIGURATIONS) ) { |
| checkType(JSONConstants.FEATURE_CONFIGURATIONS, map.get(JSONConstants.FEATURE_CONFIGURATIONS), Map.class); |
| addConfigurations(map, null, container); |
| } |
| } |
| |
| protected void readFrameworkProperties(final Map<String, Object> map, |
| final KeyValueMap container) throws IOException { |
| if ( map.containsKey(JSONConstants.FEATURE_FRAMEWORK_PROPERTIES) ) { |
| final Object propsObj= map.get(JSONConstants.FEATURE_FRAMEWORK_PROPERTIES); |
| checkType(JSONConstants.FEATURE_FRAMEWORK_PROPERTIES, propsObj, Map.class); |
| |
| @SuppressWarnings("unchecked") |
| final Map<String, Object> props = (Map<String, Object>) propsObj; |
| for(final Map.Entry<String, Object> entry : props.entrySet()) { |
| checkType("framework property value", entry.getValue(), String.class, Boolean.class, Number.class); |
| if ( container.get(entry.getKey()) != null ) { |
| throw new IOException(this.exceptionPrefix + "Duplicate framework property " + entry.getKey()); |
| } |
| container.put(entry.getKey(), entry.getValue().toString()); |
| } |
| |
| } |
| } |
| |
| protected void readExtensions(final Map<String, Object> map, |
| final List<String> keywords, |
| final Extensions container, |
| final Configurations configContainer) throws IOException { |
| final Set<String> keySet = new HashSet<>(map.keySet()); |
| keySet.removeAll(keywords); |
| // the remaining keys are considered extensions! |
| for(final String key : keySet) { |
| final int pos = key.indexOf(':'); |
| final String postfix = pos == -1 ? null : key.substring(pos + 1); |
| final int sep = (postfix == null ? key.indexOf('|') : postfix.indexOf('|')); |
| final String name; |
| final String type; |
| final String optional; |
| if ( pos == -1 ) { |
| type = ExtensionType.ARTIFACTS.name(); |
| if ( sep == -1 ) { |
| name = key; |
| optional = Boolean.FALSE.toString(); |
| } else { |
| name = key.substring(0, sep); |
| optional = key.substring(sep + 1); |
| } |
| } else { |
| name = key.substring(0, pos); |
| if ( sep == -1 ) { |
| type = postfix; |
| optional = Boolean.FALSE.toString(); |
| } else { |
| type = postfix.substring(0, sep); |
| optional = postfix.substring(sep + 1); |
| } |
| } |
| if ( JSONConstants.FEATURE_KNOWN_PROPERTIES.contains(name) ) { |
| throw new IOException(this.exceptionPrefix + "Extension is using reserved name : " + name); |
| } |
| if ( container.getByName(name) != null ) { |
| throw new IOException(exceptionPrefix + "Duplicate extension with name " + name); |
| } |
| |
| final ExtensionType extType = ExtensionType.valueOf(type); |
| final boolean opt = Boolean.valueOf(optional).booleanValue(); |
| |
| final Extension ext = new Extension(extType, name, opt); |
| final Object value = map.get(key); |
| switch ( extType ) { |
| case ARTIFACTS : final List<Artifact> list = new ArrayList<>(); |
| readArtifacts("Extension " + name, "artifact", list, value, configContainer); |
| for(final Artifact a : list) { |
| if ( ext.getArtifacts().contains(a) ) { |
| throw new IOException(exceptionPrefix + "Duplicate artifact in extension " + name + " : " + a.getId().toMvnId()); |
| } |
| ext.getArtifacts().add(a); |
| } |
| break; |
| case JSON : checkType("JSON Extension " + name, value, Map.class, List.class); |
| final JsonStructure struct = build(value); |
| try ( final StringWriter w = new StringWriter()) { |
| final JsonWriter jw = Json.createWriter(w); |
| jw.write(struct); |
| w.flush(); |
| ext.setJSON(w.toString()); |
| } |
| break; |
| case TEXT : checkType("Text Extension " + name, value, String.class, List.class); |
| if ( value instanceof String ) { |
| // string |
| ext.setText(value.toString()); |
| } else { |
| // list (array of strings) |
| @SuppressWarnings("unchecked") |
| final List<Object> l = (List<Object>)value; |
| final StringBuilder sb = new StringBuilder(); |
| for(final Object o : l) { |
| checkType("Text Extension " + name + ", value " + o, o, String.class); |
| sb.append(o.toString()); |
| sb.append('\n'); |
| } |
| ext.setText(sb.toString()); |
| } |
| break; |
| } |
| |
| container.add(ext); |
| } |
| } |
| |
| private JsonStructure build(final Object value) { |
| if ( value instanceof List ) { |
| @SuppressWarnings("unchecked") |
| final List<Object> list = (List<Object>)value; |
| final JsonArrayBuilder builder = Json.createArrayBuilder(); |
| for(final Object obj : list) { |
| if ( obj instanceof String ) { |
| builder.add(obj.toString()); |
| } else if ( obj instanceof Long ) { |
| builder.add((Long)obj); |
| } else if ( obj instanceof Double ) { |
| builder.add((Double)obj); |
| } else if (obj instanceof Boolean ) { |
| builder.add((Boolean)obj); |
| } else if ( obj instanceof Map ) { |
| builder.add(build(obj)); |
| } else if ( obj instanceof List ) { |
| builder.add(build(obj)); |
| } |
| |
| } |
| return builder.build(); |
| } else if ( value instanceof Map ) { |
| @SuppressWarnings("unchecked") |
| final Map<String, Object> map = (Map<String, Object>)value; |
| final JsonObjectBuilder builder = Json.createObjectBuilder(); |
| for(final Map.Entry<String, Object> entry : map.entrySet()) { |
| if ( entry.getValue() instanceof String ) { |
| builder.add(entry.getKey(), entry.getValue().toString()); |
| } else if ( entry.getValue() instanceof Long ) { |
| builder.add(entry.getKey(), (Long)entry.getValue()); |
| } else if ( entry.getValue() instanceof Double ) { |
| builder.add(entry.getKey(), (Double)entry.getValue()); |
| } else if ( entry.getValue() instanceof Boolean ) { |
| builder.add(entry.getKey(), (Boolean)entry.getValue()); |
| } else if ( entry.getValue() instanceof Map ) { |
| builder.add(entry.getKey(), build(entry.getValue())); |
| } else if ( entry.getValue() instanceof List ) { |
| builder.add(entry.getKey(), build(entry.getValue())); |
| } |
| } |
| return builder.build(); |
| } |
| return null; |
| } |
| |
| /** |
| * Check if the value is one of the provided types |
| * @param key A key for the error message |
| * @param val The value to check |
| * @param types The allowed types, can also include {@code null} if null is an allowed value. |
| * @throws IOException If the val is not of the specified types |
| */ |
| protected void checkType(final String key, final Object val, Class<?>...types) throws IOException { |
| boolean valid = false; |
| for(final Class<?> c : types) { |
| if ( c == null && val == null) { |
| valid = true; |
| break; |
| } |
| if ( c.isInstance(val) ) { |
| valid = true; |
| break; |
| } |
| } |
| if ( !valid ) { |
| throw new IOException(this.exceptionPrefix + "Key " + key + " is not one of the allowed types " + Arrays.toString(types) + " : " + val.getClass()); |
| } |
| } |
| } |