package com.temenos.interaction.odataext.entity;
/*
* #%L
* interaction-odata4j-ext
* %%
* Copyright (C) 2012 - 2014 Temenos Holdings N.V.
* %%
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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 Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import javax.ws.rs.HttpMethod;
import org.apache.commons.lang.StringUtils;
import org.odata4j.core.ODataVersion;
import org.odata4j.core.PrefixedNamespace;
import org.odata4j.edm.EdmAnnotation;
import org.odata4j.edm.EdmAssociation;
import org.odata4j.edm.EdmAssociationEnd;
import org.odata4j.edm.EdmComplexType;
import org.odata4j.edm.EdmDataServices;
import org.odata4j.edm.EdmDataServices.Builder;
import org.odata4j.edm.EdmEntityContainer;
import org.odata4j.edm.EdmEntitySet;
import org.odata4j.edm.EdmEntityType;
import org.odata4j.edm.EdmFunctionImport;
import org.odata4j.edm.EdmMultiplicity;
import org.odata4j.edm.EdmNavigationProperty;
import org.odata4j.edm.EdmProperty;
import org.odata4j.edm.EdmProperty.CollectionKind;
import org.odata4j.edm.EdmSchema;
import org.odata4j.edm.EdmSimpleType;
import org.odata4j.edm.EdmType;
import org.odata4j.exceptions.NotFoundException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.temenos.interaction.core.entity.EntityMetadata;
import com.temenos.interaction.core.entity.Metadata;
import com.temenos.interaction.core.entity.vocabulary.terms.AbstractOdataAnnotation;
import com.temenos.interaction.core.entity.vocabulary.terms.TermComplexGroup;
import com.temenos.interaction.core.entity.vocabulary.terms.TermComplexType;
import com.temenos.interaction.core.entity.vocabulary.terms.TermIdField;
import com.temenos.interaction.core.entity.vocabulary.terms.TermListType;
import com.temenos.interaction.core.entity.vocabulary.terms.TermRestriction;
import com.temenos.interaction.core.entity.vocabulary.terms.TermRestriction.Restriction;
import com.temenos.interaction.core.entity.vocabulary.terms.TermSemanticType;
import com.temenos.interaction.core.entity.vocabulary.terms.TermValueType;
import com.temenos.interaction.core.hypermedia.CollectionResourceState;
import com.temenos.interaction.core.hypermedia.ResourceState;
import com.temenos.interaction.core.hypermedia.ResourceStateMachine;
import com.temenos.interaction.core.hypermedia.Transition;
import com.temenos.interaction.odataext.ODataHelper;
/**
* This class converts a EntityMetadata structure to odata4j's EdmDataServices.
* <p />
* This class is responsible for providing EdmMetadata for all the resources we are working on.
* To achieve this it will convert Resource/Entities present within ServiceDocument upon creation
* and save it as EdmDataServices and load non-ServiceDocument resource on demand at runtime and
* maintain in its local cache
*
* Note:
* IRIS expects the following naming conventions
*
* Entity name: Car
* Entity type: Car
* Entity set: Cars
*/
public class MetadataOData4j {
private static final Logger LOGGER = LoggerFactory.getLogger(MetadataOData4j.class);
private static final String MULTI_NAV_PROP_TO_ENTITY = "MULTI_NAV_PROP";
private ConcurrentMap<String, EdmEntitySet> nonSrvDocEdmEntitySetMap;
private ConcurrentMap<String, EdmComplexType> nonSrvDocEdmComplexTypeMap;
private Metadata metadata;
private ResourceStateMachine hypermediaEngine;
private ResourceState serviceDocument;
private String SERVICE_DOCUMENT = "ServiceDocument";
private ODataVersion odataVersion = ODataVersion.V1;
private EdmDataServicesAdapter edmDataServicesAdapter;
private EdmDataServices edmDataServices;
/**
* Construct the odata metadata ({@link EdmDataServices}) by looking up a resource
* called 'ServiceDocument' and add an EntitySet to the metadata for any collection
* resource with a transition from this 'ServiceDocument' resource.
* @param metadata metadata
* @param hypermediaEngine containing ResourceStateMachine with ServiceDocument as initial state only
*/
public MetadataOData4j(Metadata metadata, ResourceStateMachine hypermediaEngine) {
serviceDocument = hypermediaEngine.getResourceStateByName(SERVICE_DOCUMENT);
if (serviceDocument == null)
throw new RuntimeException("No "+ SERVICE_DOCUMENT + " found.");
if (serviceDocument instanceof CollectionResourceState)
throw new RuntimeException("Initial state must be an individual resource state");
this.metadata = metadata;
this.hypermediaEngine = hypermediaEngine;
this.nonSrvDocEdmEntitySetMap= new ConcurrentHashMap<String, EdmEntitySet>();
this.nonSrvDocEdmComplexTypeMap = new ConcurrentHashMap<String, EdmComplexType>();
}
/**
* @return the odataVersion
*/
public ODataVersion getOdataVersion() {
return odataVersion;
}
/**
* Change the version of OData that will be used. OData V2 is needed to permit annotations
* (used for Semantic Types)
* @param odataVersion the odataVersion to set.
*/
public void setOdataVersion(ODataVersion odataVersion) {
LOGGER.debug("OData Version set to {}", odataVersion);
this.odataVersion = odataVersion;
}
/**
* Returns all metadata - i.e. including meta data relating to resources not in the service document
*
* @return
*/
public EdmDataServices getMetadata() {
if (edmDataServicesAdapter == null) {
edmDataServicesAdapter = new EdmDataServicesAdapter(this);
}
return edmDataServicesAdapter;
}
/**
* Returns EDM metadata ONLY - i.e. only meta data relating to resources in the service document
* @return edmdataservices object
*/
EdmDataServices getEdmMetadata() {
EdmDataServices result = null;
synchronized(this) {
if(edmDataServices == null) {
try {
edmDataServices = createOData4jMetadata(metadata, hypermediaEngine, serviceDocument);
} catch (Exception e) {
LOGGER.error("Error creating odata4j metadata for resources in service document", e);
}
}
result = edmDataServices;
}
return result;
}
public EdmComplexType findEdmComplexType(String typeName) {
EdmComplexType result = null;
try {
result = getEdmMetadata().findEdmComplexType(typeName);
} catch(Exception e) {
LOGGER.error("Error getting EDM entity complex type" + typeName, e);
}
if(result == null && nonSrvDocEdmComplexTypeMap.containsKey(typeName)) {
// Check if the type is a complex type in non service document meta data
result = (EdmComplexType)nonSrvDocEdmComplexTypeMap.get(typeName);
}
if(result == null) {
// Check if the type is non service document meta data
String tmpTypeName = typeName.substring(typeName.indexOf(".") + 1, typeName.indexOf("_"));
EdmEntitySet edmEntitySet = getEdmEntitySetFromNonSrvDocResrc(getEdmEntitySetName(tmpTypeName));
if(edmEntitySet != null) {
result = (EdmComplexType)nonSrvDocEdmComplexTypeMap.get(typeName);
}
}
return result;
}
public EdmType getEdmEntityTypeByTypeName(String typeName) {
EdmType result = null;
try {
// Check if type is in the service document / EDM meta data
result = (EdmEntityType)getEdmMetadata().findEdmEntityType(typeName);
} catch(Exception e) {
LOGGER.error("Error getting EDM entity type" + typeName, e);
}
if(result == null && nonSrvDocEdmComplexTypeMap.containsKey(typeName)) {
// Check if the type is a complex type in non service document meta data
result = nonSrvDocEdmComplexTypeMap.get(typeName);
}
if(result == null) {
// Check if the type is non service document meta data
String tmpTypeName = typeName.substring(typeName.indexOf(".") + 1);
EdmEntitySet edmEntitySet = getEdmEntitySetFromNonSrvDocResrc(getEdmEntitySetName(tmpTypeName));
if(edmEntitySet != null) {
try {
result = edmEntitySet.getType();
} catch(IllegalArgumentException e) {
// Expected for cases where entity type does not have keys i.e. ServiceDocument, Metadata, etc..
LOGGER.debug("Failed to get type for {}", typeName, e);
}
}
}
return result;
}
public EdmEntitySet getEdmEntitySetByType(EdmEntityType type) {
EdmEntitySet edmEntitySet = null;
try {
edmEntitySet = getEdmMetadata().getEdmEntitySet(type);
} catch (Exception e) {
// Ignore.... as we will be try lazy loading after this
LOGGER.debug("EntitySet for [{}] not found in EdmDataServices, try loading it seperately...", type, e);
}
// If its null
if (edmEntitySet == null) {
// Let's check if we have it in non-service doc resources
edmEntitySet = getEdmEntitySetFromNonSrvDocResrc(getEdmEntitySetName(type.getName()));
}
return edmEntitySet;
}
/**
* required by GetEntitiesCommand
* @param entityName
* @return EdmEntitySet
*
*/
public EdmEntitySet getEdmEntitySetByEntityName(String entityName) {
EdmEntitySet edmEntitySet = null;
try {
edmEntitySet = ODataHelper.getEntitySet(entityName, getEdmMetadata());
} catch (Exception e) {
// Ignore.... as we will be try lazy loading after this
LOGGER.debug("EntitySet for [{}] not found in EdmDataServices, try loading it seperately...", entityName, e);
}
// If its null
if (edmEntitySet == null) {
// Let's check if we have it in non-service doc resources
edmEntitySet = getEdmEntitySetFromNonSrvDocResrc(getEdmEntitySetName(entityName));
}
return edmEntitySet;
}
/**
*
* @param entityName
*/
public void unloadMetadata(String entityName) {
String entitySetName = getEdmEntitySetName(entityName);
if(nonSrvDocEdmEntitySetMap.containsKey(entitySetName)) {
// Non service document resource - Remove nonSrvDocEdmEntitySetMap entry so that it's meta data cannot be referenced
nonSrvDocEdmEntitySetMap.remove(entitySetName);
} else {
/* This may be a service document resource, if it is - Unload the internal reference to the real EDM data services
* so that it is completely rebuilt on the next request to it
*/
synchronized (this) {
if (edmDataServices != null) {
// EDM data services has already been initialized
if (edmDataServices.findEdmEntitySet(entitySetName) != null) {
// EDM data services, i.e. service document, contains entity set therefore it needs to be rebuilt
edmDataServices = null;
}
}
}
}
}
/**
* required by producer
* @param entityName
* @return EdmEntitySet
*
*/
public EdmEntitySet getEdmEntitySetByEntitySetName(String entitySetName) {
EdmEntitySet edmEntitySet = getEdmEntitySetFromEdmDataServices(entitySetName);
if (edmEntitySet == null ) {
// this means the Entity is not part of ServiceDocument...lets load it
edmEntitySet = getEdmEntitySetFromNonSrvDocResrc(entitySetName);
// If still not found then we have to give up
if (edmEntitySet == null )
throw new NotFoundException("Fail to find/load Entity Set for [" + entitySetName + "]");
}
return edmEntitySet;
}
/**
* Method to return EdmEntitySet from nonSrvDoc Map
* @param entitySetName
* @return
*/
private EdmEntitySet getEdmEntitySetFromNonSrvDocResrc(String entitySetName) {
// Find if we already have loaded this resource before
if (nonSrvDocEdmEntitySetMap.get(entitySetName) != null)
return nonSrvDocEdmEntitySetMap.get(entitySetName);
// Try to load
return loadEdmEntitySetFromEntityName(getEntityName(entitySetName));
}
/**
* Method to load EdmEntitySet if not loaded as yet
* @param entityName
* @return
*/
private EdmEntitySet loadEdmEntitySetFromEntityName(String entityName) {
// Let's get the metadata for the resource
EntityMetadata entityMetadata = metadata.getEntityMetadata(entityName);
if (entityMetadata == null)
throw new NotFoundException("Fail to find/load Entity Metadata for [" + entityName + "]");
// Lets build the EdmEntitySet form EntityMetadata
Map<String, EdmComplexType.Builder> complexTypes = new HashMap<String, EdmComplexType.Builder>();
EdmEntityType.Builder entityType = getEdmTypeBuilder(entityMetadata, complexTypes, false);
if (entityType != null) {
EdmEntitySet.Builder bEntitySetBuilder = EdmEntitySet.newBuilder().setName(getEdmEntitySetName(entityName)).setEntityType(entityType);
EdmEntitySet edmEntitySet = bEntitySetBuilder.build();
for(Map.Entry<String, EdmComplexType.Builder> entry: complexTypes.entrySet()) {
this.nonSrvDocEdmComplexTypeMap.put(entry.getKey(), entry.getValue().build());
}
// Append to the map
nonSrvDocEdmEntitySetMap.put(getEdmEntitySetName(entityName), edmEntitySet);
return edmEntitySet;
}
return null;
}
/**
* Method to search EdmEntitySet within EdmDataServices
* @param entitySetName
* @return
*/
private EdmEntitySet getEdmEntitySetFromEdmDataServices(String entitySetName) {
EdmEntitySet ees = null;
try {
ees = getEdmMetadata().findEdmEntitySet(entitySetName);
} catch (Exception e) {
LOGGER.warn("Failed to find EDM entity set", e);
}
return ees;
}
/**
* Create EDM metadata from Resource State Machine containing ServiceDocument
* @param metadata
* @param hypermediaEngine
* @param serviceDocument
* @return
*/
public EdmDataServices createOData4jMetadata(Metadata metadata, ResourceStateMachine hypermediaEngine, ResourceState serviceDocument) {
Builder mdBuilder = EdmDataServices.newBuilder();
mdBuilder.setVersion(odataVersion);
LOGGER.info("Using OData version {}", odataVersion);
if (odataVersion==ODataVersion.V2)
mdBuilder.addNamespaces(Collections.singletonList(new PrefixedNamespace(AbstractOdataAnnotation.NAMESPACE, AbstractOdataAnnotation.PREFIX)));
List<EdmSchema.Builder> bSchemas = new ArrayList<EdmSchema.Builder>();
EdmSchema.Builder bSchema = new EdmSchema.Builder();
List<EdmEntityContainer.Builder> bEntityContainers = new ArrayList<EdmEntityContainer.Builder>();
Map<String, EdmEntityType.Builder> bEntityTypeMap = new HashMap<String, EdmEntityType.Builder>();
Map<String, EdmComplexType.Builder> bComplexTypeMap = new HashMap<String, EdmComplexType.Builder>();
Map<String, EdmEntitySet.Builder> bEntitySetMap = new HashMap<String, EdmEntitySet.Builder>();
Map<String, EdmFunctionImport.Builder> bFunctionImportMap = new HashMap<String, EdmFunctionImport.Builder>();
List<EdmAssociation.Builder> bAssociations = new ArrayList<EdmAssociation.Builder>();
// Process meta data present in the service document
for (ResourceState state : hypermediaEngine.getStates()) {
// Skip Service Document
if (serviceDocument.equals(state))
continue;
EntityMetadata entityMetadata = null;
try {
entityMetadata = metadata.getEntityMetadata(state.getEntityName());
} catch (Exception e) {
LOGGER.warn("Failed to get metadata for state name '{}' / entity name '{}'", state.getName(), state.getEntityName(), e);
continue;
}
if(entityMetadata == null) {
LOGGER.warn("Failed to get metadata for state name '{}' / entity name '{}'", state.getName(), state.getEntityName());
continue;
}
// Always strictKeyCheck here because we will be building EdmDataServices from this
EdmEntityType.Builder bEntityType = getEdmTypeBuilder(entityMetadata, bComplexTypeMap, true);
if (bEntityType != null) {
bEntityTypeMap.put(state.getEntityName(), bEntityType);
} else {
LOGGER.warn("Entity name '{}' does not have type. entityMetadata={}", state.getEntityName(), entityMetadata);
}
}
//The model name is available after processing the states, i.e. the namespace should be created afterwards
String serviceName = metadata.getModelName();
String namespace = serviceName + Metadata.MODEL_SUFFIX;
// Add Navigation Properties
// build associations
Map<EdmEntityType.Builder, Map<String, EdmAssociation.Builder>> entityTypeToStateAssociations = new HashMap<EdmEntityType.Builder, Map<String, EdmAssociation.Builder>>();
for (EdmEntityType.Builder bEntityType : bEntityTypeMap.values()) {
Map<String, EdmAssociation.Builder> bAssociationMap = buildAssociations(namespace, bEntityType, bEntityTypeMap, hypermediaEngine, serviceDocument);
entityTypeToStateAssociations.put(bEntityType, bAssociationMap);
bAssociations.addAll(bAssociationMap.values());
}
Map<String,String> multiAssociation = new HashMap<String,String>();
int multipleAssoc = 0;
for (ResourceState source : hypermediaEngine.getStates()) {
for (ResourceState target : source.getAllTargets()) {
Collection<Transition> entityTransitions = source.getTransitions(target);
if (entityTransitions != null) {
for(Transition entityTransition : entityTransitions) {
ResourceState sourceState = entityTransition.getSource();
ResourceState targetState = entityTransition.getTarget();
EdmEntityType.Builder bEntityType = bEntityTypeMap.get(sourceState.getEntityName());
if (bEntityType != null
&& !entityTransition.getTarget().isPseudoState()
&& !entityTransition.getTarget().equals(serviceDocument)
&& !(entityTransition.getSource() instanceof CollectionResourceState)) {
//We can have more than one navigation property for the same association
String navPropertyName = targetState.getName();
//We can have transitions to a resource state from multiple source states
if (entityTransition.getLabel() != null) {
navPropertyName = entityTransition.getLabel();
} else if (multiAssociation.get(navPropertyName) != null) {
navPropertyName = navPropertyName + "_" + multipleAssoc++;
}
multiAssociation.put(navPropertyName, targetState.getName());
Map<String, EdmAssociation.Builder> bAssociationMap = entityTypeToStateAssociations.get(bEntityType);
EdmAssociation.Builder relationship = bAssociationMap.get(targetState.getName());
bEntityType.addNavigationProperties(EdmNavigationProperty
.newBuilder(navPropertyName)
.setRelationship(relationship)
.setFromTo(relationship.getEnd1(), relationship.getEnd2()));
}
}
}
}
if (source instanceof CollectionResourceState) {
// Index EntitySets by Entity name
EdmEntityType.Builder entityType = bEntityTypeMap.get(source.getEntityName());
if (entityType == null) {
LOGGER.warn("Entity type not found for {}" + source.getEntityName());
continue;
}
Transition fromInitialState = serviceDocument.getTransition(source);
EdmEntitySet.Builder bEntitySet = EdmEntitySet.newBuilder().setName(source.getName()).setEntityType(entityType);
if (fromInitialState != null) {
// Add entity set
bEntitySetMap.put(source.getEntityName(), bEntitySet);
} else {
LOGGER.debug("Not adding entity set [{}] to metadata, no transition from initial state [{}]", source.getName(), serviceDocument.getName());
}
}
}
Collection<ResourceState> allTargets = hypermediaEngine.getStates();
for (ResourceState state : allTargets) {
if (state instanceof CollectionResourceState) {
Transition fromInitialState = serviceDocument.getTransition(state);
if (fromInitialState == null) {
EdmEntitySet.Builder bEntitySet = bEntitySetMap.get(state.getEntityName());
if(bEntitySet == null) {
LOGGER.warn("Failed to find entity set for entity {}", state.getEntityName());
} else {
// Add Function
EdmFunctionImport.Builder bFunctionImport = EdmFunctionImport.newBuilder()
.setName(state.getName())
.setEntitySet(bEntitySet)
.setHttpMethod(HttpMethod.GET)
.setIsCollection(true)
.setReturnType(bEntityTypeMap.get(state.getEntityName()));
bFunctionImportMap.put(state.getName(), bFunctionImport);
}
}
}
}
EdmEntityContainer.Builder bEntityContainer = EdmEntityContainer.newBuilder()
.setName(serviceName)
.setIsDefault(true)
.addEntitySets(new ArrayList<EdmEntitySet.Builder>(bEntitySetMap.values()))
.addFunctionImports(new ArrayList<EdmFunctionImport.Builder>(bFunctionImportMap.values()));
bEntityContainers.add(bEntityContainer);
List<EdmEntityType.Builder> bEntityTypes = new ArrayList<EdmEntityType.Builder>();
bEntityTypes.addAll(bEntityTypeMap.values());
List<EdmComplexType.Builder> bComplexTypes = new ArrayList<EdmComplexType.Builder>();
bComplexTypes.addAll(bComplexTypeMap.values());
bSchema
.setNamespace(namespace)
.setAlias(serviceName)
.addEntityTypes(bEntityTypes)
.addComplexTypes(bComplexTypes)
.addAssociations(bAssociations)
.addEntityContainers(bEntityContainers);
bSchemas.add(bSchema);
mdBuilder.addSchemas(bSchemas);
//Build the EDM metadata
return mdBuilder.build();
}
/**
* Method to generate EdmEntityType.Builder from EntityMetadata
* @param entityMetadata
* @return
*/
private EdmEntityType.Builder getEdmTypeBuilder(EntityMetadata entityMetadata, Map<String, EdmComplexType.Builder> bComplexTypeMap, boolean strictKeyCheck) {
String namespace = metadata.getModelName() + Metadata.MODEL_SUFFIX;
bComplexTypeMap = bComplexTypeMap == null ? new HashMap<String, EdmComplexType.Builder>() : bComplexTypeMap;
List<EdmProperty.Builder> bProperties = new ArrayList<EdmProperty.Builder>();
List<String> keys = new ArrayList<String>();
// Build Annotations for EntityType for Odata clients to find out which properties are FlterOnly and DisplayOnly
// So for clients;
// DisplayProperties = AllProperties - FilterOnlyProperties
// FilterProperties = AllProperties - DisplayOnlyProperties
List<String> displayOnlyProps = new ArrayList<String>();
List<String> filterOnlyProps = new ArrayList<String>();
String complexTypePrefix = new StringBuilder(entityMetadata.getEntityName()).append("_").toString();
for(String propertyName : entityMetadata.getPropertyVocabularyKeySet()) {
LOGGER.debug("EdmTypeBuilder[{}] - {}", entityMetadata.getEntityName(), propertyName);
//Entity properties, lets gather some information about the property
String termComplex = entityMetadata.getTermValue(propertyName, TermComplexType.TERM_NAME); // Is vocabulary a group (Complex Type)
boolean termList = Boolean.parseBoolean(entityMetadata.getTermValue(propertyName, TermListType.TERM_NAME)); // Is vocabulary a List of (Complex Types)
String termComplexGroup = entityMetadata.getTermValue(propertyName, TermComplexGroup.TERM_NAME); // Is vocabulary belongs to a group (ComplexType)
boolean isNullable = entityMetadata.isPropertyNullable(propertyName);
if (termComplex.equals("false")) {
// This means we are dealing with plain property, either belongs to Entity or ComplexType (decide later, lets build it first)
EdmType edmType = termValueToEdmType(entityMetadata.getTermValue(propertyName, TermValueType.TERM_NAME));
EdmProperty.Builder ep = EdmProperty.newBuilder(entityMetadata.getSimplePropertyName(propertyName)).
setType(edmType).
setNullable(isNullable);
// Adding Semantic Type
if (annotationAllowed()) {
// Add an annotation if a semantic type is defined for the property
List<EdmAnnotation<?>> annotations = new LinkedList<EdmAnnotation<?>>();
String semanticType = entityMetadata.getTermValue(propertyName, TermSemanticType.TERM_NAME);
if (semanticType != null)
annotations.add((EdmAnnotation.attribute(TermSemanticType.NAMESPACE, TermSemanticType.PREFIX, TermSemanticType.CSDL_NAME, semanticType)));
ep.setAnnotations(annotations);
}
if (termComplexGroup == null) {
if (annotationAllowed()) {
if (entityMetadata.isPropertyDisplayOnly(propertyName))
displayOnlyProps.add(propertyName);
else if (entityMetadata.isPropertyFilterOnly(propertyName))
filterOnlyProps.add(propertyName);
}
// Property belongs to an Entity Type, simply add it
bProperties.add(ep);
} else {
// Property belongs to a group (complex type), first make sure we have a group
// so add a group with Entity name space and group name
addComplexType(namespace, complexTypePrefix + entityMetadata.getSimplePropertyName(termComplexGroup), bComplexTypeMap);
// And then add the property into complex type
String complexTypeName = new StringBuilder(complexTypePrefix).append(entityMetadata.getSimplePropertyName(termComplexGroup)).toString();
addPropertyToComplexType(namespace, complexTypeName, ep, bComplexTypeMap);
if (annotationAllowed()) {
String fullyQualifiedName = new StringBuilder(complexTypeName).append(".").append(entityMetadata.getSimplePropertyName(propertyName)).toString();
if (entityMetadata.isPropertyDisplayOnly(propertyName))
displayOnlyProps.add(fullyQualifiedName);
else if (entityMetadata.isPropertyFilterOnly(propertyName))
filterOnlyProps.add(fullyQualifiedName);
}
}
} else {
// This means vocabulary is a group (complex type), so add it in a map
String complexPropertyName = complexTypePrefix + entityMetadata.getSimplePropertyName(propertyName);
addComplexType(namespace, complexPropertyName, bComplexTypeMap);
if (termComplexGroup != null) {
// This mean group (complex type) belongs to a group (complex type), so make sure add the parent group and add
// nested group as group property
addComplexType(namespace, complexTypePrefix + entityMetadata.getSimplePropertyName(termComplexGroup), bComplexTypeMap);
addComplexTypeToComplexType(namespace, complexTypePrefix + entityMetadata.getSimplePropertyName(termComplexGroup), complexPropertyName, isNullable, termList, bComplexTypeMap);
} else {
// This means group (complex type) belongs to an Entity, so simply build and add as a Entity prop
EdmProperty.Builder ep;
if (termList) {
ep = EdmProperty.newBuilder(complexPropertyName).
setType(bComplexTypeMap.get(namespace + "." + complexPropertyName)).
setCollectionKind(CollectionKind.Bag).
setNullable(isNullable);
} else {
ep = EdmProperty.newBuilder(complexPropertyName).
setType(bComplexTypeMap.get(namespace + "." + complexPropertyName)).
setNullable(isNullable);
}
bProperties.add(ep);
}
}
//Entity keys
if(entityMetadata.getTermValue(propertyName, TermIdField.TERM_NAME).equals("true")) {
if(termComplex.equals("true")) {
keys.add(complexTypePrefix + entityMetadata.getSimplePropertyName(propertyName));
}
else {
keys.add(propertyName);
}
}
}
// Add entity type - sometimes we would like to create entityType even if it does not have key
// strictKeyCheck will help us in this regard where we can skip the type generated for
// EdmDataServices but we can keep the one generated for an individual entity
if (keys.size() > 0 || !strictKeyCheck) {
EdmEntityType.Builder bEntityType = EdmEntityType.newBuilder().setNamespace(namespace).setAlias(entityMetadata.getEntityName()).setName(entityMetadata.getEntityName()).addKeys(keys).addProperties(bProperties);
if (annotationAllowed()) {
// Append additional metadata as annotations
List<EdmAnnotation<?>> edmEntityTypeAnnotations = new LinkedList<EdmAnnotation<?>>();
if (displayOnlyProps.size() > 0)
edmEntityTypeAnnotations.add((EdmAnnotation.attribute(TermRestriction.NAMESPACE, TermRestriction.PREFIX, Restriction.DISPLAYONLY.getValue(), getPropertiesAsCSV(displayOnlyProps))));
if (filterOnlyProps.size() > 0)
edmEntityTypeAnnotations.add((EdmAnnotation.attribute(TermRestriction.NAMESPACE, TermRestriction.PREFIX, Restriction.FILTEREONLY.getValue(), getPropertiesAsCSV(filterOnlyProps))));
bEntityType.setAnnotations(edmEntityTypeAnnotations);
}
return bEntityType;
} else {
LOGGER.error("Unable to add EntityType for [{}] - no ID column defined", entityMetadata.getEntityName());
return null;
}
}
private Map<String, EdmAssociation.Builder> buildAssociations(String namespace, EdmEntityType.Builder entityType, Map<String, EdmEntityType.Builder> bEntityTypeMap, ResourceStateMachine hypermediaEngine, ResourceState serviceDocument) {
// Obtain the relation between entities and write navigation properties
Map<String, EdmAssociation.Builder> bAssociationMap = new HashMap<String, EdmAssociation.Builder>();
//Map<Association name, Entity relation>
Map<String, EdmAssociation.Builder> relations = new HashMap<String, EdmAssociation.Builder>();
String entityName = entityType.getName();
Collection<Transition> entityTransitions = hypermediaEngine.getTransitionsById().values();
if (entityTransitions != null) {
//Find out which target entities have more than one transition from this state
Set<String> targetStateNames = new HashSet<String>();
Map<String, String> multipleNavPropsToEntity = new HashMap<String, String>(); //Map<TargetEntityName, TargetStateName>
for(Transition entityTransition : entityTransitions) {
if (entityTransition.getSource().getEntityName().equals(entityName)
&& entityTransition.getTarget() != null
&& !entityTransition.getTarget().isPseudoState()
&& !entityTransition.getTarget().equals(serviceDocument)) {
String targetEntityName = entityTransition.getTarget().getEntityName();
String targetStateName = entityTransition.getTarget().getName();
String lastTargetStateName = multipleNavPropsToEntity.get(targetEntityName);
if(lastTargetStateName == null) {
multipleNavPropsToEntity.put(targetEntityName, targetStateName);
targetStateNames.add(entityTransition.getTarget().getName());
}
else if(!targetStateName.equals(lastTargetStateName)) { //Disregard transitions from multiple source states
multipleNavPropsToEntity.put(targetEntityName, MULTI_NAV_PROP_TO_ENTITY); //null indicates to generate multiple navigation properties
}
}
}
//Create navigation properties from transitions
Set<String> npNames = new HashSet<String>();
for(Transition entityTransition : entityTransitions) {
ResourceState sourceState = entityTransition.getSource();
ResourceState targetState = entityTransition.getTarget();
if (targetState == null)
continue;
String npName = targetState.getName();
if (sourceState.getEntityName().equals(entityName)
&& !entityTransition.getTarget().isPseudoState()
&& !entityTransition.getTarget().equals(serviceDocument)
&& !npNames.contains(npName)
&& !(entityTransition.getSource() instanceof CollectionResourceState)) { //We can have transitions to a resource state from multiple source states
// Use the entity names to define the relation
String relationName;
if(multipleNavPropsToEntity.get(targetState.getEntityName()).equals(MULTI_NAV_PROP_TO_ENTITY)) {
//More than one transition => use separate associations "sourceEntityName_navPropName"
relationName = sourceState.getEntityName() + "_" + targetState.getName();
}
else {
//Only one transition => use single association "sourceEntityName_targetEntityName"
relationName = sourceState.getEntityName() + "_" + targetState.getEntityName();
String invertedRelationName = targetState.getEntityName() + "_" + sourceState.getEntityName();
if(relations.containsKey(invertedRelationName)) {
relationName = invertedRelationName;
}
}
//Multiplicity
EdmMultiplicity multiplicitySource = targetState instanceof CollectionResourceState ? EdmMultiplicity.ONE : EdmMultiplicity.MANY;
EdmMultiplicity multiplicityTarget = targetState instanceof CollectionResourceState ? EdmMultiplicity.MANY : EdmMultiplicity.ONE;
// Association
EdmAssociationEnd.Builder sourceRole = EdmAssociationEnd.newBuilder()
.setRole(relationName + "_Source")
.setType(entityType)
.setMultiplicity(multiplicitySource);
EdmEntityType.Builder targetEntityType = bEntityTypeMap.get(targetState.getEntityName());
assert(targetEntityType != null);
EdmAssociationEnd.Builder targetRole = EdmAssociationEnd.newBuilder()
.setRole(relationName + "_Target")
.setType(targetEntityType)
.setMultiplicity(multiplicityTarget);
EdmAssociation.Builder bAssociation = EdmAssociation.newBuilder()
.setNamespace(namespace)
.setName(relationName)
.setEnds(sourceRole, targetRole);
bAssociationMap.put(targetState.getName(), bAssociation);
if (!relations.containsKey(relationName)) {
relations.put(relationName, bAssociation);
}
}
}
}
return bAssociationMap;
}
/**
* Convert a Metadata vocabulary TermValueType to EdmType
* @param type TermValueType
* @return EdmType
*/
public static EdmType termValueToEdmType(String type) {
EdmType edmType;
if(type.equals(TermValueType.NUMBER)) {
edmType = EdmSimpleType.DOUBLE;
}
else if(type.equals(TermValueType.INTEGER_NUMBER)) {
edmType = EdmSimpleType.INT64;
}
else if(type.equals(TermValueType.TIMESTAMP) ||
type.equals(TermValueType.DATE)) {
edmType = EdmSimpleType.DATETIME;
}
else if(type.equals(TermValueType.TIME)) {
edmType = EdmSimpleType.TIME;
}
else if(type.equals(TermValueType.BOOLEAN)) {
edmType = EdmSimpleType.BOOLEAN;
}
else {
edmType = EdmSimpleType.STRING;
}
return edmType;
}
/**
* Build the complex type if and only if same name group is not found in the map
* @param complexTypeName
* @param bComplexTypeMap
*/
private void addComplexType(String nameSpace, String complexTypeName,
Map<String, EdmComplexType.Builder> bComplexTypeMap) {
String complexTypeFullName = new StringBuilder(nameSpace)
.append(".").append(complexTypeName)
.toString();
if (bComplexTypeMap.get(complexTypeFullName) == null ) {
EdmComplexType.Builder cb =
EdmComplexType.newBuilder().setNamespace(nameSpace)
.setName(complexTypeName);
bComplexTypeMap.put(complexTypeFullName, cb);
}
}
/**
* Adding the property to complex type if and only if not already added as a property of the complex type
* @param serviceNameSpace
* @param complexTypeName
* @param edmPropertyBuilder
* @param bComplexTypeMap
*/
private void addPropertyToComplexType(String nameSpace, String complexTypeName, EdmProperty.Builder edmPropertyBuilder, Map<String, EdmComplexType.Builder> bComplexTypeMap) {
String complexTypeFullName = new StringBuilder(nameSpace)
.append(".").append(complexTypeName)
.toString();
if (bComplexTypeMap.get(complexTypeFullName).findProperty(edmPropertyBuilder.getName()) == null) {
List<EdmProperty.Builder> bl = new ArrayList<EdmProperty.Builder>();
bl.add(edmPropertyBuilder);
bComplexTypeMap.get(complexTypeFullName).addProperties(bl);
}
}
/**
* Adding the ComplexType to Complex Type if and only is not already added as a property of the complex type
* @param serviceNameSpace
* @param complexTypeName
* @param edmPropertyBuilder
* @param bComplexTypeMap
*/
private void addComplexTypeToComplexType(String nameSpace, String complexTypeName, String nestedComplexType, boolean isNullable, boolean isList, Map<String, EdmComplexType.Builder> bComplexTypeMap) {
String complexTypeFullName = new StringBuilder(nameSpace).append(".").append(complexTypeName).toString();
String nestComplexTypeFullName = new StringBuilder(nameSpace).append(".").append(nestedComplexType).toString();
if (bComplexTypeMap.get(complexTypeFullName).findProperty(nestedComplexType) == null) {
List<EdmProperty.Builder> bl = new ArrayList<EdmProperty.Builder>();
EdmProperty.Builder ep;
if (isList) {
ep = EdmProperty.newBuilder(nestedComplexType)
.setType(bComplexTypeMap.get(nestComplexTypeFullName))
.setCollectionKind(CollectionKind.Bag)
.setNullable(isNullable);
} else {
ep = EdmProperty.newBuilder(nestedComplexType)
.setType(bComplexTypeMap.get(nestComplexTypeFullName))
.setNullable(isNullable);
}
bl.add(ep);
bComplexTypeMap.get(complexTypeFullName).addProperties(bl);
}
}
/**
* By Odata convention EntitySetName should be plural of an EntityName. If EntityName
* provided in null or empty, this method would return empty string
* @param entityName
* @return
*/
private String getEdmEntitySetName(String entityName) {
if (entityName != null && !entityName.isEmpty())
return entityName + "s";
return "";
}
/**
* By Odata convention EntitySetName is a plural of an EntityName, this method would
* return entity name by removing the trailing 's' from the provided entitySetName,
* otherwise will return empty string
* @param entitySetName
* @return
*/
private String getEntityName(String entitySetName) {
if (entitySetName != null && !entitySetName.isEmpty())
return entitySetName.substring(0, entitySetName.length() -1);
return "";
}
/**
* XML representation for debugging
*/
@Override
public String toString() {
java.io.StringWriter wr = new java.io.StringWriter();
org.odata4j.format.xml.EdmxFormatWriter.write(getEdmMetadata(), wr);
return wr.toString();
}
/*
* Method to check if we should embed annotation in the metadata
*/
private boolean annotationAllowed() {
return !ODataVersion.V1.equals(this.odataVersion);
}
/**
* Convert List<String> to single , seperated string
* @param properties
* @return
*/
private String getPropertiesAsCSV(List<String> properties) {
return StringUtils.join(properties.toArray(), ",");
}
}