/*
* Constellation - An open source and standard compliant SDI
* http://www.constellation-sdi.org
*
* Copyright 2014 Geomatys.
*
* Licensed 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.constellation.json.metadata;
import java.util.Set;
import java.util.Map;
import java.util.List;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Collections;
import java.io.IOException;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import org.opengis.metadata.citation.Responsibility;
import org.opengis.metadata.citation.ResponsibleParty;
import org.opengis.metadata.constraint.Constraints;
import org.opengis.metadata.constraint.LegalConstraints;
import org.opengis.metadata.extent.GeographicBoundingBox;
import org.opengis.metadata.extent.GeographicExtent;
import org.opengis.metadata.identification.DataIdentification;
import org.opengis.metadata.identification.Identification;
import org.opengis.metadata.quality.ConformanceResult;
import org.opengis.metadata.quality.Result;
import org.opengis.metadata.spatial.SpatialRepresentation;
import org.opengis.metadata.spatial.VectorSpatialRepresentation;
import org.apache.sis.metadata.MetadataStandard;
import org.geotoolkit.sml.xml.v101.SensorMLStandard;
import org.constellation.json.metadata.binding.RootObj;
/**
* A JSON template in which to insert metadata values for given ISO 19115 paths.
* This class searches for object having a structure like below:
*
* <blockquote><pre>{
* "root":{
* "children":[{
* "superblock":{
* "path":null,
* "children":[{
* "block":{
* "path":null,
* "children":[{
* "field":{
* "path":"identificationInfo.citation.title",
* "defaultValue":null
* }
* },{
* "field":{
* // etc...
* }
* }
* ]}
* }
* ]}
* }
* }
*}</pre></blockquote>
*
* The only keywords handled by this class are {@code "children"}, {@code "path"}, {@code "defaultValue"}
* and {@code "value"}. All other entries will be copied verbatim. This allow templates to provide additional
* (key:value) pairs without the need to modify the {@code Template} code.
*
* <p><b>Multi-threading</b><br>
* This class is thread safe.</p>
*
* <p><b>Limitations</b></p>
* <ul>
* <li>The current implementation requires that comma is the last character on a line (ignoring whitespaces),
* or the character just before an opening {.</li>
* <li>The current implementation has only limited tolerance to the location where {, }, [ and ] characters can be placed
* (this is no a full JSON parser). We recommend to stay close to the above formatting.</li>
* </ul>
*
* @author Martin Desruisseaux (Geomatys)
*/
public class Template {
/**
* The default value to give to the {@code specialized} of {@link FormReader} constructor.
*/
private static final Map<Class<?>, Class<?>> DEFAULT_SPECIALIZED;
static {
final Map<Class<?>, Class<?>> specialized = new HashMap<>();
specialized.put(Responsibility.class, ResponsibleParty.class);
specialized.put(Identification.class, DataIdentification.class);
specialized.put(GeographicExtent.class, GeographicBoundingBox.class);
specialized.put(SpatialRepresentation.class, VectorSpatialRepresentation.class);
specialized.put(Constraints.class, LegalConstraints.class);
specialized.put(Result.class, ConformanceResult.class);
DEFAULT_SPECIALIZED = specialized;
}
/**
* Pre-defined instances. This map shall not be modified after class initialization,
* in order to allow concurrent access without synchronization.
*
* <p>By convention, name containing the {@code "sensor"} substring will be assumed to implement the
* {@link SensorMLStandard#INSTANCE} standard, and all other {@link MetadataStandard#ISO_19115}.</p>
*/
private static final Map<String,Template> INSTANCES;
static {
try {
INSTANCES = load(new byte[] {4, 6, 6, 6, 6, 10, 10},
"profile_import",
"profile_default_vector",
"profile_default_raster",
"profile_inspire_vector",
"profile_inspire_raster",
"profile_sensorml_component",
"profile_sensorml_system");
} catch (IOException e) {
throw new ExceptionInInitializerError(e); // Should never happen.
}
}
/**
* Creates a pre-defined template from resource files of the given names.
*
* @param depths The depth of each file enumerated in {@code names}.
* @param names The resource file names, without the {@code ".json"} suffix.
*/
private static Map<String,Template> load(final byte[] depths, final String... names) throws IOException {
final List<String> lines = new ArrayList<>();
final Map<String,String> sharedLines = new HashMap<>();
final Map<String,String[]> sharedPaths = new HashMap<>();
final Map<String,Template> templates = new LinkedHashMap<>(4);
for (int i=0; i<names.length; i++) {
final String name = names[i];
try (final BufferedReader in = new BufferedReader(new InputStreamReader(
Template.class.getResourceAsStream(name + ".json"), "UTF-8")))
{
String line;
while ((line = in.readLine()) != null) {
lines.add(line);
}
}
MetadataStandard standard = MetadataStandard.ISO_19115;
if (name.contains("sensorml_system")) {
standard = SensorMLStandard.SYSTEM;
} else if (name.contains("sensorml_component")) {
standard = SensorMLStandard.COMPONENT;
}
if (templates.put(name, new Template(standard, lines, sharedLines, sharedPaths, depths[i])) != null) {
throw new AssertionError(name);
}
lines.clear();
}
return templates;
}
/**
* The root node of the JSON tree to use as a template.
*/
final TemplateNode root;
/**
* The maximal length of {@link TemplateNode#path} arrays.
*/
final int depth;
/**
* The GeoAPI interfaces substitution.
* For example {@code Identification} is typically interpreted as {@code DataIdentification}.
*/
private final Map<Class<?>, Class<?>> specialized;
/**
* Creates a pre-defined template from a resource file of the given name.
*
* @param template The JSON lines to use as a template.
* @param sharedLines An initially empty map to be filled by {@link LineReader}
* for sharing same {@code String} instances when possible.
*/
private Template(final MetadataStandard standard, final Iterable<String> template, final Map<String,String> sharedLines,
final Map<String,String[]> sharedPaths, final int depth) throws IOException
{
root = new TemplateNode(new LineReader(standard, template, sharedLines, sharedPaths), true, null);
this.depth = depth;
specialized = DEFAULT_SPECIALIZED;
/*
* Do not validate the path (root.validatePath(null)). We will do that in JUnit tests instead,
* in order to avoid consuming CPU for a verification of a static resource.
*/
}
/**
* Creates a new template with the given lines.
* This constructor is for use of custom templates.
* Consider using one of the predefined templates returned by {@link #getInstance(String)} instead.
*
* @param standard The standard used by the metadata objects to write.
* @param template The JSON lines to use as a template.
* @param specialized The GeoAPI type substitution map, or {@code null} or the default map.
* For example {@code Identification} shall typically be interpreted as {@code DataIdentification}.
* @throws IOException if an error occurred while parsing the JSON template.
*
* @see #getInstance(String)
*/
public Template(final MetadataStandard standard, final Iterable<String> template,
final Map<Class<?>, Class<?>> specialized) throws IOException
{
root = new TemplateNode(new LineReader(standard, template, new HashMap<String,String>(),
new HashMap<String,String[]>()), true, null);
depth = root.validatePath(null);
this.specialized = (specialized != null) ? specialized : DEFAULT_SPECIALIZED;
}
/**
* Returns the names of available templates.
*
* @return Available templates.
*/
public static Set<String> getAvailableNames() {
return Collections.unmodifiableSet(INSTANCES.keySet());
}
/**
* Returns the template of the given name.
* Currently recognized names are:
*
* <ul>
* <li>profile_inspire_vector</li>
* <li>profile_inspire_raster</li>
* </ul>
*
* @param name Name of the template to get.
* @return The template of the given name.
* @throws IllegalArgumentException if the given name is not a known template name.
*/
public static Template getInstance(final String name) throws IllegalArgumentException {
final Template instance = INSTANCES.get(name);
if (instance == null) {
throw new IllegalArgumentException("Undefined template: " + name);
}
return instance;
}
/**
* Writes the values of the given metadata object using the template given at construction time.
*
* @param metadata The metadata object to write.
* @param out Where to write the JSO file.
* @param prune {@code true} for omitting empty nodes.
*
* @throws IOException if an error occurred while writing to {@code out}.
*/
public void write(final Object metadata, final Appendable out, final boolean prune) throws IOException {
root.write(metadata, out, prune, depth);
}
/**
* Parses the given JSON lines and write the metadata values in the given metadata object.
*
* <p>The {@code skipNulls} argument controls whether {@code null} values in the JSON file shall be skipped
* instead than stored in the metadata object. If {@code false}, null values in the JSON file will overwrite
* (erase) metadata properties that may have existed before the operation.
* This is sometime the desired effect when updating an existing {@code DefaultMetadata} instance.
* However when writing to an initially empty metadata object, a value of {@code true} will reduce
* the need to call {@link org.apache.sis.metadata.iso.DefaultMetadata#prune()} after parsing.</p>
*
* @param json Lines of the JSON file to parse.
* @param destination Where to store the metadata values.
* @param skipNulls {@code true} for skipping {@code null} values instead than storing null in the metadata object.
* @throws IOException if an error occurred while parsing.
*/
public void read(final Iterable<? extends CharSequence> json, final Object destination, final boolean skipNulls) throws IOException {
final FormReader r = new FormReader(new LineReader(root.standard, json, null, null), depth, skipNulls, specialized);
r.read((String[]) null);
r.writeToMetadata(root.standard, destination);
}
/**
* Parses the given JSON object and write the metadata values in the given metadata object.
* The {@code skipNulls} argument is used in the same way than in the
* {@link #read(Iterable, Object, boolean)} method.
*
* @param json Lines of the JSON file to parse.
* @param destination Where to store the metadata values.
* @param skipNulls {@code true} for skipping {@code null} values instead than storing null in the metadata object.
* @throws IOException if an error occurred while parsing.
*/
public void read(final RootObj json, final Object destination, final boolean skipNulls) throws IOException {
final FormReader r = new FormReader(null, depth, skipNulls, specialized);
r.read(json);
r.writeToMetadata(root.standard, destination);
}
/**
* @todo current implementation is unsafe (not thread-safe, no check for existing instances).
*/
public static void addTemplate(final String name, Template t) {
INSTANCES.put(name, t);
}
}