/*
* Copyright (c) 2016, Metron, Inc.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of Metron, Inc. nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL METRON, INC. BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.metsci.glimpse.dspl.util;
import static com.metsci.glimpse.dspl.lite.DsplLiteHelper.loadNonCanonicalDataSet_csv;
import static com.metsci.glimpse.dspl.lite.DsplLiteHelper.loadNonCanonicalDataSet_xml_lite;
import static com.metsci.glimpse.util.logging.LoggerUtils.logFine;
import static com.metsci.glimpse.util.logging.LoggerUtils.logWarning;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;
import java.util.logging.Logger;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import javax.xml.namespace.QName;
import com.metsci.glimpse.dspl.DsplParser;
import com.metsci.glimpse.dspl.parser.TableParser;
import com.metsci.glimpse.dspl.parser.table.PropertyTableData;
import com.metsci.glimpse.dspl.parser.table.SliceTableData;
import com.metsci.glimpse.dspl.parser.util.DsplCacheHelper;
import com.metsci.glimpse.dspl.schema.Attribute;
import com.metsci.glimpse.dspl.schema.Concept;
import com.metsci.glimpse.dspl.schema.ConceptProperty;
import com.metsci.glimpse.dspl.schema.Data;
import com.metsci.glimpse.dspl.schema.Data.File;
import com.metsci.glimpse.dspl.schema.DataSet;
import com.metsci.glimpse.dspl.schema.DataSet.Import;
import com.metsci.glimpse.dspl.schema.DataType;
import com.metsci.glimpse.dspl.schema.Slice;
import com.metsci.glimpse.dspl.schema.SliceConceptRef;
import com.metsci.glimpse.dspl.schema.Table;
import com.metsci.glimpse.dspl.schema.Value;
import com.metsci.glimpse.util.io.StreamOpener;
public class DsplHelper
{
public static final Logger logger = Logger.getLogger( DsplHelper.class.getName( ) );
public static final String defaultDsplNamespace = "http://schemas.google.com/dspl/2010";
public static final String canonicalGoogleNamespace = "http://www.google.com/publicdata/dataset/google/";
public static final String canonicalGoogleUrlBase = "http://dspl.googlecode.com/hg/datasets/google/canonical/";
public static final String canonicalGoogleLocalBase = "dspl/canonical/google/";
public static final String canonicalMetronNamespace = "http://www.metsci.com/dspl/";
public static final String canonicalMetronLocalBase = "dspl/canonical/metron/";
public static final String dsplSchema = "com.metsci.glimpse.dspl.schema";
public static final String objectFactoryProp = "com.sun.xml.bind.ObjectFactory";
private static JAXBContext jc;
static
{
try
{
jc = JAXBContext.newInstance( dsplSchema );
}
catch ( JAXBException e )
{
logWarning( logger, "Unable to initialize JAXB Context", e );
}
}
///////////////////////////////////////////////////////////////////////
////// DataSet utility methods //////
///////////////////////////////////////////////////////////////////////
public static DataSet loadDataset( DsplParser parser, DataSet parent, String namespace, String location ) throws JAXBException, IOException, DsplException
{
if ( namespace != null && namespace.startsWith( canonicalGoogleNamespace ) )
{
return loadCanonicalDataset( parser, namespace, canonicalGoogleLocalBase, canonicalGoogleUrlBase );
}
else if ( namespace != null && namespace.startsWith( canonicalMetronNamespace ) )
{
return loadCanonicalDataset( parser, namespace, canonicalMetronLocalBase, null );
}
else
{
return loadNonCanonicalDataSet( parser, parent, namespace, location );
}
}
public static DataSet loadNonCanonicalDataSet( DsplParser parser, DataSet parent, String namespace, String location ) throws JAXBException, IOException, DsplException
{
if ( location.endsWith( ".xml" ) )
{
try
{
return loadNonCanonicalDataSet_xml( parser, parent, namespace, location );
}
catch ( JAXBException e )
{
// if we fail to interpret the xml document using the dspl schema, try dspl-lite
return loadNonCanonicalDataSet_xml_lite( parser, location );
}
}
else if ( location.endsWith( ".zip" ) )
{
return loadNonCanonicalDataSet_zip( parser, parent, namespace, location );
}
else if ( location.endsWith( ".csv" ) )
{
return loadNonCanonicalDataSet_csv( parser, location );
}
else
{
throw new DsplException( "DataSet location must point to either xml or zip file. Provided location is invalid: %s", location );
}
}
protected static DataSet loadNonCanonicalDataSet_xml( DsplParser parser, DataSet parent, String namespace, String location ) throws JAXBException, IOException, DsplException
{
// try loading the file using the name as is (as a file or classpath resource)
try
{
InputStream stream = StreamOpener.fileThenResource.openForRead( location );
return loadDataset( parser, stream, new java.io.File( location ) );
}
catch ( IOException e )
{
logFine( logger, "Unable to load relative path: %s. Trying absolute path.", e, location );
}
if ( parent != null && parent.getFile( ) != null )
{
// try loading the file by appending the parent DataSet url
java.io.File dataSetFile = parent.getFile( );
java.io.File dataSetParent = dataSetFile.getParentFile( );
String dataSetPath = new java.io.File( dataSetParent, location ).getPath( );
InputStream stream = StreamOpener.fileThenResource.openForRead( dataSetPath );
return loadDataset( parser, stream, new java.io.File( dataSetPath ) );
}
else
{
throw new DsplException( "Unable to load data set namespace %s location %s because absolute path was not provided and no parent namespace exists.", namespace, location );
}
}
protected static DataSet loadNonCanonicalDataSet_zip( DsplParser parser, DataSet parent, String namespace, String location ) throws JAXBException, IOException, DsplException
{
try
{
java.io.File file = new java.io.File( location );
URL metadata = new URL( String.format( "jar:file:%s!/metadata.xml", file.getAbsolutePath( ) ) );
InputStream stream = metadata.openStream( );
return loadDataset( parser, stream, new java.io.File( location ) );
}
catch ( IOException e )
{
logFine( logger, "Unable to load relative path: %s. Trying absolute path.", e, location );
}
URL resource = parser.getClass( ).getClassLoader( ).getResource( location );
if ( resource != null && resource.getProtocol( ).equals( "file" ) )
{
InputStream stream = getInputStreamFromZipFile( resource.getFile( ), "metadata.xml" );
return loadDataset( parser, stream, new java.io.File( resource.getFile( ) ) );
}
else if ( parent != null && parent.getFile( ) != null )
{
// try loading the file by appending the parent DataSet url
java.io.File dataSetFile = parent.getFile( );
java.io.File dataSetParent = dataSetFile.getParentFile( );
String dataSetPath = new java.io.File( dataSetParent, location ).getPath( );
ZipFile zipFile = new ZipFile( dataSetPath );
ZipEntry metadataEntry = zipFile.getEntry( "metadata.xml" );
InputStream stream = zipFile.getInputStream( metadataEntry );
return loadDataset( parser, stream, new java.io.File( dataSetPath ) );
}
else
{
throw new DsplException( "Unable to load data set namespace %s location %s because absolute path was not provided and no parent namespace exists.", namespace, location );
}
}
protected static InputStream getInputStreamFromZipFile( String zipFilePath, String resourcePath ) throws IOException
{
URL metadata = new URL( String.format( "jar:file:%s!/%s", zipFilePath, resourcePath ) );
return metadata.openStream( );
}
public static DataSet loadLocalCanonicalDataset( DsplParser parser, String namespace, String localBase ) throws JAXBException, IOException, DsplException
{
InputStream stream = StreamOpener.fileThenResource.openForRead( localBase + getName( namespace ) + ".xml" );
DataSet canonicalDataset = loadDataset( parser, stream, null );
return canonicalDataset;
}
public static DataSet loadNetworkCanonicalDataset( DsplParser parser, String namespace, String urlBase ) throws JAXBException, IOException, DsplException
{
URL canonicalUrl = new URL( urlBase + getName( namespace ) + ".xml" );
DataSet canonicalDataset = loadDataset( parser, canonicalUrl.openStream( ), null );
return canonicalDataset;
}
public static DataSet loadCanonicalDataset( DsplParser parser, String namespace, String localBase, String urlBase ) throws JAXBException, IOException, DsplException
{
if ( parser.isNetworkMode( ) )
{
try
{
return loadLocalCanonicalDataset( parser, namespace, localBase );
}
catch ( IOException e )
{
return loadNetworkCanonicalDataset( parser, namespace, urlBase );
}
}
else
{
return loadLocalCanonicalDataset( parser, namespace, localBase );
}
}
public static DataSet loadDataset( DsplParser parser, InputStream stream, java.io.File base ) throws JAXBException, IOException, DsplException
{
try
{
Unmarshaller unmarshaller = jc.createUnmarshaller( );
DataSet dataset = ( DataSet ) unmarshaller.unmarshal( stream );
return linkDataset( parser, dataset, base );
}
finally
{
stream.close( );
}
}
public static DataSet linkDataset( DsplParser parser, DataSet dataset ) throws JAXBException, IOException, DsplException
{
return linkDataset( parser, dataset, null );
}
public static DataSet linkDataset( DsplParser parser, DataSet dataset, java.io.File base ) throws JAXBException, IOException, DsplException
{
dataset.setFile( base );
dataset.setParser( parser );
loadImportedDataSets( parser, dataset );
resolveConceptRefs( dataset );
resolveDataSetLinks( dataset );
resolveConceptIds( dataset );
resolveConceptTypes( dataset );
checkConceptReferences( dataset );
resolveConceptExtension( dataset );
parser.cacheDataset( dataset );
return dataset;
}
// make sure that we've loaded all datasets imported by this data set
public static void loadImportedDataSets( DsplParser parser, DataSet dataset ) throws JAXBException, IOException, DsplException
{
for ( Import imp : dataset.getImport( ) )
{
dataset.getDataSet( imp );
}
}
///////////////////////////////////////////////////////////////////////
////// Table utility methods //////
///////////////////////////////////////////////////////////////////////
public static SliceTableData getTableData( Slice slice ) throws IOException, JAXBException, DsplException
{
if ( slice == null ) return null;
DataSet dataset = slice.getDataSet( );
if ( dataset == null ) return null;
Table table = slice.getTable( );
if ( table == null ) return null;
DsplParser dsplParser = dataset.getParser( );
if ( dsplParser == null ) return null;
TableParser parser = dsplParser.getTableParser( table );
if ( parser == null ) return null;
if ( dsplParser.isCacheMode( ) )
{
return DsplCacheHelper.getTableData( slice );
}
else
{
return parser.parse( slice );
}
}
public static PropertyTableData getTableData( Concept concept ) throws IOException, JAXBException, DsplException
{
if ( concept == null ) return null;
DataSet dataset = concept.getDataSet( );
if ( dataset == null ) return null;
Table table = concept.getTable( );
if ( table == null ) return null;
DsplParser dsplParser = dataset.getParser( );
if ( dsplParser == null ) return null;
TableParser parser = dsplParser.getTableParser( table );
if ( parser == null ) return null;
if ( dsplParser.isCacheMode( ) )
{
return DsplCacheHelper.getTableData( concept );
}
else
{
return parser.parse( concept );
}
}
public static InputStream getTableInputStream( Concept concept ) throws IOException, JAXBException, DsplException
{
return getTableInputStream( concept.getTable( ) );
}
public static InputStream getTableInputStream( Slice slice ) throws IOException, JAXBException, DsplException
{
return getTableInputStream( slice.getTable( ) );
}
public static Table getTable( Slice slice ) throws javax.xml.bind.JAXBException, java.io.IOException, DsplException
{
if ( slice == null || slice.getDataSet( ) == null || slice.getTableMapping( ) == null ) return null;
return getTable( slice.getDataSet( ), slice.getTableMapping( ).getRef( ) );
}
public static Table getTable( Concept concept ) throws javax.xml.bind.JAXBException, java.io.IOException, DsplException
{
if ( concept == null || concept.getDataSet( ) == null || concept.getTableMapping( ) == null ) return null;
return getTable( concept.getDataSet( ), null, concept.getTableMapping( ).getRef( ) );
}
public static Table getTable( DataSet dataset, QName ref ) throws javax.xml.bind.JAXBException, java.io.IOException, DsplException
{
return getTable( dataset, ref.getNamespaceURI( ), ref.getLocalPart( ) );
}
public static Table getTable( DataSet dataset, String namespace, String local ) throws javax.xml.bind.JAXBException, java.io.IOException, DsplException
{
if ( local == null ) return null;
if ( namespace == null || namespace.equals( dataset.getTargetNamespace( ) ) || namespace.equals( com.metsci.glimpse.dspl.util.DsplHelper.defaultDsplNamespace ) )
{
if ( dataset.getTables( ) != null )
{
for ( Table table : dataset.getTables( ).getTable( ) )
{
if ( local.equals( table.getId( ) ) )
{
return table;
}
}
}
}
else if ( namespace != null )
{
for ( Import imp : dataset.getImport( ) )
{
if ( namespace.equals( imp.getNamespace( ) ) )
{
DataSet importedDataset = dataset.getDataSet( imp );
Table importedTable = getTable( importedDataset, namespace, local );
if ( importedTable != null ) return importedTable;
}
}
}
return null;
}
public static InputStream getTableInputStream( Table table ) throws IOException
{
if ( table == null ) return null;
DataSet dataset = table.getDataSet( );
DsplParser parser = dataset.getParser( );
Data data = table.getData( );
if ( data == null ) return null;
File file = data.getFile( );
String name = file.getValue( );
String namespace = dataset.getTargetNamespace( );
if ( namespace != null && namespace.startsWith( canonicalGoogleNamespace ) )
{
return getCanonicalTableInputStream( parser, name, canonicalGoogleLocalBase, canonicalGoogleUrlBase );
}
else if ( namespace != null && namespace.startsWith( canonicalMetronNamespace ) )
{
return getCanonicalTableInputStream( parser, name, canonicalMetronLocalBase, null );
}
else
{
return getLocalTableInputStream( dataset, name );
}
}
public static InputStream getCanonicalTableInputStream( DsplParser parser, String file, String localBase, String urlBase ) throws IOException
{
if ( parser.isNetworkMode( ) )
{
try
{
return getCanonicalLocalTableInputStream( file, localBase );
}
catch ( IOException e )
{
return getCanonicalNetworkTableInputStream( file, urlBase );
}
}
else
{
return getCanonicalLocalTableInputStream( file, localBase );
}
}
public static InputStream getCanonicalLocalTableInputStream( String file, String localBase ) throws IOException
{
String canonicalPathLocal = localBase + file;
return StreamOpener.fileThenResource.openForRead( canonicalPathLocal );
}
public static InputStream getCanonicalNetworkTableInputStream( String file, String urlBase ) throws IOException
{
String canonicalFile = urlBase + file;
URL canonicalUrl = new URL( canonicalFile );
return canonicalUrl.openStream( );
}
public static InputStream getLocalTableInputStream( DataSet dataSet, String name )
{
// try loading the file using the name as is (as a file or classpath resource)
try
{
return StreamOpener.fileThenResource.openForRead( name );
}
catch ( Exception e )
{
logFine( logger, "Unable to load relative path: %s", name );
}
// try loading the file by appending the parent DataSet url
String tableFile = null;
try
{
java.io.File dataSetFile = dataSet.getFile( );
if ( dataSetFile != null )
{
if ( dataSetFile.getName( ).endsWith( ".zip" ) )
{
return getInputStreamFromZipFile( dataSetFile.getAbsolutePath( ), name );
}
else
{
java.io.File dataSetParent = dataSetFile.getParentFile( );
tableFile = new java.io.File( dataSetParent, name ).getPath( );
return StreamOpener.fileThenResource.openForRead( tableFile );
}
}
}
catch ( Exception e )
{
logFine( logger, "Unable to load absolute path to file %s", e, name );
}
return null;
}
///////////////////////////////////////////////////////////////////////
////// Concept utility methods //////
///////////////////////////////////////////////////////////////////////
/**
* Searches the provided dataset (and recursively searches that dataset's
* imported datasets) for the Concept with id matching the provided id.
*
* @param dataset the dataset to search for the concept in
* @param id the concept to search for
* @return the concept referenced by id
* @throws JAXBException
* @throws IOException
*/
public static Concept getConcept( DataSet dataset, String namespace, String local ) throws javax.xml.bind.JAXBException, java.io.IOException, com.metsci.glimpse.dspl.util.DsplException
{
if ( local == null ) return null;
if ( namespace == null || namespace.equals( dataset.getTargetNamespace( ) ) || namespace.equals( com.metsci.glimpse.dspl.util.DsplHelper.defaultDsplNamespace ) )
{
if ( dataset.getConcepts( ) != null )
{
for ( Concept concept : dataset.getConcepts( ).getConcept( ) )
{
if ( local.equals( concept.getId( ) ) )
{
return concept;
}
}
}
}
else
{
for ( Import imp : dataset.getImport( ) )
{
if ( namespace == null || namespace.equals( imp.getNamespace( ) ) )
{
DataSet importedDataset = dataset.getDataSet( imp );
Concept importedConcept = getConcept( importedDataset, namespace, local );
if ( importedConcept != null ) return importedConcept;
}
}
}
return null;
}
public static void checkConceptReferences( DataSet dataset ) throws JAXBException, IOException, DsplException
{
if ( dataset == null ) return;
if ( dataset.getConcepts( ) != null )
{
for ( Concept concept : dataset.getConcepts( ).getConcept( ) )
{
for ( Attribute attribute : concept.getAttribute( ) )
{
if ( attribute.getConceptRef( ) != null && attribute.getConcept( ) == null )
{
throw new DsplException( "Could not resolve Attribute %s Concept %s in Concept %s.", attribute.getId( ), attribute.getConcept( ), concept.getId( ) );
}
}
for ( ConceptProperty property : concept.getProperty( ) )
{
if ( property.getConceptRef( ) != null && property.getConcept( ) == null )
{
throw new DsplException( "Could not resolve Property %s Concept %s in Concept %s.", property.getId( ), property.getConcept( ), concept.getId( ) );
}
}
if ( concept.getExtends( ) != null && dataset.getConcept( concept.getExtends( ) ) == null )
{
throw new DsplException( "Could not resolve super-Concept %s of Concept %s.", concept.getExtends( ), concept.getId( ) );
}
}
}
}
public static void resolveConceptIds( DataSet dataset ) throws DsplException
{
if ( dataset == null ) return;
if ( dataset.getConcepts( ) != null )
{
for ( Concept concept : dataset.getConcepts( ).getConcept( ) )
{
for ( Attribute attribute : concept.getAttribute( ) )
{
if ( attribute.getId( ) == null && attribute.getConceptRef( ) == null )
{
throw new DsplException( "Attribute of Concept %s must declare either an id or a concept.", concept.getId( ) );
}
else if ( attribute.getId( ) == null )
{
// if the attribute was not given an id, set it equal to the concept id
attribute.setId( attribute.getConceptRef( ).getLocalPart( ) );
}
}
for ( ConceptProperty property : concept.getProperty( ) )
{
if ( property.getId( ) == null && property.getConceptRef( ) == null )
{
throw new DsplException( "Property of Concept %s must declare either an id or a concept.", concept.getId( ) );
}
else if ( property.getId( ) == null )
{
// if the property was not given an id, set it equal to the concept id
property.setId( property.getConceptRef( ).getLocalPart( ) );
}
}
}
}
}
public static void resolveConceptRefs( DataSet dataset )
{
if ( dataset == null ) return;
if ( dataset.getSlices( ) != null )
{
for ( Slice slice : dataset.getSlices( ).getSlice( ) )
{
for ( SliceConceptRef ref : slice.getDimension( ) )
{
ref.setSlice( slice );
}
for ( SliceConceptRef ref : slice.getMetric( ) )
{
ref.setSlice( slice );
}
}
}
if ( dataset.getConcepts( ) != null )
{
for ( Concept concept : dataset.getConcepts( ).getConcept( ) )
{
for ( ConceptProperty ref : concept.getProperty( ) )
{
ref.setParentConcept( concept );
}
for ( Attribute ref : concept.getAttribute( ) )
{
ref.setParentConcept( concept );
}
}
}
}
public static void resolveDataSetLinks( DataSet dataset ) throws JAXBException, IOException
{
if ( dataset == null ) return;
if ( dataset.getConcepts( ) != null )
{
for ( Concept concept : dataset.getConcepts( ).getConcept( ) )
{
concept.setDataSet( dataset );
}
}
if ( dataset.getTables( ) != null )
{
for ( Table table : dataset.getTables( ).getTable( ) )
{
table.setDataSet( dataset );
}
}
if ( dataset.getSlices( ) != null )
{
for ( Slice slice : dataset.getSlices( ).getSlice( ) )
{
slice.setDataSet( dataset );
}
}
}
public static void resolveConceptExtension( DataSet dataset ) throws JAXBException, IOException, DsplException
{
if ( dataset == null || dataset.getConcepts( ) == null || dataset.getConcepts( ).getConcept( ) == null ) return;
for ( Concept concept : dataset.getConcepts( ).getConcept( ) )
{
resolveConceptExtension( dataset, concept );
}
}
public static void resolveConceptTypes( DataSet dataset ) throws JAXBException, IOException, DsplException
{
if ( dataset == null || dataset.getConcepts( ) == null || dataset.getConcepts( ).getConcept( ) == null ) return;
for ( Concept concept : dataset.getConcepts( ).getConcept( ) )
{
for ( ConceptProperty property : concept.getProperty( ) )
{
// fill in the type for properties which define no explicit type, but define a concept which has a type
if ( property.getType( ) == null )
{
Concept propertyConcept = property.getConcept( );
if ( propertyConcept != null && propertyConcept.getType( ) != null )
{
ConceptProperty.Type type = new ConceptProperty.Type( );
type.setRef( propertyConcept.getType( ).getRef( ) );
property.setType( type );
}
}
// if both a type and concept are provided, make sure the type is less restrictive than the concept type
else
{
Concept propertyConcept = property.getConcept( );
if ( propertyConcept != null && propertyConcept.getType( ) != null )
{
if ( !isLessRestrictiveThan( property.getType( ).getRef( ), propertyConcept.getType( ).getRef( ) ) ) throw new DsplException( "Property %s of Concept %s declares type %s which is not less restrictive than the type %s of the Property's Concept %s", property.getId( ), concept.getId( ), property.getType( ).getRef( ), propertyConcept.getType( ).getRef( ), propertyConcept.getId( ) );
}
}
}
for ( Attribute attribute : concept.getAttribute( ) )
{
// fill in the type for attributes which define no explicit type, but define a concept which has a type
if ( attribute.getType( ) == null )
{
Concept attributeConcept = attribute.getConcept( );
if ( attributeConcept != null && attributeConcept.getType( ) != null )
{
Attribute.Type type = new Attribute.Type( );
type.setRef( attributeConcept.getType( ).getRef( ) );
attribute.setType( type );
}
}
// if both a type and concept are provided, make sure the type is less restrictive than the concept type
else
{
Concept attributeConcept = attribute.getConcept( );
if ( attributeConcept != null && attributeConcept.getType( ) != null )
{
if ( !isLessRestrictiveThan( attribute.getType( ).getRef( ), attributeConcept.getType( ).getRef( ) ) ) throw new DsplException( "Attribute %s of Concept %s declares type %s which is not less restrictive than the type %s of the Attribute's Concept %s", attribute.getId( ), concept.getId( ), attribute.getType( ).getRef( ), attributeConcept.getType( ).getRef( ), attributeConcept.getId( ) );
}
}
}
}
}
public static Concept resolveConceptExtension( DataSet dataset, Concept concept ) throws JAXBException, IOException, DsplException
{
// if our parent concept has already been set, then this concept (and all its super-concepts) have been resolved
if ( concept.getParentConcept( ) != null ) return concept;
// get this concept's parent concept, loading the dataset in which it resides if necessary
Concept parent = getParentConcept( dataset, concept );
// if this concept does not have a parent, there is nothing to do
if ( parent == null ) return concept;
// detect cycle
if ( parent.isInstanceOf( concept ) )
{
throw new DsplException( "Cycle detected in Concept: {%s}%s", dataset.getTargetNamespace( ), concept.getId( ) );
}
// recursively resolve the parent's parent (and so forth) before working with this concept
parent = resolveConceptExtension( dataset, parent );
// save a reference to our parent concept, marking that we have been resolved
concept.setParentConcept( parent );
// child concepts inherit their type from their parent
//
// from dspl.xsd (for the type element of the Concept complex type):
//
// The data type of the concept. A concept must provide a type declaration or extend
// another concept. In the case where it's extending a concept, it may also
// provide a type declaration. The type of the extended concept must be less restrictive
// than the type of the concept extending it.
//
// "Less restrictive than" (LRT) is a partial order defined as follows:
//
// string LRT float
// float LRT integer
// string LRT date
// string LRT boolean
//
if ( concept.getType( ) == null )
{
if ( parent.getType( ) == null )
{
// this is only a problem if the concept is used as the column of a table, it is not an error otherwise
//throw new DsplException( "Concept %s nor any of its ancestors declare a type.", concept.getId( ) );
}
else
{
concept.setType( parent.getType( ) );
}
}
else
{
DataType childType = concept.getType( ).getRef( );
DataType parentType = parent.getType( ).getRef( );
// ensure that the LRT rules specified above are followed
if ( !isLessRestrictiveThan( childType, parentType ) )
{
throw createTypeException( concept, parent, childType, parentType );
}
}
// concepts inherit the properties of their parent
for ( ConceptProperty parentProperty : parent.getProperty( ) )
{
ConceptProperty childProperty = concept.getProperty( parentProperty.getId( ) );
if ( childProperty == null )
{
concept.getProperty( ).add( parentProperty );
}
else
{
if ( childProperty.getConcept( ) == null && parentProperty.getConcept( ) != null )
{
childProperty.setConceptRef( parentProperty.getConceptRef( ) );
}
if ( childProperty.getType( ) == null && parentProperty.getType( ) != null )
{
ConceptProperty.Type type = new ConceptProperty.Type( );
type.setRef( parentProperty.getType( ).getRef( ) );
childProperty.setType( type );
}
checkLegalOverride( dataset, childProperty, parentProperty );
}
}
// concepts inherit the attributes of their parent
for ( Attribute parentAttribute : parent.getAttribute( ) )
{
Attribute childAttribute = concept.getAttribute( parentAttribute.getId( ) );
if ( childAttribute == null )
{
concept.getAttribute( ).add( parentAttribute );
}
else
{
if ( childAttribute.getConcept( ) == null && parentAttribute.getConcept( ) != null )
{
childAttribute.setConceptRef( parentAttribute.getConceptRef( ) );
}
if ( childAttribute.getType( ) == null && parentAttribute.getType( ) != null )
{
Attribute.Type type = new Attribute.Type( );
type.setRef( parentAttribute.getType( ).getRef( ) );
childAttribute.setType( type );
}
checkLegalOverride( dataset, childAttribute, parentAttribute );
}
}
return concept;
}
public static boolean checkLegalOverride( DataSet dataset, Attribute child, Attribute parent ) throws JAXBException, IOException, DsplException
{
//XXX I'm unsure about the dspl rules for "overriding" attributes defined by the parent concept
//XXX this method should check for type agreement as well
Concept childConcept = child.getConcept( );
Concept parentConcept = parent.getConcept( );
if ( childConcept != null && parentConcept != null )
{
if ( !isInstanceOf( childConcept, parentConcept ) )
{
throw new DsplException( "Attribute %s with Concept %s cannot override Attribute %s with incompatible concept %s.", child.getId( ), childConcept.getId( ), parent.getId( ), parentConcept.getId( ) );
}
}
if ( child.getType( ) == null || child.getType( ).getRef( ) == null )
{
throw new DsplException( "Attribute %s has no defined type", child.getId( ) );
}
if ( parent.getType( ) == null || parent.getType( ).getRef( ) == null )
{
throw new DsplException( "Attribute %s has no defined type", parent.getId( ) );
}
DataType childType = child.getType( ).getRef( );
DataType parentType = parent.getType( ).getRef( );
if ( !isLessRestrictiveThan( childType, parentType ) )
{
throw new DsplException( "Attribute %s with Type %s cannot override Attribute %s with more restrictive type %s.", child.getId( ), childType, parent.getId( ), parentType );
}
return true;
}
public static boolean checkLegalOverride( DataSet dataset, ConceptProperty child, ConceptProperty parent ) throws JAXBException, IOException, DsplException
{
//XXX I'm unsure about the dspl rules for "overriding" attributes defined by the parent concept
//XXX this method should check for type agreement as well
Concept childConcept = child.getConcept( );
Concept parentConcept = parent.getConcept( );
if ( childConcept != null && parentConcept != null )
{
if ( !isInstanceOf( childConcept, parentConcept ) )
{
throw new DsplException( "Property %s with Concept %s cannot override Property %s with incompatible concept %s.", child.getId( ), childConcept.getId( ), parent.getId( ), parentConcept.getId( ) );
}
}
if ( child.getType( ) == null || child.getType( ).getRef( ) == null )
{
throw new DsplException( "Attribute %s has no defined type", child.getId( ) );
}
if ( parent.getType( ) == null || parent.getType( ).getRef( ) == null )
{
throw new DsplException( "Attribute %s has no defined type", parent.getId( ) );
}
DataType childType = child.getType( ).getRef( );
DataType parentType = parent.getType( ).getRef( );
if ( !isLessRestrictiveThan( childType, parentType ) )
{
throw new DsplException( "Property %s with Type %s cannot override Property %s with more restrictive type %s.", child.getId( ), childType, parent.getId( ), parentType );
}
return true;
}
public static boolean isLessRestrictiveThan( DataType childType, DataType parentType )
{
if ( childType != parentType )
{
if ( parentType == DataType.FLOAT )
{
if ( childType != DataType.STRING ) return false;
}
else if ( parentType == DataType.INTEGER )
{
if ( childType != DataType.FLOAT || childType != DataType.STRING ) return false;
}
else if ( parentType == DataType.DATE )
{
if ( childType != DataType.STRING ) return false;
}
else if ( parentType == DataType.BOOLEAN )
{
if ( childType != DataType.STRING ) return false;
}
}
return true;
}
/**
* Tests whether subConcept either is the same concept as superConcept, or has superConcept
* somewhere in its parent Concept hierarchy.
*
* @param subConcept
* @param superConcept
* @return true if subConcept is an instance of superConcept
*/
public static boolean isInstanceOf( Concept subConcept, Concept superConcept )
{
if ( equals( subConcept, superConcept ) )
{
return true;
}
else if ( subConcept == null || subConcept.getParentConcept( ) == null )
{
return false;
}
else
{
return isInstanceOf( subConcept.getParentConcept( ), superConcept );
}
}
public static boolean equals( Concept ref1, Concept ref2 )
{
if ( ref1 == null || ref2 == null ) return false;
return equals( ref1.getDataSet( ).getTargetNamespace( ), ref1.getId( ), ref2.getDataSet( ).getTargetNamespace( ), ref2.getId( ) );
}
public static boolean equals( QName ref1, QName ref2 )
{
if ( ref1 == null || ref2 == null ) return false;
return equals( ref1.getNamespaceURI( ), ref1.getLocalPart( ), ref2.getNamespaceURI( ), ref2.getLocalPart( ) );
}
public static boolean equals( String namespace1, String local1, String namespace2, String local2 )
{
if ( namespace1 == null || local1 == null || namespace2 == null || local2 == null ) return false;
return namespace1.equals( namespace2 ) && local1.equals( local2 );
}
protected static DsplException createTypeException( Concept child, Concept parent, DataType childType, DataType parentType )
{
return new DsplException( "Concept %s has illegal type %s because its parent Concept %s has type %s (%s is not less restrictive than %s).", child.getId( ), childType, parent.getId( ), parentType, childType, parentType );
}
public static Concept getParentConcept( DataSet dataset, Concept concept ) throws JAXBException, IOException, DsplException
{
QName parentRef = concept.getExtends( );
return parentRef != null ? dataset.getConcept( parentRef ) : null;
}
public static Concept getCompatibleConceptRef( Slice slice, Concept superConcept, List<SliceConceptRef> refs ) throws JAXBException, IOException, DsplException
{
for ( SliceConceptRef dimension : refs )
{
Concept subConcept = dimension.getConcept( );
if ( isInstanceOf( subConcept, superConcept ) ) return subConcept;
}
return null;
}
public static String getName( String namespace ) throws MalformedURLException
{
URL url = new URL( namespace );
String[] tokens = url.getPath( ).split( "/" );
if ( tokens.length == 0 ) return null;
String name = tokens[tokens.length - 1];
return name;
}
public static final String LANGUAGE_ENGLISH = "en";
public static String getValueEnglish( List<Value> valueList )
{
if ( valueList == null || valueList.isEmpty( ) ) return null;
// search through the names for an English value
for ( Value value : valueList )
{
if ( LANGUAGE_ENGLISH.equals( value.getLang( ) ) ) return value.getValue( );
}
// if we didn't find an English language value, return the first value
return valueList.get( 0 ).getValue( );
}
}