public final class ReadOptions {
  private FilterExpression filter;
  private OrderByExpression orderBy;
  private String skipToken;
  private Integer skip;
  private Integer top;

  /**
   * None read options.
   *
   * @return the read options
   */
  public static ReadOptions none() {
    return new ReadOptions();
  }

  /**
   * Start builder.
   *
   * @return the builder
   */
  public static Builder start() {
    return new Builder();
  }

  /**
   * Start builder.
   *
   * @param filter the filterApplied
   * @return the builder
   */
  public static Builder start(FilterExpression filter) {
    return new Builder().filter(filter);
  }

  /**
   * Start builder.
   *
   * @param orderBy the orderApplied by
   * @return the builder
   */
  public static Builder start(OrderByExpression orderBy) {
    return new Builder().order(orderBy);
  }

  /**
   * Start builder.
   *
   * @param skipToken the skipApplied token
   * @param skip the skipApplied
   * @return the builder
   */
  public static Builder start(String skipToken, Integer skip) {
    return new Builder().skip(skipToken, skip);
  }

  /**
   * Start builder.
   *
   * @param top the topApplied
   * @return the builder
   */
  public static Builder start(Integer top) {
    return new Builder().top(top);
  }

  /**
   * Gets filterApplied.
   *
   * @return the filterApplied
   */
  public FilterExpression getFilter() {
    return filter;
  }

  /**
   * Gets orderApplied by.
   *
   * @return the orderApplied by
   */
  public OrderByExpression getOrderBy() {
    return orderBy;
  }

  /**
   * Gets skipApplied.
   *
   * @return the skipApplied
   */
  public Integer getSkip() {
    return skip;
  }

  /**
   * Gets skipApplied token.
   *
   * @return the skipApplied token
   */
  public String getSkipToken() {
    return skipToken;
  }

  /**
   * Gets topApplied.
   *
   * @return the topApplied
   */
  public Integer getTop() {
    return top;
  }

  /**
   * The type Builder.
   */
  public static class Builder {
    private ReadOptions options = new ReadOptions();

    /**
     * Build read options.
     *
     * @return the read options
     */
    public ReadOptions build() {
      return options;
    }

    /**
     * Filter builder.
     *
     * @param filter the filterApplied
     * @return the builder
     */
    public Builder filter(FilterExpression filter) {
      options.filter = filter;
      return this;
    }

    /**
     * Order builder.
     *
     * @param orderBy the orderApplied by
     * @return the builder
     */
    public Builder order(OrderByExpression orderBy) {
      options.orderBy = orderBy;
      return this;
    }

    /**
     * Skip builder.
     *
     * @param skipToken the skipApplied token
     * @param skip the skipApplied
     * @return the builder
     */
    public Builder skip(String skipToken, Integer skip) {
      options.skipToken = skipToken;
      options.skip = skip;
      return this;
    }

