You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@olingo.apache.org by ch...@apache.org on 2014/08/28 05:48:19 UTC
[12/51] [partial] rename folder /datajs into /odatajs. no file
modification.
http://git-wip-us.apache.org/repos/asf/olingo-odata4-js/blob/d5ec5557/odatajs/tests/code/ReflectionDataContext.cs
----------------------------------------------------------------------
diff --git a/odatajs/tests/code/ReflectionDataContext.cs b/odatajs/tests/code/ReflectionDataContext.cs
new file mode 100644
index 0000000..1635d54
--- /dev/null
+++ b/odatajs/tests/code/ReflectionDataContext.cs
@@ -0,0 +1,743 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+
+namespace DataJS.Tests
+{
+ using System;
+ using System.Collections;
+ using System.Collections.Generic;
+ using System.Collections.ObjectModel;
+ using Microsoft.OData.Service;
+ using System.Globalization;
+ using System.Linq;
+ using System.Reflection;
+
+ /// <summary>
+ /// Provides a reflection-based, updatable data context.
+ /// </summary>
+ public abstract class ReflectionDataContext
+ {
+ // Fields
+ private List<object> deletedObjects = new List<object>();
+ private List<Action> pendingChanges;
+ private static Dictionary<Type, Dictionary<string, IList>> resourceSetsByContextTypeStorage = new Dictionary<Type, Dictionary<string, IList>>();
+
+ // Methods
+ protected ReflectionDataContext()
+ {
+ this.MetadataHelper = new ReflectionMetadataHelper(this);
+ this.pendingChanges = new List<Action>();
+ if (!resourceSetsByContextTypeStorage.ContainsKey(base.GetType()))
+ {
+ resourceSetsByContextTypeStorage.Add(base.GetType(), new Dictionary<string, IList>());
+ foreach (string resourceSetName in this.MetadataHelper.GetResourceSetNames())
+ {
+ Type resourceType = this.MetadataHelper.GetResourceTypeOfSet(resourceSetName);
+ IList listOfTInstance = Activator.CreateInstance(typeof(List<>).MakeGenericType(new Type[] { resourceType })) as IList;
+ this.ResourceSetsStorage.Add(resourceSetName, listOfTInstance);
+ }
+ }
+ this.EnsureDataIsInitialized();
+ }
+
+ public virtual void AddReferenceToCollection(object targetResource, string propertyName, object resourceToBeAdded)
+ {
+ ExceptionUtilities.CheckArgumentNotNull(targetResource, "targetResource");
+ ExceptionUtilities.CheckArgumentNotNull(propertyName, "propertyName");
+ ExceptionUtilities.CheckArgumentNotNull(resourceToBeAdded, "resourceToBeAdded");
+ UpdatableToken targetToken = UpdatableToken.AssertIsToken(targetResource, "targetResource");
+ targetResource = targetToken.Resource;
+ resourceToBeAdded = UpdatableToken.AssertIsTokenAndResolve(resourceToBeAdded, "resourceToBeAdded");
+ IList list = this.GetValue(targetToken, propertyName) as IList;
+ ExceptionUtilities.CheckObjectNotNull(list, "Property '{0}' on type '{1}' was not a list", new object[] { propertyName, targetResource.GetType().Name });
+ this.pendingChanges.Add(delegate {
+ list.Add(resourceToBeAdded);
+ });
+ }
+
+ public virtual void ClearChanges()
+ {
+ this.pendingChanges.Clear();
+ }
+
+ public void ClearData()
+ {
+ this.ResourceSetsStorage.Clear();
+ }
+
+ private static bool CompareETagValues(Dictionary<string, object> resourceCookieValues, IEnumerable<KeyValuePair<string, object>> concurrencyValues)
+ {
+ if (concurrencyValues.Count<KeyValuePair<string, object>>() != resourceCookieValues.Count)
+ {
+ return false;
+ }
+ foreach (KeyValuePair<string, object> keyValuePair in concurrencyValues)
+ {
+ if (!resourceCookieValues.ContainsKey(keyValuePair.Key))
+ {
+ return false;
+ }
+ if (keyValuePair.Value == null)
+ {
+ return (resourceCookieValues[keyValuePair.Key] == null);
+ }
+ if (!keyValuePair.Value.Equals(resourceCookieValues[keyValuePair.Key]))
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public virtual object CreateResource(string containerName, string fullTypeName)
+ {
+ ExceptionUtilities.CheckArgumentNotNull(fullTypeName, "fullTypeName");
+ UpdatableToken token = this.InstantiateResourceType(fullTypeName);
+ if (containerName != null)
+ {
+ this.pendingChanges.Add(delegate {
+ this.GetResourceSetEntities(containerName).Add(token.Resource);
+ });
+ }
+ return token;
+ }
+
+ private void DeleteAllReferences(object targetResource)
+ {
+ foreach (string currentSetName in this.MetadataHelper.GetResourceSetNames())
+ {
+ Type currentEntityType = this.MetadataHelper.GetResourceTypeOfSet(currentSetName);
+ IList entitySetList = this.GetResourceSetEntities(currentSetName);
+ foreach (NavigationPropertyInfo navigationProperty in this.MetadataHelper.GetNavigationProperties(GetResourceTypeFullName(currentEntityType)))
+ {
+ if (navigationProperty.CollectionElementType != null)
+ {
+ foreach (object currentEntityInstance in entitySetList)
+ {
+ this.RemoveResourceFromCollectionOnTargetResourceMatch(targetResource, navigationProperty, currentEntityInstance);
+ }
+ }
+ else
+ {
+ ExceptionUtilities.CheckObjectNotNull(navigationProperty.PropertyInfo, "Invalid navigation property info", new object[0]);
+ foreach (object currentEntityInstance in entitySetList)
+ {
+ this.SetEntityReferenceToNullOnTargetResourceMatch(targetResource, navigationProperty, currentEntityInstance);
+ }
+ }
+ }
+ }
+ }
+
+ public virtual void DeleteResource(object targetResource)
+ {
+ ExceptionUtilities.CheckArgumentNotNull(targetResource, "targetResource");
+ targetResource = UpdatableToken.AssertIsTokenAndResolve(targetResource, "targetResource");
+ string resourceSetName = this.GetResourceSetOfTargetResource(targetResource);
+ ExceptionUtilities.CheckObjectNotNull(resourceSetName, "Unable to find set of the resource to delete", new object[0]);
+ this.deletedObjects.Add(targetResource);
+ IList resourceSetList = this.GetResourceSetEntities(resourceSetName);
+ this.DeleteAllReferences(targetResource);
+ this.pendingChanges.Add(delegate {
+ resourceSetList.Remove(targetResource);
+ });
+ }
+
+ protected abstract void EnsureDataIsInitialized();
+
+ protected virtual Type GetCollectionPropertyType(string fullTypeName, string propertyName)
+ {
+ Type type = this.MetadataHelper.FindClrTypeByFullName(fullTypeName);
+ Type collectionType = null;
+ if (type != null)
+ {
+ PropertyInfo property = type.GetProperty(propertyName);
+ if (property != null)
+ {
+ collectionType = property.PropertyType;
+ }
+ }
+ return collectionType;
+ }
+
+ private Dictionary<string, object> GetConcurrencyValues(object targetResource)
+ {
+ Dictionary<string, object> etagValues = new Dictionary<string, object>();
+ foreach (string etagProperty in this.MetadataHelper.GetETagPropertiesOfType(GetResourceTypeFullName(targetResource.GetType())))
+ {
+ etagValues.Add(etagProperty, targetResource.GetType().GetProperty(etagProperty).GetValue(targetResource, null));
+ }
+ return etagValues;
+ }
+
+ public virtual object GetResource(IQueryable query, string fullTypeName)
+ {
+ ExceptionUtilities.CheckArgumentNotNull(query, "query");
+ object resource = null;
+ foreach (object r in query)
+ {
+ ExceptionUtilities.Assert(resource == null, "Invalid Uri specified. The query '{0}' must refer to a single resource", new object[] { query.ToString() });
+ resource = r;
+ }
+ if (resource == null)
+ {
+ return null;
+ }
+ if (fullTypeName != null)
+ {
+ this.ValidateResourceType(resource, fullTypeName);
+ }
+ return new UpdatableToken(resource);
+ }
+
+ public IList<T> GetResourceSetEntities<T>(string resourceSetName)
+ {
+ return (IList<T>) this.GetResourceSetEntities(resourceSetName);
+ }
+
+ internal IList GetResourceSetEntities(string resourceSetName)
+ {
+ IList entities;
+ if (!this.ResourceSetsStorage.TryGetValue(resourceSetName, out entities))
+ {
+ Type elementType = this.MetadataHelper.GetResourceTypeOfSet(resourceSetName);
+ entities = (IList) Activator.CreateInstance(typeof(List<>).MakeGenericType(new Type[] { elementType }));
+ this.ResourceSetsStorage[resourceSetName] = entities;
+ }
+ return entities;
+ }
+
+ private string GetResourceSetOfTargetResource(object targetResource)
+ {
+ foreach (string currentResourceSetName in this.MetadataHelper.GetResourceSetNames())
+ {
+ if (this.GetResourceSetEntities(currentResourceSetName).Contains(targetResource))
+ {
+ return currentResourceSetName;
+ }
+ }
+ return null;
+ }
+
+ public static string GetResourceTypeFullName(Type type)
+ {
+ return type.FullName.Replace('+', '_');
+ }
+
+ public virtual object GetValue(object targetResource, string propertyName)
+ {
+ ExceptionUtilities.CheckArgumentNotNull(targetResource, "targetResource");
+ ExceptionUtilities.CheckArgumentNotNull(propertyName, "propertyName");
+ UpdatableToken token = UpdatableToken.AssertIsToken(targetResource, "targetResource");
+ if (token.PendingPropertyUpdates.ContainsKey(propertyName))
+ {
+ return token.PendingPropertyUpdates[propertyName];
+ }
+ targetResource = token.Resource;
+ PropertyInfo pi = targetResource.GetType().GetProperty(propertyName);
+ ExceptionUtilities.CheckObjectNotNull(pi, "Cannot find the property '{0}' on type '{1}'", new object[] { propertyName, targetResource.GetType().Name });
+ object value = pi.GetValue(targetResource, null);
+ if ((value != null) && (pi.PropertyType.Assembly == base.GetType().Assembly))
+ {
+ ExceptionUtilities.Assert(!this.MetadataHelper.IsTypeAnEntityType(pi.PropertyType), "GetValue should never be called for reference properties. Type was '{0}', property was '{1}'", new object[] { pi.PropertyType.FullName, propertyName });
+ value = new UpdatableToken(value);
+ }
+ return value;
+ }
+
+ private UpdatableToken InstantiateResourceType(string fullTypeName)
+ {
+ Type t = this.MetadataHelper.FindClrTypeByFullName(fullTypeName);
+ object instance = Activator.CreateInstance(t);
+ UpdatableToken token = new UpdatableToken(instance);
+ foreach (PropertyInfo p in t.GetProperties())
+ {
+ object generatedValue;
+ PropertyInfo property = p;
+ if (this.IsCollectionProperty(property))
+ {
+ Type collectionType = this.GetCollectionPropertyType(GetResourceTypeFullName(t), property.Name);
+ if (collectionType != null)
+ {
+ object newCollection = Activator.CreateInstance(collectionType);
+ token.PendingPropertyUpdates[property.Name] = newCollection;
+ this.pendingChanges.Add(delegate {
+ property.SetValue(instance, newCollection, null);
+ });
+ }
+ }
+ if (this.TryGetStoreGeneratedValue(fullTypeName, property.Name, out generatedValue))
+ {
+ token.PendingPropertyUpdates[property.Name] = generatedValue;
+ this.pendingChanges.Add(delegate {
+ property.SetValue(instance, generatedValue, null);
+ });
+ }
+ }
+ return token;
+ }
+
+ protected virtual bool IsCollectionProperty(PropertyInfo propertyInfo)
+ {
+ return ((typeof(IEnumerable).IsAssignableFrom(propertyInfo.PropertyType) && (propertyInfo.PropertyType != typeof(string))) && (propertyInfo.PropertyType != typeof(byte[])));
+ }
+
+ public virtual void RemoveReferenceFromCollection(object targetResource, string propertyName, object resourceToBeRemoved)
+ {
+ ExceptionUtilities.CheckArgumentNotNull(targetResource, "targetResource");
+ ExceptionUtilities.CheckArgumentNotNull(propertyName, "propertyName");
+ ExceptionUtilities.CheckArgumentNotNull(resourceToBeRemoved, "resourceToBeRemoved");
+ UpdatableToken.AssertIsToken(targetResource, "targetResource");
+ resourceToBeRemoved = UpdatableToken.AssertIsTokenAndResolve(resourceToBeRemoved, "resourceToBeRemoved");
+ IList list = this.GetValue(targetResource, propertyName) as IList;
+ ExceptionUtilities.CheckObjectNotNull(list, "Property '{0}' on type '{1}' was not a list", new object[] { propertyName, targetResource.GetType().Name });
+ this.pendingChanges.Add(delegate {
+ list.Remove(resourceToBeRemoved);
+ });
+ }
+
+ private void RemoveResourceFromCollectionOnTargetResourceMatch(object targetResource, NavigationPropertyInfo navigationPropertyInfo, object currentEntityInstance)
+ {
+ IEnumerable childCollectionObject = navigationPropertyInfo.PropertyInfo.GetValue(currentEntityInstance, null) as IEnumerable;
+ if (childCollectionObject.Cast<object>().Any<object>(delegate (object o) {
+ return o == targetResource;
+ }))
+ {
+ MethodInfo removeMethod = navigationPropertyInfo.PropertyInfo.PropertyType.GetMethod("Remove");
+ this.pendingChanges.Add(delegate {
+ removeMethod.Invoke(childCollectionObject, new object[] { targetResource });
+ });
+ }
+ }
+
+ public virtual object ResetResource(object resource)
+ {
+ ExceptionUtilities.CheckArgumentNotNull(resource, "resource");
+ UpdatableToken token = UpdatableToken.AssertIsToken(resource, "resource");
+ resource = token.Resource;
+ token = new UpdatableToken(resource);
+ object newInstance = Activator.CreateInstance(resource.GetType());
+ ExceptionUtilities.CheckObjectNotNull(newInstance, "Cannot reset resource because unable to creating new instance of type '{0}' returns null", new object[] { resource.GetType().Name });
+ foreach (string propertyToReset in this.MetadataHelper.GetPropertiesToReset(GetResourceTypeFullName(resource.GetType())))
+ {
+ PropertyInfo pi = newInstance.GetType().GetProperty(propertyToReset);
+ ExceptionUtilities.CheckObjectNotNull(pi, "Cannot reset resource because unable to find property '{0}'", new object[] { propertyToReset });
+ object newValue = pi.GetValue(newInstance, null);
+ this.pendingChanges.Add(delegate {
+ pi.SetValue(resource, newValue, null);
+ });
+ token.PendingPropertyUpdates[propertyToReset] = newValue;
+ }
+ return token;
+ }
+
+ public virtual object ResolveResource(object resource)
+ {
+ ExceptionUtilities.CheckArgumentNotNull(resource, "resource");
+ return UpdatableToken.AssertIsTokenAndResolve(resource, "resource");
+ }
+
+ public virtual void SaveChanges()
+ {
+ foreach (Action pendingChange in this.pendingChanges)
+ {
+ pendingChange();
+ }
+ this.pendingChanges.Clear();
+ foreach (object deleted in this.deletedObjects)
+ {
+ foreach (object entity in this.ResourceSetsStorage.SelectMany<KeyValuePair<string, IList>, object>(delegate (KeyValuePair<string, IList> p) {
+ return p.Value.Cast<object>();
+ }))
+ {
+ ExceptionUtilities.Assert(!object.ReferenceEquals(deleted, entity), "Found deleted entity!", new object[0]);
+ foreach (PropertyInfo propertyInfo in entity.GetType().GetProperties())
+ {
+ object value = propertyInfo.GetValue(entity, null);
+ ExceptionUtilities.Assert(!object.ReferenceEquals(deleted, value), "Found deleted entity!", new object[0]);
+ IEnumerable enumerable = value as IEnumerable;
+ if (enumerable != null)
+ {
+ foreach (object valueElement in enumerable.Cast<object>())
+ {
+ ExceptionUtilities.Assert(!object.ReferenceEquals(deleted, valueElement), "Found deleted entity!", new object[0]);
+ }
+ }
+ }
+ }
+ }
+ this.deletedObjects.Clear();
+ }
+
+ protected virtual void SetCollectionPropertyValue(object targetResource, PropertyInfo propertyInfo, IEnumerable propertyValue)
+ {
+ object collection;
+ ExceptionUtilities.CheckArgumentNotNull(targetResource, "targetResource");
+ ExceptionUtilities.CheckArgumentNotNull(propertyInfo, "propertyInfo");
+ ExceptionUtilities.CheckArgumentNotNull(propertyValue, "propertyValue");
+ Type collectionType = this.GetCollectionPropertyType(GetResourceTypeFullName(propertyInfo.ReflectedType), propertyInfo.Name);
+ ExceptionUtilities.CheckObjectNotNull(collectionType, "Could not infer collection type for property", new object[0]);
+ propertyValue = propertyValue.Cast<object>().Select<object, object>(delegate (object o) {
+ return UpdatableToken.ResolveIfToken(o);
+ });
+ ConstructorInfo enumerableConstructor = collectionType.GetConstructor(new Type[] { typeof(IEnumerable) });
+ if (enumerableConstructor != null)
+ {
+ collection = enumerableConstructor.Invoke(new object[] { propertyValue });
+ }
+ else if (collectionType.IsGenericType && (collectionType.GetGenericArguments().Count<Type>() == 1))
+ {
+ Type typeArgument = collectionType.GetGenericArguments().Single<Type>();
+ ConstructorInfo typedEnumerableConstructor = collectionType.GetConstructor(new Type[] { typeof(IEnumerable<>).MakeGenericType(new Type[] { typeArgument }) });
+ if (typedEnumerableConstructor != null)
+ {
+ object typedEnumerable = typeof(Enumerable).GetMethod("Cast").MakeGenericMethod(new Type[] { typeArgument }).Invoke(null, new object[] { propertyValue });
+ collection = typedEnumerableConstructor.Invoke(new object[] { typedEnumerable });
+ }
+ else
+ {
+ MethodInfo typedAddMethod = collectionType.GetMethod("Add", new Type[] { typeArgument });
+ ExceptionUtilities.CheckObjectNotNull(typedAddMethod, "Could not find constructor or add method for type: " + collectionType.FullName, new object[0]);
+ collection = Activator.CreateInstance(collectionType);
+ foreach (object element in propertyValue)
+ {
+ typedAddMethod.Invoke(collection, new object[] { element });
+ }
+ }
+ }
+ else
+ {
+ MethodInfo addMethod = collectionType.GetMethod("Add");
+ ExceptionUtilities.CheckObjectNotNull(addMethod, "Could not find constructor or add method for type: " + collectionType.FullName, new object[0]);
+ collection = Activator.CreateInstance(collectionType);
+ foreach (object element in propertyValue)
+ {
+ addMethod.Invoke(collection, new object[] { element });
+ }
+ }
+ propertyInfo.SetValue(targetResource, collection, null);
+ }
+
+ public virtual void SetConcurrencyValues(object resourceCookie, bool? checkForEquality, IEnumerable<KeyValuePair<string, object>> concurrencyValues)
+ {
+ ExceptionUtilities.CheckArgumentNotNull(resourceCookie, "resourceCookie");
+ ExceptionUtilities.ThrowDataServiceExceptionIfFalse(checkForEquality.HasValue, 0x1a1, "Missing concurrency token for update operation", new object[0]);
+ ExceptionUtilities.Assert(checkForEquality.Value, "Should not be called with check for equality parameter equal to false", new object[0]);
+ ExceptionUtilities.CheckArgumentNotNull(concurrencyValues, "concurrencyValues");
+ if (concurrencyValues.Any<KeyValuePair<string, object>>())
+ {
+ resourceCookie = UpdatableToken.AssertIsTokenAndResolve(resourceCookie, "resourceCookie");
+ ExceptionUtilities.ThrowDataServiceExceptionIfFalse(CompareETagValues(this.GetConcurrencyValues(resourceCookie), concurrencyValues), 0x19c, "Concurrency tokens do not match", new object[0]);
+ }
+ }
+
+ private void SetEntityReferenceToNullOnTargetResourceMatch(object targetResource, NavigationPropertyInfo navigationPropertyInfo, object currentEntityInstance)
+ {
+ if (navigationPropertyInfo.PropertyInfo.GetValue(currentEntityInstance, null) == targetResource)
+ {
+ this.pendingChanges.Add(delegate {
+ navigationPropertyInfo.PropertyInfo.SetValue(currentEntityInstance, null, null);
+ });
+ }
+ }
+
+ public virtual void SetReference(object targetResource, string propertyName, object propertyValue)
+ {
+ ExceptionUtilities.CheckArgumentNotNull(targetResource, "targetResource");
+ ExceptionUtilities.CheckArgumentNotNull(propertyName, "propertyName");
+ if (propertyValue != null)
+ {
+ UpdatableToken.AssertIsToken(propertyValue, "propertyValue");
+ }
+ this.SetValue(targetResource, propertyName, propertyValue);
+ }
+
+ public virtual void SetValue(object targetResource, string propertyName, object propertyValue)
+ {
+ ExceptionUtilities.CheckArgumentNotNull(targetResource, "targetResource");
+ ExceptionUtilities.CheckArgumentNotNull(propertyName, "propertyName");
+ UpdatableToken token = UpdatableToken.AssertIsToken(targetResource, "targetResource");
+ targetResource = token.Resource;
+ token.PendingPropertyUpdates[propertyName] = propertyValue;
+ this.pendingChanges.Add(delegate {
+ object generatedValue;
+ Type t = targetResource.GetType();
+ PropertyInfo pi = t.GetProperty(propertyName);
+ ExceptionUtilities.CheckObjectNotNull(pi, "Unable to find property '{0}' on type '{1}'", new object[] { propertyName, targetResource.GetType().Name });
+ if (this.TryGetStoreGeneratedValue(GetResourceTypeFullName(t), propertyName, out generatedValue))
+ {
+ propertyValue = generatedValue;
+ }
+ if (this.IsCollectionProperty(pi))
+ {
+ ExceptionUtilities.CheckObjectNotNull(propertyValue, "Collection property value was null", new object[0]);
+ IEnumerable enumerable = propertyValue as IEnumerable;
+ ExceptionUtilities.CheckObjectNotNull(enumerable, "Collection property value was not an enumerable", new object[0]);
+ this.SetCollectionPropertyValue(targetResource, pi, enumerable);
+ }
+ else
+ {
+ propertyValue = UpdatableToken.ResolveIfToken(propertyValue);
+ pi.SetValue(targetResource, propertyValue, null);
+ }
+ });
+ }
+
+ protected virtual bool TryGetStoreGeneratedValue(string fullTypeName, string propertyName, out object propertyValue)
+ {
+ propertyValue = null;
+ return false;
+ }
+
+ private void ValidateResourceType(object targetResource, string fullTypeName)
+ {
+ ExceptionUtilities.Assert(this.MetadataHelper.FindClrTypeByFullName(fullTypeName).IsAssignableFrom(targetResource.GetType()), "Invalid uri specified. expected type: '{0}', actual type: '{1}'", new object[] { fullTypeName, targetResource.GetType().FullName });
+ }
+
+ // Properties
+ internal ReflectionMetadataHelper MetadataHelper { get; set; }
+
+ internal Dictionary<string, IList> ResourceSetsStorage
+ {
+ get
+ {
+ Dictionary<string, IList> resourceSetsLookup = null;
+ Type currentContextType = base.GetType();
+ ExceptionUtilities.Assert(resourceSetsByContextTypeStorage.TryGetValue(currentContextType, out resourceSetsLookup), "Cannot find resource sets by the context type '{0}'", new object[] { currentContextType });
+ return resourceSetsLookup;
+ }
+ }
+
+ #region Inner types.
+
+ internal class ReflectionMetadataHelper
+ {
+ // Fields
+ private ReflectionDataContext reflectionDataContext;
+
+ // Methods
+ public ReflectionMetadataHelper(ReflectionDataContext reflectionDataContext)
+ {
+ this.reflectionDataContext = reflectionDataContext;
+ }
+
+ public Type FindClrTypeByFullName(string resourceTypeFullName)
+ {
+ Type type = this.reflectionDataContext.GetType().Assembly.GetTypes().Where<Type>(delegate (Type t) {
+ return (ReflectionDataContext.GetResourceTypeFullName(t) == resourceTypeFullName);
+ }).FirstOrDefault<Type>();
+ ExceptionUtilities.CheckObjectNotNull(type, "Unable to find type '{0}'", new object[] { resourceTypeFullName });
+ return type;
+ }
+
+ public string[] GetETagPropertiesOfType(string fullTypeName)
+ {
+ Type type = this.FindClrTypeByFullName(fullTypeName);
+ List<string> etags = new List<string>();
+ foreach (ETagAttribute customAttribute in type.GetCustomAttributes(typeof(ETagAttribute), true))
+ {
+ etags.AddRange(customAttribute.PropertyNames);
+ }
+
+ return etags.ToArray();
+ }
+
+ public string[] GetKeyProperties(string fullTypeName)
+ {
+ Type type = this.FindClrTypeByFullName(fullTypeName);
+ List<string> keyPropertyList = new List<string>();
+ foreach (PropertyInfo keyProperty in type.GetProperties().Where(pi => pi.Name.Contains("ID")))
+ {
+ keyPropertyList.Add(keyProperty.Name);
+ }
+
+ return keyPropertyList.ToArray();
+ }
+
+ public NavigationPropertyInfo[] GetNavigationProperties(string fullTypeName)
+ {
+ Type type = this.FindClrTypeByFullName(fullTypeName);
+ var navigationProperties = new List<NavigationPropertyInfo>();
+ var keyProperties = new List<string>(this.GetKeyProperties(fullTypeName));
+ foreach (PropertyInfo pi in type.GetProperties())
+ {
+ if (!keyProperties.Contains(pi.Name))
+ {
+ if (this.IsTypeAnEntityType(pi.PropertyType))
+ {
+ navigationProperties.Add(new NavigationPropertyInfo(pi, null));
+ }
+
+ if (pi.PropertyType.IsGenericType && ((pi.PropertyType.GetGenericTypeDefinition() == typeof(List<>)) || (pi.PropertyType.GetGenericTypeDefinition() == typeof(Collection<>))))
+ {
+ Type elementType = pi.PropertyType.GetGenericArguments()[0];
+ if (this.IsTypeAnEntityType(elementType))
+ {
+ navigationProperties.Add(new NavigationPropertyInfo(pi, elementType));
+ }
+ }
+ }
+ }
+
+ return navigationProperties.ToArray();
+ }
+
+ public string[] GetPropertiesToReset(string fullTypeName)
+ {
+ Type type = this.FindClrTypeByFullName(fullTypeName);
+ var keyProperties = new List<string>(this.GetKeyProperties(fullTypeName));
+ var navigationProperties = new List<string>(this.GetNavigationProperties(fullTypeName).Select(ni =>ni.PropertyInfo.Name));
+ return type.GetProperties().Where(
+ pi => !keyProperties.Contains(pi.Name) && !navigationProperties.Contains(pi.Name)
+ ).Select(pi => pi.Name).ToArray();
+ }
+
+ public string[] GetResourceSetNames()
+ {
+ return this.reflectionDataContext.GetType().GetProperties().Where(
+ pi => pi.PropertyType.IsGenericType && (pi.PropertyType.GetGenericTypeDefinition() == typeof(IQueryable<>))
+ ).Select(pi => pi.Name).ToArray();
+ }
+
+ public Type GetResourceTypeOfSet(string resourceSetName)
+ {
+ PropertyInfo resourceSetPropertyInfo = this.reflectionDataContext.GetType().GetProperties().Where(pi => pi.Name == resourceSetName).FirstOrDefault();
+ ExceptionUtilities.CheckObjectNotNull(resourceSetPropertyInfo, "Error finding type of set '{0}'", new object[] { resourceSetName });
+ return resourceSetPropertyInfo.PropertyType.GetGenericArguments()[0];
+ }
+
+ public bool IsTypeAnEntityType(Type t)
+ {
+ foreach (string setName in this.GetResourceSetNames())
+ {
+ if (this.GetResourceTypeOfSet(setName).IsAssignableFrom(t))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+
+ internal static class ExceptionUtilities
+ {
+ // Methods
+ public static void Assert(bool condition, string errorMessage, params object[] messageArguments)
+ {
+ if (!condition)
+ {
+ throw new InvalidOperationException("Assertion failed: " + string.Format(CultureInfo.InvariantCulture, errorMessage, messageArguments));
+ }
+ }
+
+ public static void CheckArgumentNotNull(object argument, string argumentName)
+ {
+ if (argument == null)
+ {
+ throw new ArgumentNullException(argumentName);
+ }
+ }
+
+ public static void CheckCollectionNotEmpty<TElement>(IEnumerable<TElement> argument, string argumentName)
+ {
+ CheckArgumentNotNull(argument, argumentName);
+ if (!argument.Any<TElement>())
+ {
+ throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "Collection argument '{0}' must have at least one element.", new object[] { argumentName }));
+ }
+ }
+
+ public static void CheckObjectNotNull(object value, string exceptionMessageFormatText, params object[] messageArguments)
+ {
+ Assert(exceptionMessageFormatText != null, "message cannnot be null", new object[0]);
+ Assert(messageArguments != null, "messageArguments cannnot be null", new object[0]);
+ if (value == null)
+ {
+ throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, exceptionMessageFormatText, messageArguments));
+ }
+ }
+
+ public static void ThrowDataServiceExceptionIfFalse(bool condition, int statusCode, string errorMessage, params object[] messageArguments)
+ {
+ if (!condition)
+ {
+ throw new DataServiceException(statusCode, string.Format(CultureInfo.InvariantCulture, errorMessage, messageArguments));
+ }
+ }
+ }
+
+ public class UpdatableToken
+ {
+ // Methods
+ public UpdatableToken(object resource)
+ {
+ ExceptionUtilities.CheckArgumentNotNull(resource, "resource");
+ this.Resource = resource;
+ this.PendingPropertyUpdates = new Dictionary<string, object>();
+ }
+
+ public static UpdatableToken AssertIsToken(object resource, string name)
+ {
+ ExceptionUtilities.CheckArgumentNotNull(resource, "resource");
+ UpdatableToken token = resource as UpdatableToken;
+ ExceptionUtilities.CheckObjectNotNull(token, "{0} was not a token. Type was: '{1}'", new object[] { name, resource.GetType() });
+ return token;
+ }
+
+ public static object AssertIsTokenAndResolve(object resource, string name)
+ {
+ return AssertIsToken(resource, name).Resource;
+ }
+
+ public static object ResolveIfToken(object resource)
+ {
+ UpdatableToken token = resource as UpdatableToken;
+ if (token != null)
+ {
+ resource = token.Resource;
+ }
+ return resource;
+ }
+
+ // Properties
+ public IDictionary<string, object> PendingPropertyUpdates { get; set; }
+
+ public object Resource { get; set; }
+ }
+
+ internal class NavigationPropertyInfo
+ {
+ // Methods
+ internal NavigationPropertyInfo(PropertyInfo pi, Type collectionElementType)
+ {
+ this.PropertyInfo = pi;
+ this.CollectionElementType = collectionElementType;
+ }
+
+ // Properties
+ public Type CollectionElementType { get; set; }
+
+ public PropertyInfo PropertyInfo { get; set; }
+ }
+
+ #endregion Inner types.
+ }
+}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/olingo-odata4-js/blob/d5ec5557/odatajs/tests/code/csdlreader.cs
----------------------------------------------------------------------
diff --git a/odatajs/tests/code/csdlreader.cs b/odatajs/tests/code/csdlreader.cs
new file mode 100644
index 0000000..c88333e
--- /dev/null
+++ b/odatajs/tests/code/csdlreader.cs
@@ -0,0 +1,205 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/// <summary>
+/// Class used to parse csdl to create the metatdata object
+/// </summary>
+///
+
+namespace DataJS.Tests
+{
+ using System;
+ using System.Collections.Generic;
+ using System.IO;
+ using System.Linq;
+ using System.Xml.Linq;
+
+
+ public static class CsdlReader
+ {
+ static readonly string knownNamespace = "http://docs.oasis-open.org";
+ static readonly string[] repeatingElements =
+ {
+ "Action",
+ "ActionImport",
+ "Annotation",
+ "Annotations",
+ "Apply",
+ "Binary",
+ "Bool",
+ "Cast",
+ "Collection",
+ "ComplexType",
+ "Date",
+ "DateTimeOffset",
+ "Decimal",
+ "Duration",
+ "EntitySet",
+ "EntityType",
+ "EnumMember",
+ "EnumType",
+ "Float",
+ "Function",
+ "FunctionImport",
+ "Guid",
+ "If",
+ "Int",
+ "IsOf",
+ "Key",
+ "LabeledElement",
+ "LabeledElementReference",
+ "Member",
+ "NavigationProperty",
+ "NavigationPropertyBinding",
+ "NavigationPropertyPath",
+ "Null",
+ "OnDelete",
+ "Path",
+ "Parameter",
+ "Property",
+ "PropertyPath",
+ "PropertyRef",
+ "PropertyValue",
+ "Record",
+ "ReferentialConstraint",
+ "String",
+ "Schema",
+ "Singleton",
+ "Term",
+ "TimeOfDay",
+ "TypeDefinition",
+ "UrlRef",
+ "Reference",
+ "Include",
+ "IncludeAnnotations"
+ };
+
+ public static Dictionary<string, object> ReadCsdl(TextReader payload)
+ {
+ return BuildElementJsonObject(XElement.Load(payload));
+ }
+
+ /// <summary>
+ /// Build the attribute object
+ /// </summary>
+ /// <param name="xmlAttributes">IEnumberable of XAttributes to build the attribute object</param>
+ /// <returns>The JsonObject containing the name-value pairs for an element's attributes</returns>
+ static Dictionary<string, object> BuildAttributeJsonObject(IEnumerable<XAttribute> xmlAttributes)
+ {
+ Dictionary<string, object> jsonObject = new Dictionary<string, object>();
+
+ foreach (XAttribute attribute in xmlAttributes)
+ {
+ if (!attribute.IsNamespaceDeclaration)
+ {
+ string attributeNamespace = attribute.Name.Namespace.ToString();
+ if (string.IsNullOrEmpty(attributeNamespace) ||
+ attributeNamespace.StartsWith(knownNamespace, StringComparison.InvariantCultureIgnoreCase))
+ {
+ jsonObject[MakeFirstLetterLowercase(attribute.Name.LocalName)] = attribute.Value;
+ }
+ }
+ }
+
+ return jsonObject;
+ }
+
+ /// <summary>
+ /// Creates a JsonObject from an XML container element with each attribute or subelement as a property
+ /// </summary>
+ /// <param name="container">The XML container</param>
+ /// <param name="buildValue">Function that builds a value from a property element</param>
+ /// <returns>The JsonObject containing the name-value pairs</returns>
+ public static Dictionary<string, object> BuildElementJsonObject(XElement container)
+ {
+ if (container == null)
+ {
+ return null;
+ }
+
+ Dictionary<string, object> jsonObject = new Dictionary<string, object>();
+ string keyName = MakeFirstLetterLowercase(container.Name.LocalName);
+
+ if (container.HasAttributes || container.HasElements)
+ {
+ Dictionary<string, List<Dictionary<string, object>>> repeatingObjectArrays = new Dictionary<string, List<Dictionary<string, object>>>();
+
+ jsonObject = BuildAttributeJsonObject(container.Attributes());
+
+ foreach (XElement propertyElement in container.Elements())
+ {
+ string propertyName = MakeFirstLetterLowercase(propertyElement.Name.LocalName);
+ string properyNamespace = propertyElement.Name.Namespace.ToString();
+
+ if (string.IsNullOrEmpty(properyNamespace) || properyNamespace.StartsWith(knownNamespace, StringComparison.InvariantCultureIgnoreCase))
+ {
+ // Check to see if the element is repeating and needs to be an array
+ if (repeatingElements.Contains(propertyElement.Name.LocalName))
+ {
+ // See if property was already created as an array, if not then create it
+ if (!repeatingObjectArrays.ContainsKey(propertyName))
+ {
+ repeatingObjectArrays.Add(propertyName, new List<Dictionary<string, object>>());
+ }
+
+ repeatingObjectArrays[propertyName].Add(BuildElementJsonObject(propertyElement));
+ }
+ else
+ {
+ jsonObject[propertyName] = BuildElementJsonObject(propertyElement);
+ }
+ }
+ }
+
+ foreach (string key in repeatingObjectArrays.Keys)
+ {
+ jsonObject[key] = repeatingObjectArrays[key].ToArray();
+ }
+ }
+ else
+ {
+ jsonObject[MakeFirstLetterLowercase(container.Name.LocalName)] = container.Value;
+ }
+
+ return jsonObject;
+ }
+
+ /// <summary>
+ /// Makes the first letter of a string lowercase
+ /// </summary>
+ /// <param name="name">The string to be modified</param>
+ /// <returns>Modified string</returns>
+ private static string MakeFirstLetterLowercase(string str)
+ {
+ if (!string.IsNullOrWhiteSpace(str))
+ {
+ if (str.Length > 1 && !(str[1].ToString() == str[1].ToString().ToUpper()))
+ {
+ return str[0].ToString().ToLower() + str.Substring(1);
+ }
+ else
+ {
+ return str;
+ }
+ }
+
+ return str;
+ }
+ }
+}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/olingo-odata4-js/blob/d5ec5557/odatajs/tests/code/jsdate.cs
----------------------------------------------------------------------
diff --git a/odatajs/tests/code/jsdate.cs b/odatajs/tests/code/jsdate.cs
new file mode 100644
index 0000000..06f995d
--- /dev/null
+++ b/odatajs/tests/code/jsdate.cs
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/// <summary>
+/// The oracle's representation of a Javascript date object as deserialized by the library
+/// </summary>
+
+namespace DataJS.Tests
+{
+ using System;
+ using System.Collections.Generic;
+ using System.IO;
+ using System.Linq;
+ using System.Net;
+ using System.Runtime.Serialization;
+ using System.ServiceModel;
+ using System.ServiceModel.Activation;
+ using System.ServiceModel.Syndication;
+ using System.ServiceModel.Web;
+ using System.Xml;
+ using System.Xml.Linq;
+ using Microsoft.Spatial;
+ using Microsoft.OData.Core;
+
+ [Serializable]
+ public class JsDate : JsonObject
+ {
+ private static readonly DateTime JsEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
+
+ public JsDate(DateTime dateTime)
+ : base()
+ {
+ this["milliseconds"] = dateTime.Subtract(JsEpoch).TotalMilliseconds;
+ }
+
+ public JsDate(DateTimeOffset dateTimeOffset)
+ : this(dateTimeOffset.UtcDateTime)
+ {
+ this["__edmType"] = "Edm.DateTimeOffset";
+ this["__offset"] = (dateTimeOffset.Offset < TimeSpan.Zero ? "-" : "+") + dateTimeOffset.Offset.ToString("hh':'mm");
+ }
+ }
+}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/olingo-odata4-js/blob/d5ec5557/odatajs/tests/code/jsonobject.cs
----------------------------------------------------------------------
diff --git a/odatajs/tests/code/jsonobject.cs b/odatajs/tests/code/jsonobject.cs
new file mode 100644
index 0000000..27f4b9b
--- /dev/null
+++ b/odatajs/tests/code/jsonobject.cs
@@ -0,0 +1,99 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/// <summary>
+/// A weakly typed representation of a JSON object using a dictionary implementation
+/// </summary>
+/// <typeparam name="T">The CLR type of the values of the properties</typeparam>
+
+namespace DataJS.Tests
+{
+ using System;
+ using System.Collections;
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.Runtime.Serialization;
+
+ [Serializable]
+ [KnownType(typeof(JsonObject))]
+ [KnownType(typeof(JsonObject[]))]
+ [KnownType(typeof(JsDate))]
+ [KnownType(typeof(List<object>))]
+ public class JsonObject : ISerializable, IEnumerable<KeyValuePair<string, object>>
+ {
+ Dictionary<string, object> dictionary = new Dictionary<string, object>();
+
+ public void Remove(string key)
+ {
+ dictionary.Remove(key);
+ }
+
+ public object this[string key]
+ {
+ get
+ {
+ return this.dictionary[key];
+ }
+ set
+ {
+ this.dictionary[key] = value;
+ }
+ }
+
+ public bool ContainsKey(string key)
+ {
+ return this.dictionary.ContainsKey(key);
+ }
+
+ public static JsonObject Merge(JsonObject first, JsonObject second)
+ {
+ if (first == null)
+ {
+ return second;
+ }
+
+ if (second != null)
+ {
+ JsonObject merged = new JsonObject();
+ merged.dictionary = new Dictionary<string, object>(first.dictionary);
+ foreach (var pair in second.dictionary)
+ {
+ merged.dictionary[pair.Key] = pair.Value;
+ }
+ return merged;
+ }
+ return first;
+ }
+
+ public void GetObjectData(SerializationInfo info, StreamingContext context)
+ {
+ this.dictionary.ToList().ForEach(pair => info.AddValue(pair.Key, pair.Value));
+ }
+
+ public IEnumerator<KeyValuePair<string, object>> GetEnumerator()
+ {
+ return this.dictionary.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return this.dictionary.GetEnumerator();
+ }
+ }
+}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/olingo-odata4-js/blob/d5ec5557/odatajs/tests/code/readerutils.cs
----------------------------------------------------------------------
diff --git a/odatajs/tests/code/readerutils.cs b/odatajs/tests/code/readerutils.cs
new file mode 100644
index 0000000..094a70c
--- /dev/null
+++ b/odatajs/tests/code/readerutils.cs
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+using System.Collections.Generic;
+using System.IO;
+using System.Net;
+using System.Web.Script.Serialization;
+
+namespace DataJS.Tests
+{
+ public static class ReaderUtils
+ {
+ public static JsonObject CreateEntryPropertyMetadata(string type)
+ {
+ return CreateEntryPropertyMetadata(type, true);
+ }
+
+ public static JsonObject CreateEntryPropertyMetadata(string type, bool withExtensions)
+ {
+ JsonObject json = new JsonObject();
+ json["type"] = type;
+
+
+ // TODO: add proper support for property extensions
+ if (withExtensions)
+ {
+ json["extensions"] = new JsonObject[] { };
+ }
+
+ return json;
+ }
+
+ public static JsonObject CreateExtension(string name, string nameSpace, string value)
+ {
+ JsonObject json = new JsonObject();
+ json["name"] = name;
+ json["namespaceURI"] = nameSpace;
+ json["value"] = value;
+ return json;
+ }
+
+ public static WebRequest CreateRequest(string url, string user = null, string password = null)
+ {
+ WebRequest request = WebRequest.Create(url);
+ if (user != null || password != null)
+ {
+ request.Credentials = new NetworkCredential(user, password);
+ request.PreAuthenticate = true;
+ }
+
+ return request;
+ }
+
+ public static Stream ConvertDictionarytoJsonlightStream(Dictionary<string, object> dict)
+ {
+ MemoryStream stream = new MemoryStream();
+ if (dict == null)
+ {
+ return stream;
+ }
+
+ string jsonString = new JavaScriptSerializer().Serialize(dict);
+ StreamWriter writer = new StreamWriter(stream);
+ writer.Write(jsonString);
+ writer.Flush();
+ stream.Position = 0;
+ return stream;
+ }
+
+ }
+}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/olingo-odata4-js/blob/d5ec5557/odatajs/tests/common/CacheOracle.js
----------------------------------------------------------------------
diff --git a/odatajs/tests/common/CacheOracle.js b/odatajs/tests/common/CacheOracle.js
new file mode 100644
index 0000000..d34d79f
--- /dev/null
+++ b/odatajs/tests/common/CacheOracle.js
@@ -0,0 +1,241 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+// CacheOracle.js
+// This object verifies the operation of the cache.
+// Internally it maintains a simple model of the cache implemented using a lookup array of the expected cached pages.
+
+(function (window, undefined) {
+
+ var CacheOracle = function (baseUri, pageSize, total, cacheSize) {
+ /** Creates a new CacheOracle
+ * @param {String} baseUri - The base URI of the collection
+ * @param {Integer} pageSize - The page size used in the cache
+ * @param {Integer} total - The total number of items in the collection
+ * @param {Integer} cacheSize - Cache size in bytes
+ */
+ this.baseUri = baseUri;
+ this.pageSize = pageSize;
+ this.total = total;
+ this.cacheSize = (cacheSize !== undefined) ? cacheSize : 1024 * 1024;
+ this.actualSize = 0;
+ this.actualCount = 0;
+ this.cachedPages = [];
+ this.exactPageCount = (total % pageSize === 0);
+ this.maxPage = Math.floor(total / pageSize);
+ this.overflowed = this.cacheSize === 0;
+ };
+
+ CacheOracle.mechanisms = {
+ memory: "memory",
+ indexeddb: "indexeddb",
+ dom: "dom",
+ best: "best"
+ };
+
+ CacheOracle.isMechanismAvailable = function (mechanism) {
+ /** Determines if the specified local storage mechanism is available
+ * @param mechanism - The name of the mechanism
+ * @returns Whether the mechanism is available
+ */
+ switch (mechanism) {
+ case CacheOracle.mechanisms.indexeddb:
+ if (window.msIndexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.indexedDB) {
+ return true;
+ }
+ else {
+ return false;
+ }
+ break;
+ case CacheOracle.mechanisms.dom:
+ if (window.localStorage) {
+ return true;
+ }
+ else {
+ return false;
+ }
+ break;
+ case CacheOracle.mechanisms.memory:
+ case CacheOracle.mechanisms.best:
+ case undefined:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ CacheOracle.prototype.clear = function () {
+ /** Clears the cache in the oracle
+ */
+ this.cachedPages = [];
+ this.actualSize = 0;
+ this.actualCount = 0;
+ this.overflowed = this.cacheSize === 0;
+ }
+
+ CacheOracle.prototype.verifyRequests = function (requests, responses, index, count, description, backwards, isPrefetch) {
+ /** Verifies the HTTP requests for a single data request, and updates the oracle with cached pages
+ * @param {Array} requests - The sequence of request objects (from OData.defaultHttpClient)
+ * @param {Array} responses - The sequence of response objects (from OData.defaultHttpClient)
+ * @param {Integer} index - The starting index of the read
+ * @param {Integer} count - The count of items in the read
+ * @param {String} description - The description of the requests being verified
+ * @param {Boolean} backwards - Whether or not filterBack is being verified
+ * @param {Boolean} isPrefetch - Whether the requests being verified come from the prefetcher
+ */
+ var that = this;
+
+ index = (index < 0 ? 0 : index);
+ var pageIndex = function (index) {
+ /** Returns the page index that the given item index belongs to
+ * @param {Integer} index - The item index
+ * @returns The page index
+ */
+ return Math.floor(index / that.pageSize);
+ };
+
+ var estimateSize = function (obj) {
+ /** Estimates the size of an object in bytes.
+ * @param {Object} obj - Object to determine the size of.
+ * @returns {Number} Estimated size of the object in bytes.
+ */
+
+ var size = 0;
+ var type = typeof obj;
+
+ if (type === "object" && obj) {
+ for (var name in obj) {
+ size += name.length * 2 + estimateSize(obj[name]);
+ }
+ } else if (type === "string") {
+ size = obj.length * 2;
+ } else {
+ size = 8;
+ }
+ return size;
+ };
+
+ var expectedUris = [];
+ var responseIndex = 0;
+ if (count >= 0) {
+ var minPage = pageIndex(index);
+ var maxPage = Math.min(pageIndex(index + count - 1), pageIndex(this.total));
+
+ // In the case that the index is outside the range of the collection the minPage will be greater than the maxPage
+ maxPage = Math.max(minPage, maxPage);
+
+ if (!(isPrefetch && !this.exactPageCount && minPage > this.maxPage)) {
+ for (var page = minPage; page <= maxPage && this.actualCount <= this.total && !(isPrefetch && this.overflowed); page++) {
+ if (!this.cachedPages[page]) {
+
+ expectedUris.push(that.baseUri + "?$skip=" + page * this.pageSize + "&$top=" + (this.pageSize));
+
+ var actualPageSize = 0;
+ var actualPageCount = 0;
+ if (responses[responseIndex] && responses[responseIndex].data) {
+ actualPageSize += estimateSize(responses[responseIndex].data);
+ actualPageCount += responses[responseIndex].data.value.length;
+ // Handle server paging skipToken requests
+ while (responses[responseIndex].data["@odata.nextLink"]) {
+ var nextLink = responses[responseIndex].data["@odata.nextLink"];
+ if (nextLink) {
+ var index = that.baseUri.indexOf(".svc/", 0);
+ if (index != -1) {
+ nextLink = that.baseUri.substring(0, index + 5) + nextLink;
+ }
+ }
+
+ expectedUris.push(nextLink);
+ responseIndex++;
+ actualPageSize += estimateSize(responses[responseIndex].data);
+ actualPageCount += responses[responseIndex].data.value.length;
+ }
+
+ actualPageSize += 24; // 24 byte overhead for the pages (i)ndex, and (c)ount fields
+ }
+
+ responseIndex++;
+
+ this.overflowed = this.cacheSize >= 0 && this.actualSize + actualPageSize > this.cacheSize;
+ if (!this.overflowed) {
+ this.cachedPages[page] = true;
+ this.actualSize += actualPageSize;
+ this.actualCount += actualPageCount;
+ }
+ }
+ }
+ }
+ }
+
+ if (backwards) {
+ expectedUris.reverse();
+ }
+
+ var actualUris = $.map(requests, function (r) { return r.requestUri; });
+ djstest.assertAreEqualDeep(actualUris, expectedUris, description);
+ };
+
+ CacheOracle.getExpectedFilterResults = function (data, filterIndex, filterCount, predicate, backwards) {
+ /** Verifies the cache filter returns the correct data
+ * @param {Array} collection - Array of items in the collection
+ * @param {Integer} filterIndex - The index value
+ * @param {Integer} filterCount - The count value
+ * @param {Function} predicate - Predicate to be applied in filter, takes an item
+ * @param {Boolean} backwards - Whether or not filterBackwards is being verified
+ */
+ if (!data || !data.value) {
+ return data;
+ }
+
+ var value = [];
+ if (filterCount !== 0) {
+ // Convert [item0, item1, ...] into [{ index: 0, item: item0 }, { index: 1, item: item1 }, ...]
+ var indexedCollection = $.map(data.value, function (item, index) {
+ return { index: index, item: item };
+ });
+
+ var grepPredicate = function (element, index) {
+ return predicate(element.item);
+ };
+
+ var index = filterIndex < 0 ? 0 : filterIndex;
+ var count = filterCount < 0 ? indexedCollection.length : filterCount;
+
+ value = backwards ?
+ // Slice up to 'index', filter, then slice 'count' number of items from the end
+ $.grep(indexedCollection.slice(0, index + 1), grepPredicate).slice(-count) :
+ // Slice from 'index' to the end, filter, then slice 'count' number of items from the beginning
+ $.grep(indexedCollection.slice(index), grepPredicate).slice(0, count);
+ }
+
+ var expectedResults = {};
+ for (var property in data) {
+ if (property == "value") {
+ expectedResults[property] = value;
+ } else {
+ expectedResults[property] = data[property];
+ }
+ }
+
+ return expectedResults;
+ };
+
+ window.CacheOracle = CacheOracle;
+
+})(this);
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/olingo-odata4-js/blob/d5ec5557/odatajs/tests/common/Instrument.js
----------------------------------------------------------------------
diff --git a/odatajs/tests/common/Instrument.js b/odatajs/tests/common/Instrument.js
new file mode 100644
index 0000000..fab583a
--- /dev/null
+++ b/odatajs/tests/common/Instrument.js
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+// Instrument.js
+// Instrumentation utilities
+
+(function (window, undefined) {
+
+ var warmedUp = false;
+ var getBrowserMemorySize = function (success) {
+ /** Gets the memory size (in bytes) of the browser process
+ * @param {Function} success - The success callback
+ */
+ var makeRequest = function (success) {
+ $.get("./common/Instrument.svc/GetBrowserMemorySize", function (data) {
+ success(parseInt(data));
+ }, "text");
+ };
+
+ if (window.CollectGarbage) {
+ window.CollectGarbage();
+ }
+
+ if (!warmedUp) {
+ // Make a dummy request to warm it up
+ makeRequest(function () {
+ warmedUp = true;
+ makeRequest(success);
+ });
+ } else {
+ makeRequest(success);
+ }
+ }
+
+ window.Instrument = {
+ getBrowserMemorySize: getBrowserMemorySize
+ };
+
+})(this);
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/olingo-odata4-js/blob/d5ec5557/odatajs/tests/common/Instrument.svc
----------------------------------------------------------------------
diff --git a/odatajs/tests/common/Instrument.svc b/odatajs/tests/common/Instrument.svc
new file mode 100644
index 0000000..111020f
--- /dev/null
+++ b/odatajs/tests/common/Instrument.svc
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+*/
+
+<%@ ServiceHost Language="C#" Debug="true" Factory="System.ServiceModel.Activation.WebScriptServiceHostFactory"
+ Service="DataJS.Tests.Instrument" %>
+
+namespace DataJS.Tests
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics;
+ using System.IO;
+ using System.Linq;
+ using System.Runtime.Serialization;
+ using System.ServiceModel;
+ using System.ServiceModel.Activation;
+ using System.ServiceModel.Syndication;
+ using System.ServiceModel.Web;
+ using System.Text;
+
+ /// <summary>
+ /// Instrumentation utilities
+ /// </summary>
+ [ServiceContract]
+ [ServiceBehavior(IncludeExceptionDetailInFaults = true)]
+ [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
+ public class Instrument
+ {
+ static readonly Dictionary<string, string> userAgents = new Dictionary<string,string>
+ {
+ { "MSIE", "iexplore" },
+ { "Firefox", "firefox" },
+ { "Chrome", "chrome" },
+ { "Safari", "safari" }
+ };
+
+ /// <summary>
+ /// Gets the memory size used by the browser
+ /// </summary>
+ /// <returns>The memory size used by the browser (in bytes), or zero if browser is not supported</returns>
+ [OperationContract]
+ [WebGet]
+ public Stream GetBrowserMemorySize()
+ {
+ string userAgentString = WebOperationContext.Current.IncomingRequest.UserAgent;
+ string userAgentKey = Instrument.userAgents.Keys.FirstOrDefault(ua => userAgentString.Contains(ua));
+
+ if (userAgentKey != null)
+ {
+ string processName = userAgents[userAgentKey];
+ long totalMemory = Process.GetProcessesByName(processName).Select(p => p.WorkingSet64).Sum();
+
+ return new MemoryStream(Encoding.UTF8.GetBytes(totalMemory.ToString()));
+ }
+ else
+ {
+ return new MemoryStream(Encoding.UTF8.GetBytes("0"));
+ }
+ }
+ }
+}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/olingo-odata4-js/blob/d5ec5557/odatajs/tests/common/ODataReadOracle.js
----------------------------------------------------------------------
diff --git a/odatajs/tests/common/ODataReadOracle.js b/odatajs/tests/common/ODataReadOracle.js
new file mode 100644
index 0000000..83967ce
--- /dev/null
+++ b/odatajs/tests/common/ODataReadOracle.js
@@ -0,0 +1,201 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+
+// Client for the odata.read oracle service
+
+(function (window, undefined) {
+ var jsonMime = "application/json";
+ var universalMime = "*/*";
+
+ var readFeed = function (url, success, mimeType, recognizeDates) {
+ /** Calls the ReadFeed endpoint with the specified URL
+ * @param {String} url - The URL to read the feed from
+ * @param {Function} success - The success callback function
+ * @param {String} mimeType - The MIME media type in the Accept header
+ */
+ var readMethod = getReadMethod(mimeType);
+ oracleRequest("GET", readMethod, typeof url === "string" ? { url: url} : url, mimeType, recognizeDates, function (data) {
+ success(data);
+ });
+ };
+
+ var readEntry = function (url, success, mimeType, recognizeDates) {
+ /** Calls the ReadEntry endpoint with the specified URL
+ * @param {String} url - The URL to read the entry from
+ * @param {Function} success - The success callback function
+ * @param {String} mimeType - The MIME media type in the Accept header
+ */
+ var readMethod = getReadMethod(mimeType);
+ oracleRequest("GET", readMethod, typeof url === "string" ? { url: url} : url, mimeType, recognizeDates, success);
+ };
+
+ var readLinksEntry = function (url, success) {
+ /** Calls the ReadMetadata endpoint with the specified URL
+ * @param {String} url - The URL to read the metadata from
+ * @param {Function} success - The success callback function
+ */
+ readJson(
+ url,
+ success
+ );
+ };
+
+ var readLinksFeed = function (url, success) {
+ /** Calls the ReadMetadata endpoint with the specified URL
+ * @param {String} url - The URL to read the metadata from
+ * @param {Function} success - The success callback function
+ */
+ readJson(
+ url,
+ function (data) {
+ success(data);
+ }
+ );
+ };
+
+ var readMetadata = function (url, success) {
+ /** Calls the ReadMetadata endpoint with the specified URL
+ * @param {String} url - The URL to read the metadata from
+ * @param {Function} success - The success callback function
+ */
+ oracleRequest("GET", "ReadMetadata", typeof url === "string" ? { url: url} : url, null, null, success);
+ };
+
+ var readServiceDocument = function (url, success, mimeType) {
+ /** Calls the ReadServiceDocument endpoint with the specified URL
+ * @param {String} url - The URL to the service
+ * @param {Function} success - The success callback function
+ * @param {String} mimeType - The MIME type being tested
+ */
+ var readMethod = getReadMethod(mimeType);
+ oracleRequest("GET", readMethod, typeof url === "string" ? { url: url} : url, mimeType, null, success);
+ };
+
+ var readJson = function (url, success) {
+ $.ajax({
+ url: url,
+ accepts: null,
+ dataType: "json",
+ beforeSend: function (xhr) {
+ xhr.setRequestHeader("Accept", jsonMime);
+ xhr.setRequestHeader("OData-MaxVersion", "4.0");
+ },
+ success: function (data) {
+ success(data);
+ }
+ });
+ };
+
+ var readJsonAcrossServerPages = function (url, success) {
+ var data = {};
+ var readPage = function (url) {
+ readJson(url, function (feedData) {
+ var nextLink = feedData["@odata.nextLink"];
+ if (nextLink) {
+ var index = url.indexOf(".svc/", 0);
+ if (index != -1) {
+ nextLink = url.substring(0, index + 5) + nextLink;
+ }
+ }
+
+ if (data.value && feedData.value) {
+ data.value = data.value.concat(feedData.value);
+ }
+ else {
+ for (var property in feedData) {
+ if (property != "@odata.nextLink") {
+ data[property] = feedData[property];
+ }
+ }
+ }
+
+ if (nextLink) {
+ readPage(nextLink);
+ }
+ else {
+ success(data);
+ }
+ });
+ };
+
+ readPage(url);
+ };
+
+ var getReadMethod = function (mimeType) {
+ switch (mimeType) {
+ case jsonMime:
+ case universalMime:
+ default:
+ return "ReadJson";
+ }
+ };
+
+ var oracleRequest = function (method, endpoint, data, mimeType, recognizeDates, success) {
+ /** Requests a JSON object from the oracle service, removing WCF-specific artifacts
+ * @param {String} method - The HTTP method (GET or POST)
+ * @param {String} endpoint - The oracle endpoint
+ * @param {Object} data - The data to send with the request
+ * @param {Function} reviver - The reviver function to run on each deserialized object
+ * @param {Function} success - Success callback
+ */
+ var url = "./common/ODataReadOracle.svc/" + endpoint;
+ if (mimeType) {
+ data.mimeType = mimeType;
+ }
+
+ $.ajax({
+ type: method,
+ url: url,
+ data: data,
+ dataType: "text",
+ success: function (data) {
+ var json = JSON.parse(data);
+ success(json);
+ }
+ });
+ };
+
+ var removeProperty = function (data, property) {
+ /** Removes the specified property recursively from the given object
+ * @param {Object} data - The object to operate on
+ * @param {String} property - The name of the property to remove
+ */
+ if (typeof data === "object" && data !== null) {
+ if (data[property]) {
+ delete data[property];
+ }
+
+ for (prop in data) {
+ removeProperty(data[prop], property);
+ }
+ }
+ };
+
+ window.ODataReadOracle = {
+ readFeed: readFeed,
+ readEntry: readEntry,
+ readLinksEntry: readLinksEntry,
+ readLinksFeed: readLinksFeed,
+ readJson: readJson,
+ readJsonAcrossServerPages: readJsonAcrossServerPages,
+ readMetadata: readMetadata,
+ readServiceDocument: readServiceDocument
+ };
+})(window);
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/olingo-odata4-js/blob/d5ec5557/odatajs/tests/common/ODataReadOracle.svc
----------------------------------------------------------------------
diff --git a/odatajs/tests/common/ODataReadOracle.svc b/odatajs/tests/common/ODataReadOracle.svc
new file mode 100644
index 0000000..339666c
--- /dev/null
+++ b/odatajs/tests/common/ODataReadOracle.svc
@@ -0,0 +1,114 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+*/
+
+<%@ ServiceHost Language="C#" Debug="true" Factory="System.ServiceModel.Activation.WebScriptServiceHostFactory"
+ Service="DataJS.Tests.ODataReadOracle" %>
+
+//uncomment this line to debug JSON serialization.
+//#define DEBUG_SERIALIZATION
+
+namespace DataJS.Tests
+{
+ using System;
+ using System.Collections.Generic;
+ using System.IO;
+ using System.Linq;
+ using System.Net;
+ using System.Runtime.Serialization;
+ using System.ServiceModel;
+ using System.ServiceModel.Activation;
+ using System.ServiceModel.Syndication;
+ using System.ServiceModel.Web;
+ using System.Xml;
+ using System.Xml.Linq;
+ using Microsoft.Spatial;
+ using Microsoft.OData.Core;
+ using System.Web.Script.Serialization;
+
+ /// <summary>
+ /// Oracle for the OData.read library function
+ /// </summary>
+ [ServiceContract]
+ [ServiceBehavior(IncludeExceptionDetailInFaults = true)]
+ [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
+ public class ODataReadOracle
+ {
+ const string jsonlightMediaType = "application/json";
+
+ /// <summary>
+ /// Reads a URI that will return a metadata object
+ /// </summary>
+ /// <param name="url">The URL to send the request to</param>
+ /// <returns>Stream of metadata in json light format</returns>
+ [OperationContract]
+ [WebGet]
+ public Stream ReadMetadata(string url)
+ {
+ WebResponse response = WebRequest.Create(ResolveUri(url, UriKind.Absolute)).GetResponse();
+ Dictionary<string, object> jsonObject = CsdlReader.ReadCsdl(new StreamReader(response.GetResponseStream()));
+ return ReaderUtils.ConvertDictionarytoJsonlightStream(jsonObject);
+ }
+
+ /// <summary>
+ /// Reads a URI that will get the Json response and return the stream
+ /// </summary>
+ /// <param name="url">URL of the entry</param>
+ /// <param name="user">The username for basic authentication</param>
+ /// <param name="password">The password for basic authentication</param>
+ /// <returns>Stream of the Json response expected to be returned by OData.read</returns>
+ [OperationContract]
+ [WebGet(ResponseFormat = WebMessageFormat.Json)]
+ public Stream ReadJson(string url, string mimeType, string user, string password)
+ {
+ if (mimeType == null)
+ {
+ mimeType = jsonlightMediaType + ";odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=false;charset=utf-8";
+ }
+
+ HttpWebRequest request = (HttpWebRequest)ReaderUtils.CreateRequest(ResolveUri(url, UriKind.Absolute), user, password);
+ request.Accept = mimeType;
+ WebResponse response = request.GetResponse();
+
+ return response.GetResponseStream();
+ }
+
+ /// <summary>
+ /// Resolves the given url string to a URI
+ /// </summary>
+ /// <param name="url">The given URL string</param>
+ /// <param name="urlKind">URI kind to resolve to</param>
+ /// <returns>The resolved URI</returns>
+ private static string ResolveUri(string url, UriKind uriKind)
+ {
+ Uri resolvedUri = new Uri(url, UriKind.RelativeOrAbsolute);
+ if (!resolvedUri.IsAbsoluteUri)
+ {
+ // If the given URI is relative, then base it on the Referer URI
+ Uri baseUri = new Uri(WebOperationContext.Current.IncomingRequest.Headers["Referer"]);
+ resolvedUri = new Uri(baseUri, resolvedUri);
+ if (uriKind == UriKind.Relative)
+ {
+ resolvedUri = baseUri.MakeRelativeUri(resolvedUri);
+ }
+ }
+
+ return resolvedUri.ToString();
+ }
+ }
+}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/olingo-odata4-js/blob/d5ec5557/odatajs/tests/common/ObservableHttpClient.js
----------------------------------------------------------------------
diff --git a/odatajs/tests/common/ObservableHttpClient.js b/odatajs/tests/common/ObservableHttpClient.js
new file mode 100644
index 0000000..9be75f8
--- /dev/null
+++ b/odatajs/tests/common/ObservableHttpClient.js
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+// ObservableHttpClient.js
+// This object extends OData's default httpClient by supporting request and response recording sessions, and firing a custom
+// JQuery event for each request/response.
+//
+// The events fired by this object are:
+// request: Before a request is made
+// success: Before the primary success handler is called
+//
+// To bind to an event, JQuery event attachers can be used on the object, e.g.
+// $(observableHttpClient).bind("request", function (request) { ... });
+//
+// To begin a new recording session, use:
+// var session = observableHttpClient.newSession();
+//
+// Requests and responses are then recorded in session.requests and session.responses. Session can be ended by session.end().
+// Multiple simultaneous sessions are supported.
+
+(function (window, undefined) {
+
+ var ObservableHttpClient = function (provider) {
+ this.provider = provider ? provider : window.odatajs.oData.net.defaultHttpClient;
+ };
+
+ ObservableHttpClient.prototype.newSession = function () {
+ return new Session(this);
+ };
+
+ ObservableHttpClient.prototype.request = function (request, success, error) {
+ var that = this;
+
+ $(this).triggerHandler("request", request);
+ return this.provider.request(request, function (response) {
+ $(that).triggerHandler("success", response);
+ success(response);
+ }, error);
+ };
+
+
+ var Session = function (client) {
+ var that = this;
+
+ this.client = client;
+ this.clear();
+
+ this.requestHandler = function (event, request) { that.requests.push(request); };
+ $(client).bind("request", this.requestHandler);
+
+ this.successHandler = function (event, response) { that.responses.push(response); };
+ $(client).bind("success", this.successHandler);
+ };
+
+ Session.prototype.clear = function () {
+ this.requests = [];
+ this.responses = [];
+ }
+
+ Session.prototype.end = function () {
+ $(this.client).unbind("request", this.requestHandler);
+ $(this.client).unbind("success", this.successHandler);
+ };
+
+ window.ObservableHttpClient = ObservableHttpClient;
+ window.Session = Session;
+
+})(this);
\ No newline at end of file