/**
Copyright (C) SYSTAP, LLC DBA Blazegraph 2006-2016. All rights reserved.
Contact:
SYSTAP, LLC DBA Blazegraph
2501 Calvert ST NW #106
Washington, DC 20008
licenses@blazegraph.com
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; version 2 of the License.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
/*
* Created on Feb 10, 2016
*/
package com.bigdata.service.geospatial;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.log4j.Logger;
import org.codehaus.jettison.json.JSONArray;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
import org.openrdf.model.URI;
import org.openrdf.model.impl.URIImpl;
import com.bigdata.rdf.internal.impl.extensions.InvalidGeoSpatialDatatypeConfigurationError;
import com.bigdata.service.geospatial.GeoSpatialDatatypeFieldConfiguration.ServiceMapping;
import com.bigdata.service.geospatial.GeoSpatialDatatypeFieldConfiguration.ValueType;
/**
* Configuration of a single geospatial datatype, including value type, multiplier,
* min possible value, and mapping to the service.
*
* @author <a href="mailto:ms@metaphacts.com">Michael Schmidt</a>
* @version $Id$
*/
public class GeoSpatialDatatypeConfiguration implements Serializable {
private static final long serialVersionUID = 1L;
final static private transient Logger log =
Logger.getLogger(GeoSpatialDatatypeConfiguration.class);
// URI of the datatype, e.g. <http://my.custom.geospatial.coordinate>
private final URI uri;
private final IGeoSpatialLiteralSerializer literalSerializer;
// ordered list of fields defining the datatype
private List<GeoSpatialDatatypeFieldConfiguration> fields;
// derived members
private boolean hasLat = false;
private boolean hasLon = false;
private boolean hasTime = false;
private boolean hasCoordSystem = false;
private Map<String,Integer> customFieldsIdxs = new HashMap<String,Integer>();
/**
* Constructor, setting up a {@link GeoSpatialDatatypeConfiguration} given a uri and a
* JSON array defining the fields as input. Throws an {@link InvalidGeoSpatialDatatypeConfigurationError}
* if the uri is null or empty or in case the JSON array does not describe a set of
* valid fields.
*/
public GeoSpatialDatatypeConfiguration(
final String uriStr, final String literalSerializerClass, final JSONArray fieldsJson) {
if (uriStr==null || uriStr.isEmpty())
throw new InvalidGeoSpatialDatatypeConfigurationError("URI parameter must not be null or empty");
try {
this.uri = new URIImpl(uriStr);
} catch (Exception e) {
throw new InvalidGeoSpatialDatatypeConfigurationError("Invalid URI in geospatial datatype config: " + uriStr);
}
if (literalSerializerClass==null || literalSerializerClass.isEmpty()) {
literalSerializer = new GeoSpatialDefaultLiteralSerializer();
} else {
try {
Class<?> literalSerializerClazz = Class.forName(literalSerializerClass);
try {
final Object instance = literalSerializerClazz.newInstance();
if (!(instance instanceof IGeoSpatialLiteralSerializer)) {
throw new InvalidGeoSpatialDatatypeConfigurationError("Literal serializer class "
+ literalSerializerClass +
" does not implement GeoSpatialLiteralSerializer interface.");
}
literalSerializer = (IGeoSpatialLiteralSerializer)instance;
} catch (Exception e) {
throw new InvalidGeoSpatialDatatypeConfigurationError(
"Literal serializer class " + literalSerializerClass +
" could not be instantiated: " + e.getMessage());
}
} catch (ClassNotFoundException e) {
throw new InvalidGeoSpatialDatatypeConfigurationError(
"Literal serializer class not found: " + literalSerializerClass);
}
}
fields = new ArrayList<GeoSpatialDatatypeFieldConfiguration>();
/**
* We expect a JSON array of the following format (example):
*
* [
* { "valueType": "DOUBLE", "multiplier": "100000", "serviceMapping": "LATITUDE" },
* { "valueType": "DOUBLE", "multiplier": "100000", "serviceMapping": "LONGITUDE" },
* { "valueType": "LONG, "multiplier": "1", "minValue" : "0" , "serviceMapping": "TIME" },
* { "valueType": "LONG", "multiplier": "1", "minValue" : "0" , "serviceMapping" : "COORD_SYSTEM" }
* ]
*/
for (int i=0; i<fieldsJson.length(); i++) {
try {
// { "valueType": "double", "multiplier": "100000", "serviceMapping": "latitude" }
JSONObject fieldJson = (JSONObject)fieldsJson.getJSONObject(i);
fields.add(new GeoSpatialDatatypeFieldConfiguration(fieldJson));
} catch (JSONException e) {
log.warn("Invalid JSON for field description: " + e.getMessage());
throw new InvalidGeoSpatialDatatypeConfigurationError(e.getMessage()); // forward exception
}
}
// validate that there is at least one field defined for the geospatial datatype
if (fields.isEmpty()) {
throw new InvalidGeoSpatialDatatypeConfigurationError(
"Geospatial datatype config for datatype " + uri + " must have at least one field, but has none.");
}
// validate that there are no duplicate service mappings used for the fields
final Set<ServiceMapping> serviceMappings = new HashSet<ServiceMapping>();
final Set<String> customServiceMappings = new HashSet<String>();
for (int i=0; i<fields.size(); i++) {
final GeoSpatialDatatypeFieldConfiguration field = fields.get(i);
final ServiceMapping curServiceMapping = field.getServiceMapping();
if (ServiceMapping.CUSTOM.equals(curServiceMapping)) {
final String customServiceMapping = field.getCustomServiceMapping();
if (customServiceMappings.contains(customServiceMapping)) {
throw new InvalidGeoSpatialDatatypeConfigurationError(
"Duplicate custom service mapping used for geospatial datatype config: " + customServiceMapping);
}
customServiceMappings.add(customServiceMapping);
} else if (serviceMappings.contains(curServiceMapping)) {
throw new InvalidGeoSpatialDatatypeConfigurationError(
"Duplicate service mapping used for geospatial datatype config: " + curServiceMapping);
}
serviceMappings.add(curServiceMapping);
}
initDerivedMembers();
}
/**
* Alternative constructor (to ease writing test cases)
*
* @param uri
* @param fields
*/
public GeoSpatialDatatypeConfiguration(
final String uriString, final IGeoSpatialLiteralSerializer literalSerializer,
final List<GeoSpatialDatatypeFieldConfiguration> fields) {
if (uriString==null || uriString.isEmpty()) {
throw new InvalidGeoSpatialDatatypeConfigurationError("URI string must not be null or empty.");
}
if (literalSerializer==null) {
throw new InvalidGeoSpatialDatatypeConfigurationError("Literal serializer must not be null.");
}
if (fields==null) {
throw new InvalidGeoSpatialDatatypeConfigurationError("Fields must not be null.");
}
this.uri = new URIImpl(uriString);
this.literalSerializer = literalSerializer;
this.fields = fields;
initDerivedMembers();
}
public URI getUri() {
return uri;
}
public IGeoSpatialLiteralSerializer getLiteralSerializer() {
return literalSerializer;
}
/**
* @return the list of fields defining the datatype (never null)
*/
public List<GeoSpatialDatatypeFieldConfiguration> getFields() {
return fields;
}
/**
* Computes the index of a field with a given (predefined) service mapping.
* Returns -1 if no such component present in the datatype or the mapping
* that is passed in is null.
*
* @param mapping
* @return
*/
public int idxOfField(final ServiceMapping mapping) {
if (mapping!=null) {
for (int i=0; i<getFields().size(); i++) {
if (mapping == fields.get(i).getServiceMapping()) {
return i;
}
}
}
return -1; // fallback
}
public int[] idxsOfCustomFields(final Set<String> customFields) {
if (customFields==null) {
return new int[0];
}
final int[] ret = new int[customFields.size()];
Arrays.fill(ret, -1); // pre-initialize
int ctr=0;
for (int i=0; i<getFields().size(); i++) {
final GeoSpatialDatatypeFieldConfiguration field = fields.get(i);
for (final String customField : customFields) {
if (ServiceMapping.CUSTOM == field.getServiceMapping() &&
customField!=null && customField.equals(field.getCustomServiceMapping())) {
ret[ctr++] = i;
}
}
}
return ret;
}
/**
* @return the number of dimensions (i.e., fields) of the configuration
*/
public int getNumDimensions() {
return fields.size();
}
public boolean hasLat() {
return hasLat;
}
public boolean hasLon() {
return hasLon;
}
public boolean hasTime() {
return hasTime;
}
public boolean hasCoordSystem() {
return hasCoordSystem;
}
public Map<String,Integer> getCustomFieldsIdxs() {
return customFieldsIdxs;
}
public boolean hasCustomFields() {
return !customFieldsIdxs.keySet().isEmpty();
}
public boolean hasCustomField(final String field) {
return customFieldsIdxs.keySet().contains(field);
}
public Integer getCustomFieldIdx(final String customField) {
return customFieldsIdxs.get(customField);
}
public ValueType getValueTypeOfCustomField(final String customField) {
final Integer idx = getCustomFieldIdx(customField);
return idx==null ? null : fields.get(idx).getValueType();
}
/**
* Initialized derived member variables, allowing efficient access to nested
* information (such as the field types).
*/
void initDerivedMembers() {
for (int i=0; i<fields.size(); i++) {
final GeoSpatialDatatypeFieldConfiguration field = fields.get(i);
switch (field.getServiceMapping())
{
case LATITUDE:
hasLat = true;
break;
case LONGITUDE:
hasLon = true;
break;
case COORD_SYSTEM:
hasCoordSystem = true;
break;
case TIME:
hasTime = true;
break;
case CUSTOM:
customFieldsIdxs.put(field.getCustomServiceMapping(), i);
break;
default:
throw new InvalidGeoSpatialDatatypeConfigurationError(
"Unhandled field type: " + field.getServiceMapping());
}
}
}
}