    /**
     * Top builder.
     *
     * @param top the topApplied
     * @return the builder
     */
    public Builder top(Integer top) {
      options.top = top;
      return this;
    }
  }
} public final class ReadResult<T> {
  private final Collection<T> result;
  private boolean appliedSkip = false;
  private boolean appliedTop = false;
  private boolean appliedOrder = false;
  private boolean appliedFilter = false;

  private ReadResult(Collection<T> result) {
    this.result = new ArrayList<>(result);
  }

  /**
   * Gets result.
   *
   * @return the result
   */
  public Collection<T> getResult() {
    return Collections.unmodifiableCollection(result);
  }

  /**
   * Get first result from results collection
   * or <code>null</code> if result collection is empty.
   *
   * @return first result from results collection
   *          or <code>null</code> if result collection is empty.
   */
  public T getFirst() {
    if(result.isEmpty()) {
      return null;
    }
    return result.iterator().next();
  }

  /**
   * Applied skipApplied.
   *
   * @return the boolean
   */
  public boolean isSkipApplied() {
    return appliedSkip;
  }

  /**
   * Applied topApplied.
   *
   * @return the boolean
   */
  public boolean isTopApplied() {
    return appliedTop;
  }

  /**
   * Applied orderApplied.
   *
   * @return the boolean
   */
  public boolean isOrderApplied() {
    return appliedOrder;
  }

  /**
   * Applied filterApplied.
   *
   * @return the boolean
   */
  public boolean isFilterApplied() {
    return appliedFilter;
  }

  /**
   * For result.
   *
   * @param <T>  the type parameter
   * @return the builder
   */
  public static <T> Builder<T> start() {
    return new Builder<>();
  }

  /**
   * For result.
   *
   * @param <T>  the type parameter
   * @param result the result
   * @return the builder
   */
  public static <T> Builder<T> forResult(Collection<T> result) {
    return new Builder<>(result);
  }

  /**
   * Start building a result with applied settings from existing result.
   *
   * @param readResult existing result
   * @param result result values
   * @param <T> type parameter for ReadResult
   * @return the new ReadResult
   */
  public static <T> Builder<T> fromResult(ReadResult<T> readResult, Collection<T> result) {
    return new Builder<>(result).apply(readResult);
  }

  /**
   * For an empty result.
   *
   * @param <T>  the type parameter
   * @return the empty read result
   */
  public static <T> ReadResult<T> empty() {
    return new Builder<T>(Collections.emptyList()).build();
  }

  /**
   * The type Builder.
   * @param <T>  the type parameter
   */
  public static class Builder<T> {
    private final ReadResult<T> readResult;

    /**
     * Instantiates a new Builder.
     */
    public Builder() {
      this(new ArrayList<>());
    }

    /**
     * Instantiates a new Builder.
     *
     * @param result the result
     */
    public Builder(Collection<T> result) {
      readResult = new ReadResult<>(result);
    }

    Builder<T> apply(ReadResult<T> result) {
      readResult.appliedFilter = result.appliedFilter;
      readResult.appliedOrder = result.appliedOrder;
      readResult.appliedSkip = result.appliedSkip;
      readResult.appliedTop = result.appliedTop;
      return this;
    }


    /**
     * Set top to applied.
     *
     * @return the builder
     */
    public Builder<T> topApplied() {
      readResult.appliedTop = true;
      return this;
    }

    /**
     * Set skip to applied.
     *
     * @return the builder
     */
    public Builder<T> skipApplied() {
      readResult.appliedSkip = true;
      return this;
    }

    /**
     * Set filter to applied.
     *
     * @return the builder
     */
    public Builder<T> filterApplied() {
      readResult.appliedFilter = true;
      return this;
    }

    /**
     * Set order to applied.
     *
     * @return the builder
     */
    public Builder<T> orderApplied() {
      readResult.appliedOrder = true;
      return this;
    }

    public Builder<T> values(Collection<T> values) {
      if(!readResult.result.isEmpty()) {
        readResult.result.clear();
      }
      readResult.result.addAll(values);
      return this;
    }

    /**
     * Build read result.
     *
     * @return the read result
     */
    public ReadResult<T> build() {
      return readResult;
    }
  }
} public interface DataSource {

  /**
   * Retrieves the whole data list for the specified entity set.
   * @param entitySet the requested
   * @param readOptions the read options
   * @return the requested data list
   * @throws ODataNotImplementedException the o data not implemented exception
   * @throws ODataNotFoundException the o data not found exception
   * @throws EdmException the edm exception
   * @throws ODataApplicationException the o data application exception
   */
  ReadResult<?> readData(EdmEntitySet entitySet, ReadOptions readOptions)
      throws ODataNotImplementedException, ODataNotFoundException, EdmException, ODataApplicationException;

  /**
   * Retrieves a single data object for the specified entity set and key.
   * @param entitySet the requested
   * @param keys the entity key as map of key names to key values
   * @return the requested data object
   * @throws ODataNotImplementedException the o data not implemented exception
   * @throws ODataNotFoundException the o data not found exception
   * @throws EdmException the edm exception
   * @throws ODataApplicationException the o data application exception
   */
  Object readData(EdmEntitySet entitySet, Map<String, Object> keys) throws ODataNotImplementedException,
      ODataNotFoundException, EdmException, ODataApplicationException;

  /**
   * <p>Retrieves related data for the specified source data, entity set, and key.</p>
   * <p>If the underlying association of the EDM is specified to have target
   * multiplicity '*' and no target key is given, this method returns a list of
   * related data, otherwise it returns a single data object.</p>
   * @param sourceEntitySet the EdmEntitySet of the source entity
   * @param sourceData the data object of the source entity
   * @param targetEntitySet the requested target
   * @param targetKeys the key of the target entity as map of key names to key values (optional)
   * @return the requested related data object, either a list or a single object
   * @throws ODataNotImplementedException the o data not implemented exception
   * @throws ODataNotFoundException the o data not found exception
   * @throws EdmException the edm exception
   * @throws ODataApplicationException the o data application exception
   */
  Object readRelatedData(EdmEntitySet sourceEntitySet, Object sourceData, EdmEntitySet targetEntitySet,
      Map<String, Object> targetKeys) throws ODataNotImplementedException, ODataNotFoundException, EdmException,
      ODataApplicationException;

  /**
   * Retrieves the binary data and the MIME type for the media resource
   * associated to the specified media-link entry.
   * @param entitySet the EdmEntitySet of the media-link entry
   * @param mediaLinkEntryData the data object of the media-link entry
   * @return the binary data and the MIME type of the media resource
   * @throws ODataNotImplementedException the o data not implemented exception
   * @throws ODataNotFoundException the o data not found exception
   * @throws EdmException the edm exception
   * @throws ODataApplicationException the o data application exception
   */
  BinaryData readBinaryData(EdmEntitySet entitySet, Object mediaLinkEntryData) throws ODataNotImplementedException,
      ODataNotFoundException, EdmException, ODataApplicationException;

  /**
   * <p>Creates and returns a new instance of the requested data-object type.</p>
   * <p>This instance must not be part of the corresponding list and should
   * have empty content, apart from the key and other mandatory properties.
   * However, intermediate objects to access complex properties must not be
   * <code>null</code>.</p>
   * @param entitySet the
   * the object must correspond to
   * @return the new data object
   * @throws ODataNotImplementedException the o data not implemented exception
   * @throws EdmException the edm exception
   * @throws ODataApplicationException the o data application exception
   */
  Object newDataObject(EdmEntitySet entitySet) throws ODataNotImplementedException, EdmException,
      ODataApplicationException;

  /**
   * Writes the binary data for the media resource associated to the
   * specified media-link entry.
   * @param entitySet the
   * of the media-link entry
   * @param mediaLinkEntryData the data object of the media-link entry
   * @param binaryData the binary data of the media resource along with
   * the MIME type of the binary data
   * @throws ODataNotImplementedException the o data not implemented exception
   * @throws ODataNotFoundException the o data not found exception
   * @throws EdmException the edm exception
   * @throws ODataApplicationException the o data application exception
   */
  void writeBinaryData(EdmEntitySet entitySet, Object mediaLinkEntryData, BinaryData binaryData)
      throws ODataNotImplementedException, ODataNotFoundException, EdmException, ODataApplicationException;

  /**
   * Deletes a single data object identified by the specified entity set and key.
   * @param entitySet the
   * of the entity to be deleted
   * @param keys the entity key as map of key names to key values
   * @throws ODataNotImplementedException the o data not implemented exception
   * @throws ODataNotFoundException the o data not found exception
   * @throws EdmException the edm exception
   * @throws ODataApplicationException the o data application exception
   */
  void deleteData(EdmEntitySet entitySet, Map<String, Object> keys) throws ODataNotImplementedException,
      ODataNotFoundException, EdmException, ODataApplicationException;

  /**
   * <p>Inserts an instance into the entity list of the specified entity set.</p>
   * <p>If {@link #newDataObject} has not set the key and other mandatory
   * properties already, this method must set them before inserting the
   * instance into the list.</p>
   *
   * @param entitySet the EdmEntitySet the object must correspond to
   * @param data the data object of the new entity
   * @return the new created object instance (which is NOT the same object as the given data parameter object)
   * @throws ODataNotImplementedException the o data not implemented exception
   * @throws EdmException the edm exception
   * @throws ODataApplicationException the o data application exception
   */
  Object createData(EdmEntitySet entitySet, Object data) throws ODataNotImplementedException, EdmException,
      ODataApplicationException;

  /**
   * Deletes the relation from the specified source data to a target entity
   * specified by entity set and key.
   * @param sourceEntitySet the EdmEntitySet of the source entity
   * @param sourceData the data object of the source entity
   * @param targetEntitySet the EdmEntitySet of the target entity
   * @param targetKeys the key of the target entity as map of key names to key values (optional)
   * @throws ODataNotImplementedException the o data not implemented exception
   * @throws ODataNotFoundException the o data not found exception
   * @throws EdmException the edm exception
   * @throws ODataApplicationException the o data application exception
   */
  void deleteRelation(EdmEntitySet sourceEntitySet, Object sourceData, EdmEntitySet targetEntitySet,
      Map<String, Object> targetKeys) throws ODataNotImplementedException, ODataNotFoundException, EdmException,
      ODataApplicationException;

  /**
   * Writes a relation from the specified source data to a target entity
   * specified by entity set and key.
   * @param sourceEntitySet the EdmEntitySet of the source entity
   * @param sourceData the data object of the source entity
   * @param targetEntitySet the EdmEntitySet of the relation target
   * @param targetKeys the key of the target entity as map of key names to key values
   * @throws ODataNotImplementedException the o data not implemented exception
   * @throws ODataNotFoundException the o data not found exception
   * @throws EdmException the edm exception
   * @throws ODataApplicationException the o data application exception
   */
  void writeRelation(EdmEntitySet sourceEntitySet, Object sourceData, EdmEntitySet targetEntitySet,
      Map<String, Object> targetKeys) throws ODataNotImplementedException, ODataNotFoundException, EdmException,
      ODataApplicationException;

  /**
   * Container to store binary data (as byte array) and the associated MIME type.
   */
  class BinaryData {
    private final byte[] data;
    private final String mimeType;

    /**
     * Instantiates a new Binary data.
     *
     * @param data the data
     * @param mimeType the mime type
     */
    public BinaryData(final byte[] data, final String mimeType) {
      this.data = data;
      this.mimeType = mimeType;
    }

    /**
     * Get data.
     *
     * @return the byte [ ]
     */
    public byte[] getData() {
      return data;
    }

    /**
     * Gets mime type.
     *
     * @return the mime type
     */
    public String getMimeType() {
      return mimeType;
    }

    @Override
    public String toString() {
      return "data=" + Arrays.toString(data) + ", mimeType=" + mimeType;
    }
  }
} class BinaryData {
  private final byte[] data;
  private final String mimeType;

  /**
   * Instantiates a new Binary data.
   *
   * @param data the data
   * @param mimeType the mime type
   */
  public BinaryData(final byte[] data, final String mimeType) {
    this.data = data;
    this.mimeType = mimeType;
  }

  /**
   * Get data.
   *
   * @return the byte [ ]
   */
  public byte[] getData() {
    return data;
  }

  /**
   * Gets mime type.
   *
   * @return the mime type
   */
  public String getMimeType() {
    return mimeType;
  }

  @Override
  public String toString() {
    return "data=" + Arrays.toString(data) + ", mimeType=" + mimeType;
  }
} public interface DataStore<T> {

  /**
   * Get class of objects which can be stored by this DataStore instance.
   *
   * @return class of objects which can be stored by this DataStore instance.
   */
  Class<T> getDataTypeClass();

  /**
   * Get name of this DataStore instance.
   *
   * @return name of this DataStore instance.
   */
  String getName();

  /**
   * Create a new object instance of the object type which can be stored by this DataStore instance.
   *
   * @return new object instance
   */
  T createInstance();

  /**
   * Store given object as new object in the DataStore
   *
   * @param object new object to be stored
   * @return the stored object
   * @throws DataStoreException the data store exception
   */
  T create(final T object) throws DataStoreException;

  /**
   * Read object which is key equal to given object (based on #isKeyEqualChecked method).
   * If no according object can be found <code>null</code> is returned
   *
   * @param object object with key fields set
   * @return according object or <code>null</code>
   */
  T read(final T object) throws DataStoreException;

  /**
   * Read all object of this DataStore.
   *
   * @return all object of this DataStore.
   */
  Collection<T> read() throws DataStoreException;

  /**
   * Read read result.
   *
   * @param readOptions the read options
   * @return the read result
   */
  ReadResult<T> read(ReadOptions readOptions) throws DataStoreException;

  /**
   * Update object which is key equal to given object (based on #isKeyEqualChecked method).
   * If no according object can be found <code>null</code> is returned and nothing is updated.
   *
   * @param object object with key fields set
   * @return according object or <code>null</code>
   */
  T update(final T object) throws DataStoreException;

  /**
   * Delete object which is key equal to given object (based on #isKeyEqualChecked method).
   * If no according object can be found <code>null</code> is returned and nothing is updated.
   *
   * @param object object with key fields set
   * @return according object or <code>null</code>
   */
  T delete(final T object) throws DataStoreException;

  /**
   * Are the key values equal for both instances.
   * If all compared key values are <code>null</code> this also means equal.
   * Before object (keys) are compared it is validated that both object instance are NOT null
   * and that both are from the same class as this {@link DataStore} (see {@link #getDataTypeClass()}).
   *
   * @param first first instance to check for key equal
   * @param second second instance to check for key equal
   * @return  <code>true</code> if object instance have equal keys set.
   * @throws DataStoreException the data store exception
   */
  boolean isKeyEqualChecked(Object first, Object second) throws DataStoreException;

} public interface DataStoreManager {
  /**
   * The KEEP _ pERSISTENT.
   */
  String KEEP_PERSISTENT = "KEEP_PERSISTENT";

  /**
   * Set default properties which are used for all methods without explicit properties parameter.
   *
   * @param name name of property
   * @param value value of property
   */
  void setDefaultProperty(String name, String value);

  /**
   * Create a DataStore without managing (keeping a reference to it).
   *
   * @param <T>  type of class for which the DataStore is created
   * @param clz class for which the DataStore is created
   * @return the created (and not managed) DataStore
   * @throws DataStoreException the data store exception
   */
  <T> DataStore<T> createDataStore(Class<T> clz) throws DataStoreException;

  /**
   * Create a DataStore without managing (keeping a reference to it).
   *
   * @param <T>  type of class for which the DataStore is created
   * @param clz class for which the DataStore is created
   * @param properties properties for creation
   * @return the created (and not managed) DataStore
   * @throws DataStoreException if something goes wrong
   */
  <T> DataStore<T> createDataStore(Class<T> clz, Map<String, String> properties) throws DataStoreException;

  /**
   * Create a DataStore and managing it (keeping a reference to it).
   * If a DataStore with given name already exists this will be returned.
   *
   * @param <T>  type of class for which the DataStore is created
   * @param name unique name for the DataStore
   * @param clz class for which the DataStore is created
   * @return the created (and managed) DataStore
   * @throws DataStoreException if something goes wrong
   */
  <T> DataStore<T> grantDataStore(String name, Class<T> clz) throws DataStoreException;

  /**
   * Create a DataStore and managing it (keeping a reference to it).
   * If a DataStore with given name already exists this will be returned.
   *
   * @param <T>  type of class for which the DataStore is created
   * @param name unique name for the DataStore
   * @param clz class for which the DataStore is created
   * @param properties properties for creation
   * @return the created (and managed) DataStore
   * @throws DataStoreException if something goes wrong
   */
  <T> DataStore<T> grantDataStore(String name, Class<T> clz, Map<String, String> properties) throws DataStoreException;

  /**
   * Get a reference to an already existing (managed) DataStore.
   *
   * @param name name of the DataStore which is requested
   * @param clz class for which the DataStore is created
   * @return the created (and managed) DataStore or <code>NULL</code> if no according DataStore exists
   * @throws DataStoreException if something goes wrong
   */
  <T> DataStore<T> getDataStore(String name, Class<T> clz) throws DataStoreException;

  /**
   * Get a reference to an already existing (managed) DataStore.
   *
   * @param name name of the DataStore which is requested
   * @return the created (and managed) DataStore or <code>NULL</code> if no according DataStore exists
   */
  DataStore<Object> getDataStore(String name);
} public class AnnotationDataSource implements DataSource {

  private static final AnnotationHelper ANNOTATION_HELPER = new AnnotationHelper();

  private final DataStoreManager dataStoreManager;

  public AnnotationDataSource(final Collection<Class<?>> annotatedClasses, final DataStoreManager dataStoreManager)
      throws ODataException {
    this.dataStoreManager = dataStoreManager;
    
    init(annotatedClasses);
  }

  public AnnotationDataSource(final String packageToScan, final DataStoreManager dataStoreManager)
          throws ODataException {
    this.dataStoreManager = dataStoreManager;

    List<Class<?>> foundClasses = ClassHelper.loadClasses(packageToScan, ANNOTATION_HELPER::isEdmAnnotated);

    init(foundClasses);
  }

  @SuppressWarnings("unchecked")
  private void init(final Collection<Class<?>> annotatedClasses) throws ODataException {
    try {
      for (Class<?> clz : annotatedClasses) {
        String entitySetName = ANNOTATION_HELPER.extractEntitySetName(clz);
        if(entitySetName != null) {
          dataStoreManager.grantDataStore(entitySetName, clz);
        } else if (!ANNOTATION_HELPER.isEdmAnnotated(clz)) {
          throw new ODataException("Found not annotated class during DataStore initialization of type: "
              + clz.getName());
        }
      }
    } catch (DataStoreException e) {
      throw new ODataException("Error in DataStore initialization with message: " + e.getMessage(), e);
    }
  }

  @SuppressWarnings("unchecked")
  public <T> DataStore<T> getDataStore(final Class<T> clazz) throws DataStoreException {
    String entitySetName = ANNOTATION_HELPER.extractEntitySetName(clazz);
    return dataStoreManager.getDataStore(entitySetName, clazz);
  }

  @Override
  public ReadResult<?> readData(final EdmEntitySet entitySet, ReadOptions readOptions) throws ODataNotImplementedException,
      ODataNotFoundException, EdmException, ODataApplicationException {

    DataStore<Object> holder = getDataStore(entitySet);
    if (holder != null) {
      return holder.read(readOptions);
    }

    throw new ODataNotFoundException(ODataNotFoundException.ENTITY);
  }

  @Override
  public Object readData(final EdmEntitySet entitySet, final Map<String, Object> keys)
      throws ODataNotFoundException, EdmException, ODataApplicationException {

    DataStore<Object> store = getDataStore(entitySet);
    if (store != null) {
      Object keyInstance = store.createInstance();
      ANNOTATION_HELPER.setKeyFields(keyInstance, keys);

      Object result = store.read(keyInstance);
      if (result != null) {
        return result;
      }
    }

    throw new ODataNotFoundException(ODataNotFoundException.ENTITY);
  }

  @Override
  public Object readRelatedData(final EdmEntitySet sourceEntitySet, final Object sourceData,
      final EdmEntitySet targetEntitySet,
      final Map<String, Object> targetKeys)
      throws ODataNotImplementedException, ODataNotFoundException, EdmException, ODataApplicationException {

    DataStore<?> sourceStore = dataStoreManager.getDataStore(sourceEntitySet.getName());
    DataStore<?> targetStore = dataStoreManager.getDataStore(targetEntitySet.getName());

    AnnotationHelper.AnnotatedNavInfo navInfo = ANNOTATION_HELPER.getCommonNavigationInfo(
        sourceStore.getDataTypeClass(), targetStore.getDataTypeClass());
    final Field sourceField;
    if(navInfo.isBiDirectional()) {
      sourceField = navInfo.getToField();
    } else {
      sourceField = navInfo.getFromField();
    }
    if (sourceField == null) {
      throw new AnnotationRuntimeException("Missing source field for related data (sourceStore='" + sourceStore
          + "', targetStore='" + targetStore + "').");
    }

    List<Object> resultData = readResultData(targetStore, sourceData, sourceField, navInfo);
    return extractResultData(targetStore, targetKeys, navInfo, resultData);
  }

  /**
   * Read the result data from the target store based on <code>sourceData</code> and <code>sourceField</code>
   * 
   * @param targetStore
   * @param sourceData
   * @param sourceField
   * @return
   * @throws DataStoreException
   */
  private List<Object> readResultData(final DataStore<?> targetStore, final Object sourceData, 
          final Field sourceField, final AnnotationHelper.AnnotatedNavInfo navInfo)
      throws DataStoreException {
    Object navigationInstance = getValue(sourceField, sourceData);
    if (navigationInstance == null) {
      return Collections.emptyList();
    }
    
    List<Object> resultData = new ArrayList<>();
    for (Object targetInstance : targetStore.read()) {
      if (navigationInstance instanceof Collection) {
        Map<String, Object> keyName2Value = 
                ANNOTATION_HELPER.getValueForAnnotatedFields(sourceData, EdmKey.class);
        Field toField = navInfo.getToField();
        Object backInstance = ClassHelper.getFieldValue(targetInstance, toField);
        boolean keyMatch = ANNOTATION_HELPER.keyMatch(backInstance, keyName2Value);
        if(keyMatch) {
          resultData.add(targetInstance);
        }
      } else if (targetStore.isKeyEqualChecked(targetInstance, navigationInstance)) {
        resultData.add(targetInstance);
      }
    }
    return resultData;
  }

  /**
   * Extract the <code>result data</code> from the <code>resultData</code> list based on
   * <code>navigation information</code> and <code>targetKeys</code>.
   * 
   * @param targetStore
   * @param targetKeys
   * @param navInfo
   * @param resultData
   * @return
   * @throws DataStoreException
   */
  private Object extractResultData(final DataStore<?> targetStore, final Map<String, Object> targetKeys,
      final AnnotationHelper.AnnotatedNavInfo navInfo, final List<Object> resultData) throws DataStoreException {
    if (navInfo.getToMultiplicity() == EdmMultiplicity.MANY) {
      if (targetKeys.isEmpty()) {
        return resultData;
      } else {
        Object keyInstance = targetStore.createInstance();
        ANNOTATION_HELPER.setKeyFields(keyInstance, targetKeys);
        for (Object result : resultData) {
          if (targetStore.isKeyEqualChecked(result, keyInstance)) {
            return result;
          }
        }
        return null;
      }
    } else {
      if (resultData.isEmpty()) {
        return null;
      }
      return resultData.get(0);
    }
  }

  @Override
  public BinaryData readBinaryData(final EdmEntitySet entitySet, final Object mediaLinkEntryData)
      throws ODataNotImplementedException, ODataNotFoundException, EdmException, ODataApplicationException {

    Object data = ANNOTATION_HELPER.getValueForField(mediaLinkEntryData, EdmMediaResourceContent.class);
    Object mimeType = ANNOTATION_HELPER.getValueForField(mediaLinkEntryData, EdmMediaResourceMimeType.class);

    if (data == null && mimeType == null) {
      DataStore<Object> dataStore = getDataStore(entitySet);
      Object readEntry = dataStore.read(mediaLinkEntryData);
      if (readEntry != null) {
        data = ANNOTATION_HELPER.getValueForField(readEntry, EdmMediaResourceContent.class);
        mimeType = ANNOTATION_HELPER.getValueForField(readEntry, EdmMediaResourceMimeType.class);
      }
    }

    return new BinaryData((byte[]) data, String.valueOf(mimeType));
  }

  @Override
  public Object newDataObject(final EdmEntitySet entitySet)
      throws ODataNotImplementedException, EdmException, ODataApplicationException {

    DataStore<Object> dataStore = getDataStore(entitySet);
    if (dataStore != null) {
      return dataStore.createInstance();
    }

    throw new AnnotationRuntimeException("No DataStore found for entitySet with name: " + entitySet.getName());
  }

  @Override
  public void writeBinaryData(final EdmEntitySet entitySet, final Object mediaEntityInstance,
      final BinaryData binaryData)
      throws ODataNotImplementedException, ODataNotFoundException, EdmException, ODataApplicationException {

    try {
      DataStore<Object> dataStore = getDataStore(entitySet);
      Object readEntry = dataStore.read(mediaEntityInstance);
      if (readEntry == null) {
        throw new ODataNotFoundException(ODataNotFoundException.ENTITY);
      } else {
        ANNOTATION_HELPER.setValueForAnnotatedField(
            mediaEntityInstance, EdmMediaResourceContent.class, binaryData.getData());
        ANNOTATION_HELPER.setValueForAnnotatedField(
            mediaEntityInstance, EdmMediaResourceMimeType.class, binaryData.getMimeType());
      }
    } catch (AnnotationHelper.ODataAnnotationException e) {
      throw new AnnotationRuntimeException("Invalid media resource annotation at entity set '" + entitySet.getName()
          + "' with message '" + e.getMessage() + "'.", e);
    }
  }

  /**
   * <p>Updates a single data object identified by the specified entity set and key fields of
   * the data object.</p>
   * @param entitySet the {@link EdmEntitySet} the object must correspond to
   * @param data the data object of the new entity
   * @return updated data object instance
   * @throws org.apache.olingo.odata2.api.exception.ODataNotImplementedException
   * @throws org.apache.olingo.odata2.api.edm.EdmException
   * @throws org.apache.olingo.odata2.api.exception.ODataApplicationException
   */
  public Object updateData(final EdmEntitySet entitySet, final Object data)
      throws EdmException, ODataApplicationException {

    DataStore<Object> dataStore = getDataStore(entitySet);
    return dataStore.update(data);
  }

  @Override
  public void deleteData(final EdmEntitySet entitySet, final Map<String, Object> keys)
      throws ODataNotImplementedException, ODataNotFoundException, EdmException, ODataApplicationException {
    DataStore<Object> dataStore = getDataStore(entitySet);
    Object keyInstance = dataStore.createInstance();
    ANNOTATION_HELPER.setKeyFields(keyInstance, keys);
    dataStore.delete(keyInstance);
  }

  @Override
  public Object createData(final EdmEntitySet entitySet, final Object data)
      throws ODataNotImplementedException, EdmException, ODataApplicationException {

    DataStore<Object> dataStore = getDataStore(entitySet);
    return dataStore.create(data);
  }

  @Override
  public void deleteRelation(final EdmEntitySet sourceEntitySet, final Object sourceData,
      final EdmEntitySet targetEntitySet,
      final Map<String, Object> targetKeys)
      throws ODataNotImplementedException, ODataNotFoundException, EdmException, ODataApplicationException {
    throw new ODataNotImplementedException(ODataNotImplementedException.COMMON);
  }

  @Override
  public void writeRelation(final EdmEntitySet sourceEntitySet, final Object sourceEntity,
      final EdmEntitySet targetEntitySet,
      final Map<String, Object> targetEntityValues)
      throws ODataNotImplementedException, ODataNotFoundException, EdmException, ODataApplicationException {
    // get common data
    DataStore<Object> sourceStore = dataStoreManager.getDataStore(sourceEntitySet.getName());
    DataStore<Object> targetStore = dataStoreManager.getDataStore(targetEntitySet.getName());

    AnnotationHelper.AnnotatedNavInfo commonNavInfo = ANNOTATION_HELPER.getCommonNavigationInfo(
        sourceStore.getDataTypeClass(), targetStore.getDataTypeClass());

    // get and validate source fields
    Field sourceField = commonNavInfo.getFromField();
    if (sourceField == null) {
      throw new AnnotationRuntimeException("Missing source field for related data (sourceStore='" + sourceStore
          + "', targetStore='" + targetStore + "').");
    }

    // get related target entity
    Object targetEntity = targetStore.createInstance();
    ANNOTATION_HELPER.setKeyFields(targetEntity, targetEntityValues);
    targetEntity = targetStore.read(targetEntity);

    // set at source
    setValueAtNavigationField(sourceEntity, sourceField, targetEntity);
    // set at target
    Field targetField = commonNavInfo.getToField();
    if (targetField != null) {
      setValueAtNavigationField(targetEntity, targetField, sourceEntity);
    }
  }

  /**
   * Set (Multiplicity != *) or add (Multiplicity == *) <code>value</code> at <code>field</code>
   * of <code>instance</code>.
   * 
   * @param instance
   * @param field
   * @param value
   * @throws EdmException
   */
  private void setValueAtNavigationField(final Object instance, final Field field, final Object value) {
    Class<?> fieldTypeClass = field.getType();
    if (Collection.class.isAssignableFrom(fieldTypeClass)) {
      @SuppressWarnings("unchecked")
      Collection<Object> collection = (Collection<Object>) ANNOTATION_HELPER.getValueForField(
          instance, field.getName(), EdmNavigationProperty.class);
      if (collection == null) {
        collection = new ArrayList<>();
        setValue(instance, field, collection);
      }
      collection.add(value);
    } else if (fieldTypeClass.isArray()) {
      throw new AnnotationRuntimeException("Write relations for internal used arrays is not supported.");
    } else {
      setValue(instance, field, value);
    }
  }

  /**
   * Returns corresponding DataStore for EdmEntitySet or if no data store is registered an
   * AnnotationRuntimeException is thrown.
   * Never returns NULL.
   * 
   * @param entitySet for which the corresponding DataStore is returned
   * @return a DataStore object
   * @throws EdmException
   * @throws AnnotationRuntimeException if no DataStore is found
   */
  private DataStore<Object> getDataStore(final EdmEntitySet entitySet) throws EdmException {
    final String name = entitySet.getName();
    DataStore<Object> dataStore = dataStoreManager.getDataStore(name);
    if (dataStore == null) {
      throw new AnnotationRuntimeException("No DataStore found for entity set '" + entitySet + "'.");
    }
    return dataStore;
  }

  private Object getValue(final Field field, final Object instance) {
    try {
      boolean access = field.isAccessible();
      field.setAccessible(true);
      Object value = field.get(instance);
      field.setAccessible(access);
      return value;
    } catch (IllegalArgumentException | IllegalAccessException e) {
      throw new AnnotationRuntimeException("Error for getting value of field '"
          + field + "' at instance '" + instance + "'.", e);
    }
  }

  private void setValue(final Object instance, final Field field, final Object value) {
    try {
      boolean access = field.isAccessible();
      field.setAccessible(true);
      field.set(instance, value);
      field.setAccessible(access);
    } catch (IllegalArgumentException | IllegalAccessException e) {
      throw new AnnotationRuntimeException("Error for setting value of field: '"
          + field + "' at instance: '" + instance + "'.", e);
    }
  }
} public class AnnotationEdmProvider extends EdmProvider {

  private static final AnnotationHelper ANNOTATION_HELPER = new AnnotationHelper();

  private final List<Class<?>> annotatedClasses;
  private final Map<String, EntityContainer> name2Container = new HashMap<>();
  private final Map<String, ContainerBuilder> containerName2ContainerBuilder = new HashMap<>();
  private final Map<String, Schema> namespace2Schema = new HashMap<>();
  private EntityContainer defaultContainer;

  /**
   * Create an <code>AnnotationEdmProvider</code> based on given annotated classes.
   *
   * @param annotatedClasses all classes which are annotated and are used as Edm
   */
  public AnnotationEdmProvider(final Collection<Class<?>> annotatedClasses) throws ODataException {

    this.annotatedClasses = new ArrayList<>(annotatedClasses.size());
    this.annotatedClasses.addAll(annotatedClasses.stream().filter(
        ANNOTATION_HELPER::isEdmAnnotated).collect(Collectors.toList()));

    init();
  }

  /**
   * Create an <code>AnnotationEdmProvider</code> based on annotated classes in given package (and sub-packages).
   *
   * @param packageToScan package (and sub-packages) which are scanned for annotated classes
   */
  public AnnotationEdmProvider(final String packageToScan) throws ODataException {
//    annotatedClasses = ClassHelper.loadClasses(packageToScan, new ClassHelper.ClassValidator() {
//      @Override
//      public boolean isClassValid(final Class<?> c) {
//        return ANNOTATION_HELPER.isEdmAnnotated(c);
//      }
//    });
    annotatedClasses = ClassHelper.loadClasses(packageToScan, ANNOTATION_HELPER::isEdmAnnotated);

    init();
  }

  private void init() throws ODataException {
    for (Class<?> aClass : annotatedClasses) {
      updateSchema(aClass);
      handleEntityContainer(aClass);
    }

    finish();
  }

  @Override
  public Association getAssociation(final FullQualifiedName edmFQName) throws ODataException {
    Schema schema = namespace2Schema.get(edmFQName.getNamespace());
    if (schema != null) {
      List<Association> associations = schema.getAssociations();
      for (Association association : associations) {
        if (association.getName().equals(edmFQName.getName())) {
          return association;
        }
      }
    }
    return null;
  }

  @Override
  public AssociationSet getAssociationSet(final String entityContainer, final FullQualifiedName association,
      final String sourceEntitySetName, final String sourceEntitySetRole) throws ODataException {
    EntityContainer container = name2Container.get(entityContainer);
    if (container != null) {
      List<AssociationSet> associations = container.getAssociationSets();
      for (AssociationSet associationSet : associations) {
        if (associationSet.getAssociation().equals(association)) {
          final AssociationSetEnd endOne = associationSet.getEnd1();
          if (endOne.getRole().equals(sourceEntitySetRole)
              && endOne.getEntitySet().equals(sourceEntitySetName)) {
            return associationSet;
          }
          final AssociationSetEnd endTwo = associationSet.getEnd2();
          if (endTwo.getRole().equals(sourceEntitySetRole)
              && endTwo.getEntitySet().equals(sourceEntitySetName)) {
            return associationSet;
          }
        }
      }
    }
    return null;
  }

  @Override
  public ComplexType getComplexType(final FullQualifiedName edmFQName) throws ODataException {
    Schema schema = namespace2Schema.get(edmFQName.getNamespace());
    if (schema != null) {
      List<ComplexType> complexTypes = schema.getComplexTypes();
      for (ComplexType complexType : complexTypes) {
        if (complexType.getName().equals(edmFQName.getName())) {
          return complexType;
        }
      }
    }
    return null;
  }

  @Override
  public EntityContainerInfo getEntityContainerInfo(final String name) throws ODataException {
    EntityContainer container = name2Container.get(name);
    if (container == null) {
      // use default container (if set)
      container = defaultContainer;
    }
    if (container != null) {
      EntityContainerInfo info = new EntityContainerInfo();
      info.setName(container.getName());
      info.setDefaultEntityContainer(container.isDefaultEntityContainer());
      info.setExtendz(container.getExtendz());
      info.setAnnotationAttributes(container.getAnnotationAttributes());
      info.setAnnotationElements(container.getAnnotationElements());

      return info;
    }

    return null;
  }

  @Override
  public EntitySet getEntitySet(final String entityContainer, final String name) throws ODataException {
    EntityContainer container = name2Container.get(entityContainer);
    if (container != null) {
      List<EntitySet> entitySets = container.getEntitySets();
      for (EntitySet entitySet : entitySets) {
        if (entitySet.getName().equals(name)) {
          return entitySet;
        }
      }
    }

    return null;
  }

  @Override
  public EntityType getEntityType(final FullQualifiedName edmFQName) throws ODataException {
    Schema schema = namespace2Schema.get(edmFQName.getNamespace());
    if (schema != null) {
      List<EntityType> complexTypes = schema.getEntityTypes();
      for (EntityType complexType : complexTypes) {
        if (complexType.getName().equals(edmFQName.getName())) {
          return complexType;
        }
      }
    }
    return null;
  }

  @Override
  public FunctionImport getFunctionImport(final String entityContainer, final String name) throws ODataException {
    EntityContainer container = name2Container.get(entityContainer);
    if (container != null) {
      List<FunctionImport> functionImports = container.getFunctionImports();
      for (FunctionImport functionImport : functionImports) {
        if (functionImport.getName().equals(name)) {
          return functionImport;
        }
      }
    }
    return null;
  }

  @Override
  public List<Schema> getSchemas() throws ODataException {
    return new ArrayList<>(namespace2Schema.values());
  }

  //
  //
  //
  private Map<String, SchemaBuilder> namespace2SchemaBuilder = new HashMap<>();

  private void updateSchema(final Class<?> aClass) {
    EdmEntityType et = aClass.getAnnotation(EdmEntityType.class);
    if (et != null) {
      updateSchema(aClass, et);
    }
    EdmComplexType ect = aClass.getAnnotation(EdmComplexType.class);
    if (ect != null) {
      updateSchema(aClass, ect);
    }
  }

  private void updateSchema(final Class<?> aClass, final EdmEntityType et) {
    SchemaBuilder b = getSchemaBuilder(et.namespace(), aClass);
    TypeBuilder typeBuilder = TypeBuilder.init(et, aClass);
    b.addEntityType(typeBuilder.buildEntityType());
    b.addAssociations(typeBuilder.buildAssociations());
  }

  private SchemaBuilder getSchemaBuilder(final String namespace, final Class<?> aClass) {
    String usedNamespace = namespace;
    if (usedNamespace.isEmpty()) {
      usedNamespace = ANNOTATION_HELPER.getCanonicalNamespace(aClass);
    }
    SchemaBuilder builder = namespace2SchemaBuilder.get(usedNamespace);
    if (builder == null) {
      builder = SchemaBuilder.init(usedNamespace);
      namespace2SchemaBuilder.put(usedNamespace, builder);
    }
    return builder;
  }

  private void updateSchema(final Class<?> aClass, final EdmComplexType et) {
    SchemaBuilder b = getSchemaBuilder(et.namespace(), aClass);
    TypeBuilder typeBuilder = TypeBuilder.init(et, aClass);
    b.addComplexType(typeBuilder.buildComplexType());
  }

  private void handleEntityContainer(final Class<?> aClass) {
    EdmEntityType entityType = aClass.getAnnotation(EdmEntityType.class);
    if (entityType != null) {
      FullQualifiedName typeName = createFqnForEntityType(aClass);
      String containerName = ANNOTATION_HELPER.extractContainerName(aClass);
      ContainerBuilder builder = containerName2ContainerBuilder.get(containerName);
      if (builder == null) {
        builder = ContainerBuilder.init(typeName.getNamespace(), containerName);
        containerName2ContainerBuilder.put(containerName, builder);
      }
      EdmEntitySet entitySet = aClass.getAnnotation(EdmEntitySet.class);
      if (entitySet != null) {
        builder.addEntitySet(createEntitySet(typeName, aClass));
      }
    }

    Method[] methods = aClass.getMethods();
    for (Method method : methods) {
      EdmFunctionImport functionImport = method.getAnnotation(EdmFunctionImport.class);
      if (functionImport != null) {
        String containerName = ANNOTATION_HELPER.extractContainerName(aClass);
        ContainerBuilder builder = containerName2ContainerBuilder.get(containerName);
        if (builder == null) {
          String namespace = ANNOTATION_HELPER.generateNamespace(aClass);
          builder = ContainerBuilder.init(namespace, containerName);
          containerName2ContainerBuilder.put(containerName, builder);
        }
        builder.addFunctionImport(createFunctionImport(method));
      }
    }
  }

  private FunctionImport createFunctionImport(final Method method) {
    String entitySetName = ANNOTATION_HELPER.extractEntitySetName(method);
    String httpMethod = ANNOTATION_HELPER.extractHttpMethod(method);
    String functionImportName = ANNOTATION_HELPER.extractFunctionImportName(method);
    ArrayList<FunctionImportParameter> functionImportParameters = ANNOTATION_HELPER.extractFunctionImportParameters(method);
    ReturnType returnType = ANNOTATION_HELPER.extractReturnType(method);

    FunctionImport fi = new FunctionImport();
    // Data which is not available though it should be?
    //    fi.setAnnotationAttributes(annotationAttributes);
    //    fi.setAnnotationElements(annotationElements);
    //    fi.setDocumentation(documentation);
    if (entitySetName != null && !entitySetName.isEmpty()) {
      fi.setEntitySet(entitySetName);
    }
    if (httpMethod != null && !httpMethod.isEmpty()) {
      fi.setHttpMethod(httpMethod);
    }
    // Data which is not available though it should be?
    //    fi.setMapping(mapping);
    fi.setName(functionImportName);
    if (functionImportParameters != null && !functionImportParameters.isEmpty()) {
      fi.setParameters(functionImportParameters);
    }
    if (returnType != null) {
      fi.setReturnType(returnType);
    }

    return fi;
  }

  private EntitySet createEntitySet(final FullQualifiedName typeName, final Class<?> entitySetClass) {
    String entitySetName = ANNOTATION_HELPER.extractEntitySetName(entitySetClass);
    return new EntitySet().setName(entitySetName).setEntityType(typeName);
  }

  private FullQualifiedName createFqnForEntityType(final Class<?> annotatedClass) {
    return ANNOTATION_HELPER.extractEntityTypeFqn(annotatedClass);
  }

  private void finish() throws ODataException {
    //
    Collection<ContainerBuilder> containers = containerName2ContainerBuilder.values();
    for (ContainerBuilder containerBuilder : containers) {
      SchemaBuilder schemaBuilder = namespace2SchemaBuilder.get(containerBuilder.getNamespace());
      containerBuilder.addAssociationSets(schemaBuilder.name2Associations.values());
      final EntityContainer container = containerBuilder.build();
      schemaBuilder.addEntityContainer(container);
      name2Container.put(container.getName(), container);
      if (container.isDefaultEntityContainer()) {
        defaultContainer = container;
      }
    }
    //
    Collection<SchemaBuilder> schemaBuilders = namespace2SchemaBuilder.values();
    for (SchemaBuilder schemaBuilder : schemaBuilders) {
      final Schema schema = schemaBuilder.build();
      namespace2Schema.put(schema.getNamespace(), schema);
    }
  }

  //
  //
  //
  static class TypeBuilder {

    @EdmProperty
    final private String namespace;
    final private String name;
    private boolean isAbstract = false;
    private boolean isMediaResource = false;
    private FullQualifiedName baseEntityType = null;
    private final List<PropertyRef> keyProperties = new ArrayList<>();
    private final List<Property> properties = new ArrayList<>();
    private final List<NavigationProperty> navProperties = new ArrayList<>();
    private final List<Association> associations = new ArrayList<>();

    public TypeBuilder(final FullQualifiedName fqn) {
      namespace = fqn.getNamespace();
      name = fqn.getName();
    }

    public static TypeBuilder init(final EdmEntityType entity, final Class<?> aClass) {
      return new TypeBuilder(ANNOTATION_HELPER.extractEntityTypeFqn(entity, aClass)).withClass(aClass);
    }

    public static TypeBuilder init(final EdmComplexType entity, final Class<?> aClass) {
      return new TypeBuilder(ANNOTATION_HELPER.extractComplexTypeFqn(entity, aClass)).withClass(aClass);
    }

    private TypeBuilder withClass(final Class<?> aClass) {
      baseEntityType = createBaseEntityFqn(aClass);

      if (Modifier.isAbstract(aClass.getModifiers())) {
        isAbstract = true;
      }

      Field[] fields = aClass.getDeclaredFields();
      for (Field field : fields) {
        EdmNavigationProperty enp = field.getAnnotation(EdmNavigationProperty.class);
        if (enp != null) {
          Class<?> fromClass = field.getDeclaringClass();
          Class<?> toClass = ClassHelper.getFieldType(field);
          AnnotationHelper.AnnotatedNavInfo info = ANNOTATION_HELPER.getCommonNavigationInfo(fromClass, toClass);

          final NavigationProperty navProperty = createNavigationProperty(namespace, field, info);
          navProperties.add(navProperty);
          Association association = createAssociation(info);
          associations.add(association);
          // continue after a navigation property is found
          continue;
        }

        EdmMediaResourceContent emrc = field.getAnnotation(EdmMediaResourceContent.class);
        if (emrc != null) {
          isMediaResource = true;
        }
        EdmProperty ep = field.getAnnotation(EdmProperty.class);
        EdmKey eti = field.getAnnotation(EdmKey.class);
        int annotationsCount = field.getAnnotations().length;
        if(ep == null && eti == null && annotationsCount > 0) {
          // skip because this field is annotated but not as property (or key)
          continue;
        } else if (ep == null) {
          ep = getDefaultEdmProperty();
        }
        properties.add(createProperty(ep, field, namespace));
        if (eti != null) {
          keyProperties.add(createKeyProperty(ep, field));
        }
      }

      return this;
    }

    private EdmProperty getDefaultEdmProperty() {
      try {
        return TypeBuilder.class.getDeclaredField("namespace").getAnnotation(EdmProperty.class);
      } catch (NoSuchFieldException e) {
        // can be ignored because we known that this field is there
        throw new RuntimeException(e);
      }
    }

    public TypeBuilder addProperty(final PropertyRef property) {
      keyProperties.add(property);
      return this;
    }

    public TypeBuilder addProperty(final Property property) {
      properties.add(property);
      return this;
    }

    public TypeBuilder addNavigationProperty(final NavigationProperty property) {
      navProperties.add(property);
      return this;
    }

    public TypeBuilder setAbstract(final boolean isAbstract) {
      this.isAbstract = isAbstract;
      return this;
    }

    public ComplexType buildComplexType() {
      ComplexType complexType = new ComplexType();
      if (baseEntityType != null) {
        complexType.setBaseType(baseEntityType);
      }
      return complexType.setName(name).setProperties(properties);
    }

    public EntityType buildEntityType() {
      EntityType entityType = new EntityType();
      if (baseEntityType != null) {
        entityType.setBaseType(baseEntityType);
      }
      if (!keyProperties.isEmpty()) {
        entityType.setKey(new Key().setKeys(keyProperties));
      }
      if (!navProperties.isEmpty()) {
        entityType.setNavigationProperties(navProperties);
      }
      return entityType.setName(name)
          .setAbstract(isAbstract)
          .setHasStream(isMediaResource)
          .setProperties(properties);
    }

    public Collection<Association> buildAssociations() {
      return Collections.unmodifiableCollection(associations);
    }

    private PropertyRef createKeyProperty(final EdmProperty et, final Field field) {
      PropertyRef keyProperty = new PropertyRef();
      String entityName = et.name();
      if (entityName.isEmpty()) {
        entityName = getCanonicalName(field);
      }
      return keyProperty.setName(entityName);
    }

    private Property createProperty(final EdmProperty ep, final Field field, final String defaultNamespace) {
      if (isAnnotatedEntity(field.getType())) {
        return createComplexProperty(field, defaultNamespace);
      } else {
        return createSimpleProperty(ep, field);
      }
    }

    private Property createSimpleProperty(final EdmProperty ep, final Field field) {
      SimpleProperty sp = new SimpleProperty();
      String entityName = ANNOTATION_HELPER.getPropertyName(field);
      sp.setName(entityName);
      //
      EdmType type = ep.type();
      if (type == EdmType.NULL) {
        type = ANNOTATION_HELPER.mapType(field.getType());
      }
      sp.setType(ANNOTATION_HELPER.mapTypeKind(type));
      sp.setFacets(createFacets(ep.facets(), field.getAnnotation(EdmConcurrencyControl.class)));
      return sp;
    }

    private Facets createFacets(final EdmFacets facets, final EdmConcurrencyControl concurrencyControl) {
      Facets resultFacets = new Facets().setNullable(facets.nullable());
      if(facets.maxLength() > -1) {
        resultFacets.setMaxLength(facets.maxLength());
      }
      if(facets.precision() > -1) {
        resultFacets.setPrecision(facets.precision());
      }
      if(facets.scale() > -1) {
        resultFacets.setScale(facets.scale());
      }
      if (concurrencyControl != null) {
        resultFacets.setConcurrencyMode(EdmConcurrencyMode.Fixed);
      }
      return resultFacets;
    }

    private Property createComplexProperty(final Field field, final String defaultNamespace) {
      ComplexProperty cp = new ComplexProperty();
      // settings from property
      String entityName = ANNOTATION_HELPER.getPropertyName(field);
      cp.setName(entityName);

      // settings from related complex entity
      EdmComplexType ece = field.getType().getAnnotation(EdmComplexType.class);
      String complexEntityNamespace = ece.namespace();
      if (complexEntityNamespace.isEmpty()) {
        complexEntityNamespace = defaultNamespace;
      }
      cp.setType(new FullQualifiedName(complexEntityNamespace, ANNOTATION_HELPER.extractComplexTypeName(field.getType())))  ;
      return cp;
    }

    private NavigationProperty createNavigationProperty(final String namespace, Field field,
                                                        AnnotationHelper.AnnotatedNavInfo navInfo) {
      NavigationProperty navProp = new NavigationProperty();
      navProp.setName(ANNOTATION_HELPER.getPropertyName(field));
      String fromRole = navInfo.getFromRoleName();
      navProp.setFromRole(fromRole);
      navProp.setToRole(navInfo.getToRoleName());

      String relationshipName = navInfo.getRelationshipName();
      navProp.setRelationship(new FullQualifiedName(namespace, relationshipName));

      return navProp;
    }

//    private EdmType getEdmType(final Class<?> type) {
//      if (type == String.class) {
//        return EdmType.STRING;
//      } else if (type == boolean.class || type == Boolean.class) {
//        return EdmType.BOOLEAN;
//      } else if (type == byte.class || type == Byte.class) {
//        return EdmType.SBYTE;
//      } else if (type == short.class || type == Short.class) {
//        return EdmType.INT16;
//      } else if (type == int.class || type == Integer.class) {
//        return EdmType.INT32;
//      } else if (type == long.class || type == Long.class) {
//        return EdmType.INT64;
//      } else if (type == double.class || type == Double.class) {
//        return EdmType.DOUBLE;
//      } else if (type == float.class || type == Float.class) {
//        return EdmType.SINGLE;
//      } else if (type == BigInteger.class || type == BigDecimal.class) {
//        return EdmType.DECIMAL;
//      } else if (type == Byte[].class || type == byte[].class) {
//        return EdmType.BINARY;
//      } else if (type == Date.class) {
//        return EdmType.DATE_TIME;
//      } else if (type == Calendar.class) {
//        return EdmType.DATE_TIME_OFFSET;
//      } else if (type == UUID.class) {
//        return EdmType.GUID;
//      } else {
//        throw new UnsupportedOperationException("Not yet supported type '" + type + "'.");
//      }
//    }

    private Class<?> checkForBaseEntityClass(final Class<?> aClass) {
      Class<?> superClass = aClass.getSuperclass();
      if (superClass == Object.class) {
        return null;
      } else {
        EdmEntityType edmEntity = superClass.getAnnotation(EdmEntityType.class);
        if (edmEntity == null) {
          return checkForBaseEntityClass(superClass);
        } else {
          return superClass;
        }
      }
    }

    private FullQualifiedName createBaseEntityFqn(final Class<?> aClass) {
      Class<?> baseEntityClass = checkForBaseEntityClass(aClass);
      if (baseEntityClass == null) {
        return null;
      }
      return ANNOTATION_HELPER.extractEntityTypeFqn(baseEntityClass);
    }

    private Association createAssociation(final AnnotationHelper.AnnotatedNavInfo info) {
      Association association = new Association();

      AssociationEnd fromEnd = new AssociationEnd();
      fromEnd.setRole(info.getFromRoleName());
      fromEnd.setType(new FullQualifiedName(namespace, info.getFromTypeName()));
      fromEnd.setMultiplicity(info.getFromMultiplicity());
      association.setEnd1(fromEnd);

      AssociationEnd toEnd = new AssociationEnd();
      toEnd.setRole(info.getToRoleName());
      toEnd.setType(new FullQualifiedName(namespace, info.getToTypeName()));
      toEnd.setMultiplicity(info.getToMultiplicity());
      association.setEnd2(toEnd);

      String associationName = info.getRelationshipName();
      association.setName(associationName);
      return association;
    }

    private String getCanonicalName(final Field field) {
      return ANNOTATION_HELPER.getCanonicalName(field);
    }

    private boolean isAnnotatedEntity(final Class<?> clazz) {
      return ANNOTATION_HELPER.isEdmTypeAnnotated(clazz);
    }
  }

  static class SchemaBuilder {

    final private String namespace;
    // private String alias;
    private final List<Using> usings = new ArrayList<>();
    private final List<EntityType> entityTypes = new ArrayList<>();
    private final List<ComplexType> complexTypes = new ArrayList<>();
    private final Map<String, Association> name2Associations = new HashMap<>();
    private final List<EntityContainer> entityContainers = new ArrayList<>();
    private final List<AnnotationAttribute> annotationAttributes = new ArrayList<>();
    private final List<AnnotationElement> annotationElements = new ArrayList<>();

    private SchemaBuilder(final String namespace) {
      this.namespace = namespace;
    }

    public static SchemaBuilder init(final String namespace) {
      return new SchemaBuilder(namespace);
    }

    public SchemaBuilder addEntityType(final EntityType type) {
      entityTypes.add(type);
      return this;
    }

    public SchemaBuilder addEntityContainer(final EntityContainer container) {
      entityContainers.add(container);
      return this;
    }

    public SchemaBuilder addComplexType(final ComplexType createEntityType) {
      complexTypes.add(createEntityType);
      return this;
    }

    public void addAssociations(final Collection<Association> associations) {
      for (Association association : associations) {
        final String relationshipName = association.getName();
        if (name2Associations.containsKey(relationshipName)) {
          association = mergeAssociations(name2Associations.get(relationshipName), association);
        }
        name2Associations.put(relationshipName, association);
      }
    }

    private Association mergeAssociations(final Association associationOne, final Association associationTwo) {
      AssociationEnd oneEnd1 = associationOne.getEnd1();
      AssociationEnd oneEnd2 = associationOne.getEnd2();
      AssociationEnd twoEnd1 = associationTwo.getEnd1();
      AssociationEnd twoEnd2 = associationTwo.getEnd2();
      AssociationEnd[] oneEnds = new AssociationEnd[] { oneEnd1, oneEnd2 };

      for (AssociationEnd associationEnd : oneEnds) {
        if (associationEnd.getRole().equals(twoEnd1.getRole())) {
          if (twoEnd1.getMultiplicity() == EdmMultiplicity.MANY) {
            associationEnd.setMultiplicity(EdmMultiplicity.MANY);
          }
        } else if (associationEnd.getRole().equals(twoEnd2.getRole())) {
          if (twoEnd2.getMultiplicity() == EdmMultiplicity.MANY) {
            associationEnd.setMultiplicity(EdmMultiplicity.MANY);
          }
        }
      }

      return associationOne;
    }

    public Schema build() {
      Schema s = new Schema();
      s.setUsings(usings);
      s.setEntityTypes(entityTypes);
      s.setComplexTypes(complexTypes);
      s.setAssociations(new ArrayList<>(name2Associations.values()));
      s.setEntityContainers(entityContainers);
      s.setAnnotationAttributes(annotationAttributes);
      s.setAnnotationElements(annotationElements);
      s.setNamespace(namespace);
      return s;
    }
  }

  private static class ContainerBuilder {

    final private String name;
    final private String namespace;
    private boolean defaultContainer = true;
    private final List<EntitySet> entitySets = new ArrayList<>();
    private final List<AssociationSet> associationSets = new ArrayList<>();
    private final List<FunctionImport> functionImports = new ArrayList<>();

    // private Documentation documentation;

    private ContainerBuilder(final String namespace, final String containerName) {
      this.namespace = namespace;
      name = containerName;
    }

    public String getNamespace() {
      return namespace;
    }

    public static ContainerBuilder init(final String namespace, final String containerName) {
      return new ContainerBuilder(namespace, containerName);
    }

    public ContainerBuilder addEntitySet(final EntitySet entitySet) {
      entitySets.add(entitySet);
      return this;
    }

    public ContainerBuilder addFunctionImport(final FunctionImport functionImport) {
      functionImports.add(functionImport);
      return this;
    }

    public void addAssociationSets(final Collection<Association> associations) throws ODataException {
      for (Association association : associations) {
        AssociationSet as = new AssociationSet();
        as.setName(association.getName());
        FullQualifiedName asAssociationFqn = new FullQualifiedName(namespace, association.getName());
        as.setAssociation(asAssociationFqn);

        AssociationSetEnd asEnd1 = new AssociationSetEnd();
        asEnd1.setEntitySet(getEntitySetName(association.getEnd1()));
        asEnd1.setRole(association.getEnd1().getRole());
        as.setEnd1(asEnd1);

        AssociationSetEnd asEnd2 = new AssociationSetEnd();
        asEnd2.setEntitySet(getEntitySetName(association.getEnd2()));
        asEnd2.setRole(association.getEnd2().getRole());
        as.setEnd2(asEnd2);

        associationSets.add(as);
      }
    }

    public EntityContainer build() {
      EntityContainer ec = new EntityContainer();
      ec.setName(name);
      ec.setDefaultEntityContainer(defaultContainer);
      ec.setEntitySets(entitySets);
      ec.setAssociationSets(associationSets);
      ec.setFunctionImports(functionImports);
      return ec;
    }

    private String getEntitySetName(final AssociationEnd end) throws ODataException {
      for (EntitySet entitySet : entitySets) {
        if (entitySet.getEntityType().equals(end.getType())) {
          return entitySet.getName();
        }
      }
      throw new ODataException("No entity set found for " + end.getType());
    }
  }
} public class AnnotationHelper {

  public static final String DEFAULT_CONTAINER_NAME = "DefaultContainer";

  /**
   * Compare keys of both instances.
   *
   * @param firstInstance
   * @param secondInstance
   * @return
   */
  public boolean keyMatch(final Object firstInstance, final Object secondInstance) {
    if (firstInstance == null || secondInstance == null) {
      return false;
    } else if (firstInstance.getClass() != secondInstance.getClass()) {
      return false;
    }

    Map<String, Object> firstKeyFields = getValueForAnnotatedFields(firstInstance, EdmKey.class);
    Map<String, Object> secondKeyFields = getValueForAnnotatedFields(secondInstance, EdmKey.class);
    if (firstKeyFields.isEmpty() && secondKeyFields.isEmpty()) {
      throw new AnnotationRuntimeException("Both object instances does not have EdmKey fields defined ["
          + "firstClass=" + firstInstance.getClass().getName()
          + " secondClass=" + secondInstance.getClass().getName() + "].");
    }

    return keyValuesMatch(firstKeyFields, secondKeyFields);
  }

  /**
   * Compare keys of instance with key values in map.
   *
   * @param instance
   * @param keyName2Value
   * @return
   */
  public boolean keyMatch(final Object instance, final Map<String, Object> keyName2Value) {
    if(instance == null) {
      return false;
    }
    Map<String, Object> instanceKeyFields = getValueForAnnotatedFields(instance, EdmKey.class);
    return keyValuesMatch(instanceKeyFields, keyName2Value);
  }

  private boolean keyValuesMatch(final Map<String, Object> firstKeyValues, final Map<String, Object> secondKeyValues) {
    if (firstKeyValues.size() != secondKeyValues.size()) {
      return false;
    } else if (firstKeyValues.isEmpty()) {
      throw new AnnotationRuntimeException("No keys given for key value matching.");
    } else {
      Set<Map.Entry<String, Object>> entries = firstKeyValues.entrySet();
      for (Map.Entry<String, Object> entry : entries) {
        Object firstKey = entry.getValue();
        Object secondKey = secondKeyValues.get(entry.getKey());
        if (!isEqual(firstKey, secondKey)) {
          return false;
        }
      }
      return true;
    }
  }

  private boolean isEqual(final Object firstKey, final Object secondKey) {
    if (firstKey == null) {
      return secondKey == null || secondKey.equals(firstKey);
    } else {
      return firstKey.equals(secondKey);
    }
  }

  public String extractEntityTypeName(final EdmNavigationProperty enp, final Class<?> fallbackClass) {
    Class<?> entityTypeClass = enp.toType();
    return extractEntityTypeName(entityTypeClass == Object.class ? fallbackClass : entityTypeClass);
  }

  public String extractEntityTypeName(final EdmNavigationProperty enp, final Field field) {
    Class<?> entityTypeClass = enp.toType();
    if (entityTypeClass == Object.class) {
      Class<?> toClass = field.getType();
      return extractEntityTypeName((toClass.isArray() || Collection.class.isAssignableFrom(toClass) ?
          (Class<?>) ((ParameterizedType) field.getGenericType()).getActualTypeArguments()[0] : toClass));
    } else {
      return extractEntityTypeName(entityTypeClass);
    }
  }

  /**
   * Returns <code>NULL</code> if given class is not annotated. If annotated the set entity type name is returned and if
   * no name is set the default name is generated from the simple class name.
   *
   * @param annotatedClass
   * @return
   */
  public String extractEntityTypeName(final Class<?> annotatedClass) {
    return extractTypeName(annotatedClass, EdmEntityType.class);
  }

  /**
   * Returns <code>NULL</code> if given class is not annotated.
   * If annotated the entity set name is returned and if
   * no name is set a default name is generated based on the simple class name.
   *
   * @param annotatedClass
   * @return
   */
  public String extractEntitySetName(final Class<?> annotatedClass) {
    if (annotatedClass == Object.class) {
      return null;
    }
    EdmEntitySet entitySet = annotatedClass.getAnnotation(EdmEntitySet.class);
    if (entitySet == null) {
      return null;
    }

    String name = entitySet.name();
    if (name.isEmpty()) {
      return getCanonicalName(annotatedClass) + "Set";
    }
    return name;
  }

  public FullQualifiedName extractEntityTypeFqn(final EdmEntityType type, final Class<?> annotatedClass) {
    if (type.namespace().isEmpty()) {
      return new FullQualifiedName(generateNamespace(annotatedClass), extractEntityTypeName(annotatedClass));
    }
    return new FullQualifiedName(type.namespace(), extractEntityTypeName(annotatedClass));
  }

  public FullQualifiedName extractEntityTypeFqn(final Class<?> annotatedClass) {
    EdmEntityType type = annotatedClass.getAnnotation(EdmEntityType.class);
    if (type == null) {
      return null;
    }
    return extractEntityTypeFqn(type, annotatedClass);
  }

  public FullQualifiedName extractComplexTypeFqn(final Class<?> annotatedClass) {
    EdmComplexType type = annotatedClass.getAnnotation(EdmComplexType.class);
    if (type == null) {
      return null;
    }
    return extractComplexTypeFqn(type, annotatedClass);
  }

  public FullQualifiedName extractComplexTypeFqn(final EdmComplexType type, final Class<?> annotatedClass) {
    if (type.namespace().isEmpty()) {
      return new FullQualifiedName(generateNamespace(annotatedClass), extractComplexTypeName(annotatedClass));
    }
    return new FullQualifiedName(type.namespace(), extractComplexTypeName(annotatedClass));
  }

  public String extractComplexTypeName(final Class<?> annotatedClass) {
    return extractTypeName(annotatedClass, EdmComplexType.class);
  }

  public String generateNamespace(final Class<?> annotatedClass) {
    return annotatedClass.getPackage().getName();
  }

  /**
   *
   *
   * @param <T> must be EdmEntityType or EdmComplexType annotation
   * @param annotatedClass
   * @param typeAnnotation
   * @return null if annotatedClass is not annotated or name set via annotation or generated via
   * {@link #getCanonicalName(java.lang.Class)}
   */
  private <T extends Annotation> String extractTypeName(final Class<?> annotatedClass, final Class<T> typeAnnotation) {
    if (annotatedClass == Object.class) {
      return null;
    }
    T type = annotatedClass.getAnnotation(typeAnnotation);
    if (type == null) {
      return null;
    }

    String name;
    if (typeAnnotation == EdmEntityType.class) {
      name = ((EdmEntityType) type).name();
    } else if (typeAnnotation == EdmComplexType.class) {
      name = ((EdmComplexType) type).name();
    } else {
      return null;
    }

    if (name.isEmpty()) {
      return getCanonicalName(annotatedClass);
    }
    return name;
  }

  /**
   * Get the set property name from an EdmProperty or EdmNavigationProperty annotation.
   * If no property annotations is set an empty string (<code>""</code>).
   *
   * @param field which is checked
   * @return the name of the property or if no property annotations is set an empty string (<code>""</code>).
   */
  public String getPropertyNameFromAnnotation(final Field field) {
    EdmProperty property = field.getAnnotation(EdmProperty.class);
    if (property != null) {
      return property.name();
    }
    EdmNavigationProperty navProperty = field.getAnnotation(EdmNavigationProperty.class);
    if (navProperty != null) {
      return navProperty.name();
    }
    return "";
  }

  public String getPropertyName(final Field field) {
    String propertyName = getPropertyNameFromAnnotation(field);
    if (propertyName.isEmpty()) {
      propertyName = getCanonicalName(field);
    }
    return propertyName;
  }

  public String extractToRoleName(final EdmNavigationProperty enp, final Field field) {
    String role = enp.toRole();
    if (role.isEmpty()) {
      role = getCanonicalRoleName(field.getName());
    }
    return role;
  }

  public String extractFromRoleEntityName(final Field field) {
    return extractEntityTypeName(field.getDeclaringClass());
  }

  public String extractToRoleEntityName(final EdmNavigationProperty enp, final Field field) {
    Class<?> clazz = enp.toType();
    if (clazz == Object.class) {
      if(field.getType().isArray() || Collection.class.isAssignableFrom(field.getType())) {
        clazz = (Class<?>) ((ParameterizedType) field.getGenericType()).getActualTypeArguments()[0];
      } else {
        clazz = field.getType();
      }
    }
    return extractEntityTypeName(clazz);
  }

  public String getCanonicalRoleName(String name) {
    return "r_" + name.substring(0, 1).toUpperCase(Locale.ENGLISH) + name.substring(1);
  }

  public String extractRelationshipName(final EdmNavigationProperty enp, final Field field) {
    String relationshipName = enp.association();
    if (relationshipName.isEmpty()) {
      final String fromRole = extractFromRoleEntityName(field);
      final String toRole = extractToRoleEntityName(enp, field);
      return createCanonicalRelationshipName(fromRole, toRole);
    }
    return relationshipName;
  }

  public String createCanonicalRelationshipName(String fromRole, String toRole) {
    if (fromRole.compareTo(toRole) > 0) {
      return toRole + "_2_" + fromRole;
    } else {
      return fromRole + "_2_" + toRole;
    }
  }

  public EdmMultiplicity extractMultiplicity(final EdmNavigationProperty enp, final Field field) {
    EdmMultiplicity multiplicity = mapMultiplicity(enp.toMultiplicity());
    final boolean isCollectionType = field.getType().isArray() || Collection.class.isAssignableFrom(field.getType());

    if (multiplicity == EdmMultiplicity.ONE && isCollectionType) {
      return EdmMultiplicity.MANY;
    }
    return multiplicity;
  }

  /**
   * Set key fields based on values in map.
   * If an key field is not available or <code>NULL</code> in the map
   * it will be not set as <code>NULL</code> at the instance object.
   *
   * @param instance
   * @param keys
   * @return
   */
  public <T> T setKeyFields(final T instance, final Map<String, Object> keys) {
    List<Field> fields = getAnnotatedFields(instance, EdmKey.class);

    for (Field field : fields) {
      String propertyName = getPropertyName(field);
      Object keyValue = keys.get(propertyName);
      setValueForProperty(instance, propertyName, keyValue);
    }

    return instance;
  }

  public static final class ODataAnnotationException extends ODataException {
    private static final long serialVersionUID = 42L;

    public ODataAnnotationException(final String message) {
      super(message);
    }

    public ODataAnnotationException(final String message, final Exception cause) {
      super(message, cause);
    }
  }

  public class AnnotatedNavInfo {
    private final Field fromField;
    private final Field toField;
    private final EdmNavigationProperty fromNavigation;
    private final EdmNavigationProperty toNavigation;

    public AnnotatedNavInfo(final Field fromField, final Field toField, final EdmNavigationProperty fromNavigation,
        final EdmNavigationProperty toNavigation) {
      this.fromField = fromField;
      this.toField = toField;
      this.fromNavigation = fromNavigation;
      this.toNavigation = toNavigation;
    }

    public Field getFromField() {
      return fromField;
    }

    public String getFromRoleName() {
      if(isBiDirectional()) {
        return extractFromRoleEntityName(toField);
      }
      return extractToRoleName(toNavigation, toField);
    }

    public Field getToField() {
      return toField;
    }

    public String getToRoleName() {
      if(isBiDirectional()) {
        return extractToRoleName(toNavigation, toField);
      }
      return extractToRoleName(fromNavigation, fromField);
    }

    public EdmMultiplicity getFromMultiplicity() {
      if(isBiDirectional()) {
        return EdmMultiplicity.ONE;
      }
      return extractMultiplicity(toNavigation, toField);
    }

    public EdmMultiplicity getToMultiplicity() {
      if(isBiDirectional()) {
        return extractMultiplicity(toNavigation, toField);
      }
      return extractMultiplicity(fromNavigation, fromField);
    }

    public boolean isBiDirectional() {
      return fromNavigation == null;
    }

    public String getRelationshipName() {
      String toAssociation = toNavigation.association();
      String fromAssociation = "";
      if(!isBiDirectional()) {
        fromAssociation = fromNavigation.association();
      }

      if(fromAssociation.isEmpty() && fromAssociation.equals(toAssociation)) {
        return createCanonicalRelationshipName(getFromRoleName(), getToRoleName());
      } else if(toAssociation.isEmpty()) {
        return fromAssociation;
      } else if(!toAssociation.equals(fromAssociation)) {
        throw new AnnotationRuntimeException("Invalid associations for navigation properties '" +
            this.toString() + "'");
      }
      return toAssociation;
    }

    public String getFromTypeName() {
      if(isBiDirectional()) {
        return extractEntityTypeName(toField.getDeclaringClass());
      }
      return extractEntityTypeName(fromField.getDeclaringClass());
    }

    public String getToTypeName() {
      if(isBiDirectional()) {
        return extractEntityTypeName(ClassHelper.getFieldType(toField));
      }
      return extractEntityTypeName(toField.getDeclaringClass());
    }

    @Override
    public String toString() {
      if(isBiDirectional()) {
        return "AnnotatedNavInfo{biDirectional = true" +
            ", toField=" + toField.getName() +
            ", toNavigation=" + toNavigation.name() + '}';
      }
      return "AnnotatedNavInfo{" +
          "fromField=" + fromField.getName() +
          ", toField=" + toField.getName() +
          ", fromNavigation=" + fromNavigation.name() +
          ", toNavigation=" + toNavigation.name() + '}';
    }
  }


  public AnnotatedNavInfo getCommonNavigationInfo(final Class<?> sourceClass, final Class<?> targetClass) {
    List<Field> sourceFields = getAnnotatedFields(sourceClass, EdmNavigationProperty.class);
    List<Field> targetFields = getAnnotatedFields(targetClass, EdmNavigationProperty.class);

    if(sourceClass == targetClass) {
      // special case, actual handled as bi-directional
      return getCommonNavigationInfoBiDirectional(sourceClass, targetClass);
    }

    // first try via association name to get full navigation information
    for (Field sourceField : sourceFields) {
      if(ClassHelper.getFieldType(sourceField) == targetClass) {
        final EdmNavigationProperty sourceNav = sourceField.getAnnotation(EdmNavigationProperty.class);
        final String sourceAssociation = extractRelationshipName(sourceNav, sourceField);
        for (Field targetField : targetFields) {
          if(ClassHelper.getFieldType(targetField) == sourceClass) {
            final EdmNavigationProperty targetNav = targetField.getAnnotation(EdmNavigationProperty.class);
            final String targetAssociation = extractRelationshipName(targetNav, targetField);
            if (sourceAssociation.equals(targetAssociation)) {
              return new AnnotatedNavInfo(sourceField, targetField, sourceNav, targetNav);
            }
          }
        }
      }
    }

    // if nothing was found assume/guess none bi-directional navigation
    return getCommonNavigationInfoBiDirectional(sourceClass, targetClass);
  }

  private AnnotatedNavInfo getCommonNavigationInfoBiDirectional(final Class<?> sourceClass,
                                                                final Class<?> targetClass) {
    List<Field> sourceFields = getAnnotatedFields(sourceClass, EdmNavigationProperty.class);

    String targetEntityTypeName = extractEntityTypeName(targetClass);
    for (Field sourceField : sourceFields) {
      final EdmNavigationProperty sourceNav = sourceField.getAnnotation(EdmNavigationProperty.class);
      final String navTargetEntityName = extractEntityTypeName(sourceNav, sourceField);

      if (navTargetEntityName.equals(targetEntityTypeName)) {
        return new AnnotatedNavInfo(null, sourceField, null, sourceNav);
      }
    }

    return null;
  }

  public Class<?> getFieldTypeForProperty(final Class<?> clazz, final String propertyName)
      throws ODataAnnotationException {
    if (clazz == null) {
      return null;
    }

    Field field = getFieldForPropertyName(propertyName, clazz, true);
    if (field == null) {
      throw new ODataAnnotationException("No field for property '" + propertyName
          + "' found at class '" + clazz + "'.");
    }
    return field.getType();
  }

  public Class<?> getFieldTypeForProperty(final Object instance, final String propertyName)
      throws ODataAnnotationException {
    if (instance == null) {
      return null;
    }

    return getFieldTypeForProperty(instance.getClass(), propertyName);
  }

  public Object getValueForProperty(final Object instance, final String propertyName) throws ODataAnnotationException {
    if (instance == null) {
      return null;
    }

    Field field = getFieldForPropertyName(propertyName, instance.getClass(), true);
    if (field == null) {
      throw new ODataAnnotationException("No field for property '" + propertyName
          + "' found at class '" + instance.getClass() + "'.");
    }
    return getFieldValue(instance, field);
  }

  public void setValueForProperty(final Object instance, final String propertyName, final Object propertyValue) {
    if (instance != null) {
      Field field = getFieldForPropertyName(propertyName, instance.getClass(), true);
      if (field != null) {
        setFieldValue(instance, field, propertyValue);
      }
    }
  }

  private Field getFieldForPropertyName(final String propertyName,
      final Class<?> resultClass, final boolean inherited) {

    Field[] fields = resultClass.getDeclaredFields();
    for (Field field : fields) {
      EdmProperty property = field.getAnnotation(EdmProperty.class);
      if (property == null) {
        if(getCanonicalName(field).equals(propertyName)) {
          return field;
        }
      } else {
        if (property.name().isEmpty() && getCanonicalName(field).equals(propertyName)) {
          return field;
        } else if (property.name().equals(propertyName)) {
          return field;
        }
      }
    }

    Class<?> superClass = resultClass.getSuperclass();
    if (inherited && superClass != Object.class) {
      return getFieldForPropertyName(propertyName, superClass, true);
    }

    return null;
  }

  public Object getValueForField(final Object instance, final String fieldName,
      final Class<? extends Annotation> annotation) {
    if (instance == null) {
      return null;
    }
    return getValueForField(instance, fieldName, instance.getClass(), annotation, true);
  }

  public Object getValueForField(final Object instance, final Class<? extends Annotation> annotation) {
    if (instance == null) {
      return null;
    }
    return getValueForField(instance, instance.getClass(), annotation, true);
  }

  private Object getValueForField(final Object instance, final Class<?> resultClass,
      final Class<? extends Annotation> annotation, final boolean inherited) {
    return getValueForField(instance, null, resultClass, annotation, inherited);
  }

  public Map<String, Object> getValueForAnnotatedFields(final Object instance,
      final Class<? extends Annotation> annotation) {
    if (instance == null) {
      return null;
    }
    return getValueForAnnotatedFields(instance, instance.getClass(), annotation, true);
  }

  private Map<String, Object> getValueForAnnotatedFields(final Object instance, final Class<?> resultClass,
      final Class<? extends Annotation> annotation, final boolean inherited) {
    Field[] fields = resultClass.getDeclaredFields();
    Map<String, Object> fieldName2Value = new HashMap<>();

    for (Field field : fields) {
      if (field.getAnnotation(annotation) != null) {
        Object value = getFieldValue(instance, field);
        final String name = extractPropertyName(field);
        fieldName2Value.put(name, value);
      }
    }

    Class<?> superClass = resultClass.getSuperclass();
    if (inherited && superClass != Object.class) {
      Map<String, Object> tmp = getValueForAnnotatedFields(instance, superClass, annotation, true);
      fieldName2Value.putAll(tmp);
    }

    return fieldName2Value;
  }

  private String extractPropertyName(final Field field) {
    final EdmProperty property = field.getAnnotation(EdmProperty.class);
    if (property == null || property.name().isEmpty()) {
      return getCanonicalName(field);
    } else {
      return property.name();
    }
  }

  /**
   * Returns <code>NULL</code> if given method is not annotated.
   * If annotated the function import name is returned and if
   * no name is set a default name is generated based on the method name.
   *
   * @param annotatedMethod
   * @return
   */
  public String extractFunctionImportName(final Method annotatedMethod) {
    EdmFunctionImport functionImport = annotatedMethod.getAnnotation(EdmFunctionImport.class);
    if (functionImport == null) {
      return null;
    }

    String name = functionImport.name();
    if (name == null || name.isEmpty()) {
      name = annotatedMethod.getName();
    }
    return name;
  }

  public ArrayList<FunctionImportParameter> extractFunctionImportParameters(final Method method) {
    ArrayList<FunctionImportParameter> functionImportParameters = new ArrayList<>();
    Annotation[][] parameterAnnotations = method.getParameterAnnotations();
    Class<?>[] parameterTypes = method.getParameterTypes();
    for (int i = 0; i < parameterTypes.length; i++) {
      Class<?> parameterType = parameterTypes[i];
      Annotation[] annotations = parameterAnnotations[i];
      for (Annotation annotation : annotations) {
        if (annotation.annotationType().equals(EdmFunctionImportParameter.class)) {
          FunctionImportParameter fip = new FunctionImportParameter();
          EdmFunctionImportParameter fipAnnotation = (EdmFunctionImportParameter)annotation;
          // Set facets
          if (fipAnnotation.facets() != null) {
            EdmFacets edmFacets = fipAnnotation.facets();
            Facets facets = new Facets();
            // Data which is not available though it should be?
            //            facets.setCollation(collation);
            //            facets.setConcurrencyMode(concurrencyMode);
            //            facets.setDefaultValue(defaultValue);
            //            facets.setFixedLength(fixedLength);
            facets.setMaxLength(edmFacets.maxLength());
            facets.setNullable(edmFacets.nullable());
            facets.setPrecision(edmFacets.precision());
            facets.setScale(edmFacets.scale());
            // Data which is not available though it should be?
            //            facets.setUnicode(unicode);
            fip.setFacets(facets);
          }
          // Set name
          fip.setName(fipAnnotation.name());
          // Set type
          EdmType edmType;
          EdmSimpleTypeKind edmSimpleTypeKind;
          if (fipAnnotation.type() != null && !fipAnnotation.type().equals(EdmType.NULL)) {
            edmSimpleTypeKind = mapTypeKind(fipAnnotation.type());
          } else {
            edmType = mapType(parameterType);
            edmSimpleTypeKind = mapTypeKind(edmType);
          }
          fip.setType(edmSimpleTypeKind);
          // Add function import parameter
          functionImportParameters.add(fip);
        }
      }

    }

    return functionImportParameters;
  }

  public ReturnType extractReturnType(final Method annotatedMethod) {
    EdmFunctionImport functionImport = annotatedMethod.getAnnotation(EdmFunctionImport.class);
    ReturnType returnType = new ReturnType();
    if (functionImport.returnType().isCollection()) {
      returnType.setMultiplicity(EdmMultiplicity.MANY);
    } else {
      returnType.setMultiplicity(EdmMultiplicity.ZERO_TO_ONE);
    }

    switch (functionImport.returnType().type()) {
      case SIMPLE:
        EdmType edmType = mapType(annotatedMethod.getReturnType());
        EdmSimpleTypeKind edmSimpleTypeKind = mapTypeKind(edmType);
        returnType.setTypeName(edmSimpleTypeKind.getFullQualifiedName());
        break;
      case ENTITY:
        returnType.setTypeName(extractEntityTypeFqn(
                determineAnnotatedClass(functionImport, annotatedMethod)));
        break;
      case COMPLEX:
        returnType.setTypeName(extractComplexTypeFqn(
                determineAnnotatedClass(functionImport, annotatedMethod)));
        break;
      default:
        throw new UnsupportedOperationException("Not yet supported return type type '" + functionImport.returnType().type() + "'.");
    }
    return returnType;
  }

  private Class<?> determineAnnotatedClass(final EdmFunctionImport functionImport,
                                           final Method annotatedMethod) {
    if (functionImport.returnType().isCollection()) {
      ParameterizedType parameterizedType = (ParameterizedType) annotatedMethod.getGenericReturnType();
      return (Class<?>) parameterizedType.getActualTypeArguments()[0];
    } else {
      return annotatedMethod.getReturnType();
    }
  }

  public String extractEntitySetName(final Method annotatedMethod) {
    EdmFunctionImport functionImport = annotatedMethod.getAnnotation(EdmFunctionImport.class);
    if (functionImport.entitySet() != null && !functionImport.entitySet().isEmpty()) {
      return functionImport.entitySet();
    }
    return null;
  }

  public String extractHttpMethod(final Method annotatedMethod) {
    EdmFunctionImport functionImport = annotatedMethod.getAnnotation(EdmFunctionImport.class);
    if (functionImport.httpMethod() != null) {
      return functionImport.httpMethod().name();
    }
    return null;
  }


  public void setValueForAnnotatedField(final Object instance, final Class<? extends Annotation> annotation,
      final Object value)
      throws ODataAnnotationException {
    List<Field> fields = getAnnotatedFields(instance, annotation);

    if (fields.isEmpty()) {
      throw new ODataAnnotationException("No field found for annotation '" + annotation
          + "' on instance '" + instance + "'.");
    } else if (fields.size() > 1) {
      throw new ODataAnnotationException("More then one field found for annotation '" + annotation
          + "' on instance '" + instance + "'.");
    }

    setFieldValue(instance, fields.get(0), value);
  }

  public void setValuesToAnnotatedFields(final Object instance,
      final Class<? extends Annotation> annotation, final Map<String, Object> fieldName2Value) {
    List<Field> fields = getAnnotatedFields(instance, annotation);

    // XXX: refactore
    for (Field field : fields) {
      final String canonicalName = getCanonicalName(field);
      if (fieldName2Value.containsKey(canonicalName)) {
        Object value = fieldName2Value.get(canonicalName);
        setFieldValue(instance, field, value);
      }
    }
  }

  public List<Field> getAnnotatedFields(final Object instance, final Class<? extends Annotation> annotation) {
    if (instance == null) {
      return null;
    }
    return getAnnotatedFields(instance.getClass(), annotation, true);
  }

  public List<Field> getAnnotatedFields(final Class<?> fieldClass, final Class<? extends Annotation> annotation) {
    return getAnnotatedFields(fieldClass, annotation, true);
  }

  /**
   *
   * @param resultClass
   * @param annotation
   * @param inherited
   * @return
   */
  private List<Field> getAnnotatedFields(final Class<?> resultClass,
      final Class<? extends Annotation> annotation, final boolean inherited) {
    if (resultClass == null) {
      return null;
    }

    Field[] fields = resultClass.getDeclaredFields();
    List<Field> annotatedFields = new ArrayList<>();

    for (Field field : fields) {
      if (field.getAnnotation(annotation) != null) {
        annotatedFields.add(field);
      }
    }

    Class<?> superClass = resultClass.getSuperclass();
    if (inherited && superClass != Object.class) {
      List<Field> tmp = getAnnotatedFields(superClass, annotation, true);
      annotatedFields.addAll(tmp);
    }

    return annotatedFields;
  }

  private Object getValueForField(final Object instance, final String fieldName, final Class<?> resultClass,
      final Class<? extends Annotation> annotation, final boolean inherited) {
    if (instance == null) {
      return null;
    }

    Field[] fields = resultClass.getDeclaredFields();
    for (Field field : fields) {
      if (field.getAnnotation(annotation) != null
          && (fieldName == null || field.getName().equals(fieldName))) {
        return getFieldValue(instance, field);
      }
    }

    Class<?> superClass = resultClass.getSuperclass();
    if (inherited && superClass != Object.class) {
      return getValueForField(instance, fieldName, superClass, annotation, true);
    }

    return null;
  }

  private Object getFieldValue(final Object instance, final Field field) {
    try {
      boolean access = field.isAccessible();
      field.setAccessible(true);
      Object value = field.get(instance);
      field.setAccessible(access);
      return value;
    } catch (IllegalArgumentException | IllegalAccessException ex) { // should never happen
      throw new AnnotationRuntimeException(ex);
    }
  }

  private void setFieldValue(final Object instance, final Field field, final Object value) {
    try {
      Object usedValue = value;
      if (value != null
          && field.getType() != value.getClass()
          && value.getClass() == String.class) {
        usedValue = convert(field, (String) value);
      }
      boolean access = field.isAccessible();
      field.setAccessible(true);
      field.set(instance, usedValue);
      field.setAccessible(access);
    } catch (IllegalArgumentException | IllegalAccessException ex) { // should never happen
      throw new AnnotationRuntimeException(ex);
    }
  }


  public List<Method> getAnnotatedMethods(final Class<?> resultClass,
                                           final Class<? extends Annotation> annotation,
                                           final boolean inherited) {
    if (resultClass == null) {
      return null;
    }

    Method[] methods = resultClass.getDeclaredMethods();
    List<Method> annotatedMethods = new ArrayList<>();

    for (Method method: methods) {
      if (method.getAnnotation(annotation) != null) {
        annotatedMethods.add(method);
      }
    }

    Class<?> superClass = resultClass.getSuperclass();
    if (inherited && superClass != Object.class) {
      List<Method> tmp = getAnnotatedMethods(superClass, annotation, true);
      annotatedMethods.addAll(tmp);
    }

    return annotatedMethods;
  }


  private Object convert(final Field field, final String propertyValue) {
    Class<?> fieldClass = field.getType();
    try {
      EdmProperty property = field.getAnnotation(EdmProperty.class);
      EdmSimpleTypeKind type = mapTypeKind(property.type());
      return type.getEdmSimpleTypeInstance().valueOfString(propertyValue,
          EdmLiteralKind.DEFAULT, null, fieldClass);
    } catch (EdmSimpleTypeException ex) {
      throw new AnnotationRuntimeException("Conversion failed for string property ["
          + propertyValue + "] on field ["
          + field + "] with error: " + ex.getMessage(), ex);
    }
  }

  public boolean isEdmAnnotated(final Object object) {
    if (object == null) {
      return false;
    }
    return isEdmAnnotated(object.getClass());
  }

  public boolean isEdmTypeAnnotated(final Class<?> clazz) {
    boolean isComplexEntity = clazz.getAnnotation(EdmComplexType.class) != null;
    boolean isEntity = clazz.getAnnotation(EdmEntityType.class) != null;
    return isComplexEntity || isEntity;
  }

  public boolean isEdmAnnotated(final Class<?> clazz) {
    if (clazz == null) {
      return false;
    } else {
      // TODO: mibo: do next checks only if first was not true
      final boolean isEntity = null != clazz.getAnnotation(EdmEntityType.class);
      final boolean isEntitySet = null != clazz.getAnnotation(EdmEntitySet.class);
      final boolean isComplexEntity = null != clazz.getAnnotation(EdmComplexType.class);
      final boolean hasFunctionImport = hasEdmFunction(clazz);

      return isEntity || isEntitySet || isComplexEntity || hasFunctionImport;
    }
  }

  public boolean hasEdmFunction(final Class<?> clazz) {
    Method[] methods = clazz.getMethods();
    for (Method method : methods) {
      if (method.getAnnotation(EdmFunctionImport.class) != null) {
        return true;
      }
    }
    return false;
  }

  public String getCanonicalName(final Field field) {
    return firstCharToUpperCase(field.getName());
  }

  public String getCanonicalName(final Class<?> clazz) {
    return firstCharToUpperCase(clazz.getSimpleName());
  }

  private String firstCharToUpperCase(final String content) {
    if (content == null || content.isEmpty()) {
      return content;
    }
    return content.substring(0, 1).toUpperCase(Locale.ENGLISH) + content.substring(1);
  }

  public EdmSimpleTypeKind mapTypeKind(final org.apache.olingo.odata2.api.annotation.edm.EdmType type) {
    switch (type) {
    case BINARY:
      return EdmSimpleTypeKind.Binary;
    case BOOLEAN:
      return EdmSimpleTypeKind.Boolean;
    case BYTE:
      return EdmSimpleTypeKind.Byte;
    case COMPLEX:
      return EdmSimpleTypeKind.Null;
    case DATE_TIME:
      return EdmSimpleTypeKind.DateTime;
    case DATE_TIME_OFFSET:
      return EdmSimpleTypeKind.DateTimeOffset;
    case DECIMAL:
      return EdmSimpleTypeKind.Decimal;
    case DOUBLE:
      return EdmSimpleTypeKind.Double;
    case GUID:
      return EdmSimpleTypeKind.Guid;
    case INT16:
      return EdmSimpleTypeKind.Int16;
    case INT32:
      return EdmSimpleTypeKind.Int32;
    case INT64:
      return EdmSimpleTypeKind.Int64;
    case NULL:
      return EdmSimpleTypeKind.Null;
    case SBYTE:
      return EdmSimpleTypeKind.SByte;
    case SINGLE:
      return EdmSimpleTypeKind.Single;
    case STRING:
      return EdmSimpleTypeKind.String;
    case TIME:
      return EdmSimpleTypeKind.Time;
    default:
      throw new AnnotationRuntimeException("Unknown type '" + type
          + "' for mapping to EdmSimpleTypeKind.");
    }
  }

  public EdmType mapType(final Class<?> type) {
    if (type == String.class || type == URI.class || type == URL.class
        || type.isEnum() || type == TimeZone.class || type == Locale.class  ) {
      return EdmType.STRING;
    } else if (type == boolean.class || type == Boolean.class) {
      return EdmType.BOOLEAN;
    } else if (type == byte.class || type == Byte.class) {
      return EdmType.SBYTE;
    } else if (type == short.class || type == Short.class) {
      return EdmType.INT16;
    } else if (type == int.class || type == Integer.class) {
      return EdmType.INT32;
    } else if (type == long.class || type == Long.class) {
      return EdmType.INT64;
    } else if (type == double.class || type == Double.class) {
      return EdmType.DOUBLE;
    } else if (type == float.class || type == Float.class) {
      return EdmType.SINGLE;
    } else if (type == BigInteger.class || type == BigDecimal.class) {
      return EdmType.DECIMAL;
    } else if (type == Byte[].class || type == byte[].class) {
      return EdmType.BINARY;
    } else if (type == java.util.Date.class || type == java.sql.Date.class || type == Timestamp.class) {
      return EdmType.DATE_TIME;
    } else if (type == Calendar.class) {
      return EdmType.DATE_TIME_OFFSET;
    } else if (type == UUID.class) {
      return EdmType.GUID;
    } else {
      throw new UnsupportedOperationException("Not yet supported type '" + type + "'.");
    }
  }

  public EdmMultiplicity mapMultiplicity(final Multiplicity multiplicity) {
    switch (multiplicity) {
    case ZERO_OR_ONE:
      return EdmMultiplicity.ZERO_TO_ONE;
    case ONE:
      return EdmMultiplicity.ONE;
    case MANY:
      return EdmMultiplicity.MANY;
    default:
      throw new AnnotationRuntimeException("Unknown type '" + multiplicity + "' for mapping to EdmMultiplicity.");
    }
  }

  /**
   *
   */
  private static class EdmAnnotationException extends RuntimeException {

    private static final long serialVersionUID = 42L;

    public EdmAnnotationException(final String message) {
      super(message);
    }
  }

  public String getCanonicalNamespace(final Class<?> aClass) {
    return generateNamespace(aClass);
  }

  public String extractContainerName(final Class<?> aClass) {
    EdmEntitySet entitySet = aClass.getAnnotation(EdmEntitySet.class);
    if (entitySet != null) {
      String containerName = entitySet.container();
      if (!containerName.isEmpty()) {
        return containerName;
      }
    }
    return DEFAULT_CONTAINER_NAME;
  }
} public class AnnotationRuntimeException extends RuntimeException {

  /** Manual generated */
  private static final long serialVersionUID = 42L;

  public AnnotationRuntimeException(final String message) {
    super(message);
  }

  public AnnotationRuntimeException(final Throwable cause) {
    super(cause);
  }

  public AnnotationRuntimeException(final String message, final Throwable cause) {
    super(message, cause);
  }
}
import org.apache.olingo.odata2.api.annotation.edm.EdmKey;
import org.apache.olingo.odata2.api.annotation.edm.EdmProperty;
import org.apache.olingo.odata2.api.edm.EdmEntitySet;
import org.apache.olingo.odata2.api.edm.EdmEntityType;
import org.apache.olingo.odata2.api.edm.FullQualifiedName;
import org.apache.olingo.odata2.api.edm.provider.EntitySet;
import org.apache.olingo.odata2.api.exception.ODataException;
import org.apache.olingo.odata2.api.exception.ODataNotFoundException;
import org.apache.olingo.odata2.janos.processor.api.data.ReadOptions;
import org.apache.olingo.odata2.janos.processor.api.data.ReadResult;
import org.apache.olingo.odata2.janos.processor.api.data.source.DataSource;
import org.apache.olingo.odata2.janos.processor.api.data.source.DataSource.BinaryData;
import org.apache.olingo.odata2.janos.processor.api.data.store.DataStore;
import org.apache.olingo.odata2.janos.processor.api.data.store.DataStoreManager;
import org.apache.olingo.odata2.janos.processor.core.data.source.AnnotationDataSource;
import org.apache.olingo.odata2.janos.processor.core.edm.AnnotationEdmProvider;
import org.apache.olingo.odata2.janos.processor.core.model.*;
import org.apache.olingo.odata2.janos.processor.core.util.AnnotationHelper;
import org.apache.olingo.odata2.janos.processor.core.util.AnnotationRuntimeException;
import org.junit.Assert;
import org.junit.Ignore;
import org.junit.Test;
import org.mockito.Mockito;
import java.nio.charset.Charset;
import java.util.*;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
    datasource = new AnnotationDataSource(Building.class.getPackage().getName(), dataStoreFactory);
    edmProvider = new AnnotationEdmProvider(Building.class.getPackage().getName());
  }

  @Test
  public void initFromPackage() throws Exception {
    AnnotationDataSource ds = new AnnotationDataSource(Building.class.getPackage().getName(), dataStoreFactory);
    Assert.assertNotNull(ds);
  }

  @Test(expected = IllegalArgumentException.class)
  public void initFromNotExistingPackage() throws Exception {
    AnnotationDataSource ds = new AnnotationDataSource("does.not.exist", dataStoreFactory);
    Assert.assertNotNull(ds);
  }

  @Test
  public void initFromPackageWithoutAnnotatedClasses() throws Exception {
    AnnotationDataSource ds = new AnnotationDataSource(this.getClass().getPackage().getName(), dataStoreFactory);
    Assert.assertNotNull(ds);
  }

  @Test
  public void initFromClassCollectionEntitySets() throws Exception {
    AnnotationDataSource ds = new AnnotationDataSource(ANNOTATED_ENTITY_SET_CLASSES, dataStoreFactory);
    Assert.assertNotNull(ds);
  }

  @Test
  public void initFromClassCollectionModel() throws Exception {
    AnnotationDataSource ds = new AnnotationDataSource(ANNOTATED_MODEL_CLASSES, dataStoreFactory);
    Assert.assertNotNull(ds);
  }

  @Test(expected = ODataException.class)
  public void initFromClassCollectionNotAnnotated() throws Exception {
    Set<Class<?>> annotatedClassesAndMore = new HashSet<>(ANNOTATED_ENTITY_SET_CLASSES);
    annotatedClassesAndMore.add(String.class);
    annotatedClassesAndMore.add(Object.class);
    AnnotationDataSource ds = new AnnotationDataSource(annotatedClassesAndMore, dataStoreFactory);
    Assert.assertNotNull(ds);
  }

  @Test
  @Ignore
  public void multiThreadedSyncOnBuildingsTest() throws Exception {
    final EdmEntitySet edmEntitySet = createMockedEdmEntitySet("Buildings");
    CountDownLatch latch;

    List<Thread> threads = new ArrayList<>();
    int max = 500;

    latch = new CountDownLatch(max);
    for (int i = 0; i < max; i++) {
      threads.add(createBuildingThread(latch, datasource, edmEntitySet, String.valueOf("10")));
    }

    for (Thread thread : threads) {
      thread.start();
    }

    latch.await(60, TimeUnit.SECONDS);

    DataStore<Building> ds = datasource.getDataStore(Building.class);
    Collection<Building> buildings = ds.read();
    Assert.assertEquals(max, buildings.size());
  }

  @org.apache.olingo.odata2.api.annotation.edm.EdmEntitySet
  @org.apache.olingo.odata2.api.annotation.edm.EdmEntityType
  private static class SimpleEntity {

    @EdmKey
    @EdmProperty
    public Integer id;
    @EdmProperty
    public String name;
  }

  @Test
  @Ignore
  public void multiThreadedSyncCreateReadTest() throws Exception {
    Collection<Class<?>> ac = new ArrayList<>();
    ac.add(SimpleEntity.class);
    final AnnotationDataSource localDs = new AnnotationDataSource(SimpleEntity.class.getPackage().getName(), dataStoreFactory);
    final AnnotationEdmProvider localProvider = new AnnotationEdmProvider(ac);
    final EdmEntitySet edmEntitySet = createMockedEdmEntitySet(localProvider, "SimpleEntitySet");
    final CountDownLatch latch;

    List<Thread> threads = new ArrayList<>();
    int max = 500;
    latch = new CountDownLatch(max);
    for (int i = 0; i < max; i++) {
      Runnable run = () -> {
        SimpleEntity se = new SimpleEntity();
        se.id = Integer.valueOf(String.valueOf(System.currentTimeMillis()).substring(8));
        se.name = "Name: " + System.currentTimeMillis();
        try {
          localDs.createData(edmEntitySet, se);
        } catch (Exception ex) {
          throw new RuntimeException(ex);
        } finally {
          latch.countDown();
        }
      };

      threads.add(new Thread(run));
    }

    for (Thread thread : threads) {
      thread.start();
    }

    latch.await(60, TimeUnit.SECONDS);

    DataStore<SimpleEntity> ds = localDs.getDataStore(SimpleEntity.class);
    Collection<SimpleEntity> buildings = ds.read();
    Assert.assertEquals(max, buildings.size());
  }

  private Thread createBuildingThread(final CountDownLatch latch, final DataSource datasource,