- added object cache implementation
- bug fixes

git-svn-id: https://svn.apache.org/repos/asf/incubator/chemistry/dotcmis/trunk@1069900 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/DotCMIS.suo b/DotCMIS.suo
deleted file mode 100644
index 6b2db92..0000000
--- a/DotCMIS.suo
+++ /dev/null
Binary files differ
diff --git a/DotCMIS/DotCMIS.csproj b/DotCMIS/DotCMIS.csproj
index 7009548..f4b2d58 100644
--- a/DotCMIS/DotCMIS.csproj
+++ b/DotCMIS/DotCMIS.csproj
@@ -76,6 +76,7 @@
       <DesignTime>True</DesignTime>

       <DependentUpon>Reference.svcmap</DependentUpon>

     </Compile>

+    <Compile Include="util.cs" />

   </ItemGroup>

   <ItemGroup />

   <ItemGroup>

diff --git a/DotCMIS/binding/binding-caches.cs b/DotCMIS/binding/binding-caches.cs
index 42ef619..91de73d 100644
--- a/DotCMIS/binding/binding-caches.cs
+++ b/DotCMIS/binding/binding-caches.cs
@@ -24,6 +24,7 @@
 using System.Threading;

 using DotCMIS.Binding.Impl;

 using DotCMIS.Data;

+using DotCMIS.Util;

 

 namespace DotCMIS.Binding

 {

@@ -350,11 +351,10 @@
         }

     }

 

-    internal abstract class AbstractDictionaryCacheLevel : IBindingCacheLevel

+    internal abstract class AbstractCacheLevel : IBindingCacheLevel

     {

         protected static string NullKey = "";

 

-        private IDictionary<string, object> dict;

         private bool fallbackEnabled = false;

         private string fallbackKey = null;

         private bool singleValueEnabled = false;

@@ -365,42 +365,48 @@
         {

             get

             {

-                object value = null;

-                if (dict.TryGetValue(key == null ? NullKey : key, out value))

+                object value = GetValue(key == null ? NullKey : key);

+                if (value != null)

                 {

                     return value;

                 }

 

-                if (fallbackEnabled && dict.TryGetValue(fallbackKey, out value))

+                if (fallbackEnabled)

                 {

-                    return value;

+                    value = GetValue(fallbackKey);

+                    if (value != null)

+                    {

+                        return value;

+                    }

                 }

 

-                if (singleValueEnabled && dict.Count == 1)

+                if (singleValueEnabled)

                 {

-                    value = dict.Values.First();

+                    value = GetSingleValue();

+                    if (value != null)

+                    {

+                        return value;

+                    }

                 }

 

-                return value;

+                return null;

             }

             set

             {

                 if (value != null)

                 {

-                    dict[key == null ? NullKey : key] = value;

+                    AddValue(key == null ? NullKey : key, value);

                 }

             }

         }

 

-        public virtual void Remove(string key)

-        {

-            dict.Remove(key);

-        }

+        public abstract void Remove(string key);

 

-        public void SetDictionary(IDictionary<string, object> dict)

-        {

-            this.dict = dict;

-        }

+        protected abstract object GetValue(string key);

+

+        protected abstract object GetSingleValue();

+

+        protected abstract void AddValue(string key, object value);

 

         protected void EnableKeyFallback(string key)

         {

@@ -470,91 +476,93 @@
         }

     }

 

-    internal class DictionaryCacheLevel : AbstractDictionaryCacheLevel

+    internal class DictionaryCacheLevel : AbstractCacheLevel

     {

         public const string Capacity = "capacity";

         public const string SingleValue = "singleValue";

 

+        private IDictionary<string, object> dict;

+

         public override void Initialize(IDictionary<string, string> parameters)

         {

             int initialCapacity = GetIntParameter(parameters, Capacity, 32);

             bool singleValue = GetBooleanParameter(parameters, SingleValue, false);

 

-            SetDictionary(new Dictionary<string, object>(initialCapacity));

+            dict = new Dictionary<string, object>(initialCapacity);

             if (singleValue)

             {

                 EnableSingeValueFallback();

             }

         }

+

+        public override void Remove(string key)

+        {

+            dict.Remove(key);

+        }

+

+        protected override object GetValue(string key)

+        {

+            object value;

+            if (dict.TryGetValue(key, out value))

+            {

+                return value;

+            }

+

+            return null;

+        }

+

+        protected override object GetSingleValue()

+        {

+            if (dict.Count == 1)

+            {

+                return dict.Values.First();

+            }

+

+            return null;

+        }

+

+        protected override void AddValue(string key, object value)

+        {

+            dict[key] = value;

+        }

     }

 

-    internal class LruCacheLevel : AbstractDictionaryCacheLevel

+    internal class LruCacheLevel : AbstractCacheLevel

     {

         public const string MaxEntries = "maxEntries";

 

-        private LinkedList<string> keyList;

-        private int maxEntries;

+        private LRUCache<string, object> cache;

 

         public override void Initialize(IDictionary<string, string> parameters)

         {

-            maxEntries = GetIntParameter(parameters, MaxEntries, 100);

-            keyList = new LinkedList<string>();

-            SetDictionary(new Dictionary<string, object>(maxEntries + 1));

-        }

+            int maxEntries = GetIntParameter(parameters, MaxEntries, 100);

 

-        public override object this[string key]

-        {

-            get

-            {

-                object value = base[key];

-                if (value != null)

-                {

-                    LinkedListNode<string> node = keyList.Find(key);

-                    if (node == null)

-                    {

-                        throw new ApplicationException("Cache error!");

-                    }

-                    else

-                    {

-                        keyList.Remove(node);

-                        keyList.AddFirst(node);

-                    }

-                }

-

-                return value;

-            }

-            set

-            {

-                if (value == null)

-                {

-                    return;

-                }

-

-                LinkedListNode<string> node = keyList.Find(key);

-                if (node == null)

-                {

-                    keyList.AddFirst(key);

-                    while (keyList.Count > maxEntries)

-                    {

-                        LinkedListNode<string> lastNode = keyList.Last;

-                        base.Remove(lastNode.Value);

-                        keyList.RemoveLast();

-                    }

-                }

-                else

-                {

-                    keyList.Remove(node);

-                    keyList.AddFirst(node);

-                }

-

-                base[key] = value;

-            }

+            cache = new LRUCache<string, object>(maxEntries, TimeSpan.FromDays(1));

         }

 

         public override void Remove(string key)

         {

-            keyList.Remove(key);

-            base.Remove(key);

+            cache.Remove(key);

+        }

+

+        protected override object GetValue(string key)

+        {

+            return cache.Get(key);

+        }

+

+        protected override object GetSingleValue()

+        {

+            if (cache.Count == 1)

+            {

+                return cache.GetLatest();

+            }

+

+            return null;

+        }

+

+        protected override void AddValue(string key, object value)

+        {

+            cache.Add(key, value);

         }

     }

 

diff --git a/DotCMIS/client/client-caches.cs b/DotCMIS/client/client-caches.cs
index 7c103ba..b5121a7 100644
--- a/DotCMIS/client/client-caches.cs
+++ b/DotCMIS/client/client-caches.cs
@@ -20,6 +20,8 @@
 using System.Collections.Generic;

 using System.Linq;

 using System.Text;

+using System.Threading;

+using DotCMIS.Util;

 

 namespace DotCMIS.Client.Impl.Cache

 {

@@ -54,4 +56,253 @@
         public void Clear() { }

         public int CacheSize { get { return 0; } }

     }

+

+    public class CmisObjectCache : ICache

+    {

+        private int cacheSize;

+        private int cacheTtl;

+        private int pathToIdSize;

+        private int pathToIdTtl;

+

+        private LRUCache<string, IDictionary<string, ICmisObject>> objectCache;

+        private LRUCache<string, string> pathToIdCache;

+

+        private object cacheLock = new object();

+

+        public CmisObjectCache() { }

+

+        public void Initialize(ISession session, IDictionary<string, string> parameters)

+        {

+            Lock();

+            try

+            {

+                // cache size

+                cacheSize = 1000;

+                try

+                {

+                    string cacheSizeStr;

+                    if (parameters.TryGetValue(SessionParameter.CacheSizeObjects, out cacheSizeStr))

+                    {

+                        cacheSize = Int32.Parse(cacheSizeStr);

+                        if (cacheSize < 0)

+                        {

+                            cacheSize = 0;

+                        }

+                    }

+                }

+                catch (Exception) { }

+

+                // cache time-to-live

+                cacheTtl = 2 * 60 * 60 * 1000;

+                try

+                {

+                    string cacheTtlStr;

+                    if (parameters.TryGetValue(SessionParameter.CacheTTLObjects, out cacheTtlStr))

+                    {

+                        cacheTtl = Int32.Parse(cacheTtlStr);

+                        if (cacheTtl < 0)

+                        {

+                            cacheTtl = 2 * 60 * 60 * 1000;

+                        }

+                    }

+                }

+                catch (Exception) { }

+

+                // path-to-id size

+                pathToIdSize = 1000;

+                try

+                {

+                    string pathToIdSizeStr;

+                    if (parameters.TryGetValue(SessionParameter.CacheSizePathToId, out pathToIdSizeStr))

+                    {

+                        pathToIdSize = Int32.Parse(pathToIdSizeStr);

+                        if (pathToIdSize < 0)

+                        {

+                            pathToIdSize = 0;

+                        }

+                    }

+                }

+                catch (Exception) { }

+

+                // path-to-id time-to-live

+                pathToIdTtl = 30 * 60 * 1000;

+                try

+                {

+                    string pathToIdTtlStr;

+                    if (parameters.TryGetValue(SessionParameter.CacheTTLPathToId, out pathToIdTtlStr))

+                    {

+                        pathToIdTtl = Int32.Parse(pathToIdTtlStr);

+                        if (pathToIdTtl < 0)

+                        {

+                            pathToIdTtl = 30 * 60 * 1000;

+                        }

+                    }

+                }

+                catch (Exception) { }

+

+                InitializeInternals();

+            }

+            finally

+            {

+                Unlock();

+            }

+        }

+

+        private void InitializeInternals()

+        {

+            Lock();

+            try

+            {

+                objectCache = new LRUCache<string, IDictionary<string, ICmisObject>>(cacheSize, TimeSpan.FromMilliseconds(cacheTtl));

+                pathToIdCache = new LRUCache<string, string>(pathToIdSize, TimeSpan.FromMilliseconds(pathToIdTtl));

+            }

+            finally

+            {

+                Unlock();

+            }

+        }

+

+        public void Clear()

+        {

+            InitializeInternals();

+        }

+

+        public bool ContainsId(string objectId, string cacheKey)

+        {

+            Lock();

+            try

+            {

+                return objectCache.Get(objectId) != null;

+            }

+            finally

+            {

+                Unlock();

+            }

+        }

+

+        public bool ContainsPath(string path, string cacheKey)

+        {

+            Lock();

+            try

+            {

+                return pathToIdCache.Get(path) != null;

+            }

+            finally

+            {

+                Unlock();

+            }

+        }

+

+        public ICmisObject GetById(string objectId, string cacheKey)

+        {

+            Lock();

+            try

+            {

+                IDictionary<string, ICmisObject> cacheKeyDict = objectCache.Get(objectId);

+                if (cacheKeyDict == null)

+                {

+                    return null;

+                }

+

+                ICmisObject cmisObject;

+                if (cacheKeyDict.TryGetValue(cacheKey, out cmisObject))

+                {

+                    return cmisObject;

+                }

+

+                return null;

+            }

+            finally

+            {

+                Unlock();

+            }

+        }

+

+        public ICmisObject GetByPath(string path, string cacheKey)

+        {

+            Lock();

+            try

+            {

+                string id = pathToIdCache.Get(path);

+                if (id == null)

+                {

+                    return null;

+                }

+

+                return GetById(id, cacheKey);

+            }

+            finally

+            {

+                Unlock();

+            }

+        }

+

+        public void Put(ICmisObject cmisObject, string cacheKey)

+        {

+            // no object, no id, no cache key - no cache

+            if (cmisObject == null || cmisObject.Id == null || cacheKey == null)

+            {

+                return;

+            }

+

+            Lock();

+            try

+            {

+                IDictionary<string, ICmisObject> cacheKeyDict = objectCache.Get(cmisObject.Id);

+                if (cacheKeyDict == null)

+                {

+                    cacheKeyDict = new Dictionary<string, ICmisObject>();

+                    objectCache.Add(cmisObject.Id, cacheKeyDict);

+                }

+

+                cacheKeyDict[cacheKey] = cmisObject;

+

+                // folders may have a path, use it!

+                string path = cmisObject.GetPropertyValue(PropertyIds.Path) as string;

+                if (path != null)

+                {

+                    pathToIdCache.Add(path, cmisObject.Id);

+                }

+            }

+            finally

+            {

+                Unlock();

+            }

+        }

+

+        public void PutPath(string path, ICmisObject cmisObject, string cacheKey)

+        {

+            // no path, no object, no id, no cache key - no cache

+            if (path ==null || cmisObject == null || cmisObject.Id == null || cacheKey == null)

+            {

+                return;

+            }

+

+            Lock();

+            try

+            {

+                Put(cmisObject, cacheKey);

+                pathToIdCache.Add(path, cmisObject.Id);

+            }

+            finally

+            {

+                Unlock();

+            }

+        }

+

+        public int CacheSize

+        {

+            get { return cacheSize; }

+        }

+

+        protected void Lock()

+        {

+            Monitor.Enter(cacheLock);

+        }

+

+        protected void Unlock()

+        {

+            Monitor.Exit(cacheLock);

+        }

+    }

 }

diff --git a/DotCMIS/client/client-impl.cs b/DotCMIS/client/client-impl.cs
index 4bd58dd..7461469 100644
--- a/DotCMIS/client/client-impl.cs
+++ b/DotCMIS/client/client-impl.cs
@@ -257,7 +257,7 @@
                 }

                 else

                 {

-                    cacheType = typeof(NoCache);

+                    cacheType = typeof(CmisObjectCache);

                 }

 

                 ICache cacheObject = Activator.CreateInstance(cacheType) as ICache;

diff --git a/DotCMIS/client/client-intf.cs b/DotCMIS/client/client-intf.cs
index 329d702..930fb96 100644
--- a/DotCMIS/client/client-intf.cs
+++ b/DotCMIS/client/client-intf.cs
@@ -99,7 +99,21 @@
     }

 

     /// <summary>

-    /// Session interface.

+    /// A session is a connection to a CMIS repository with a specific user.

+    /// <para>

+    /// Not all operations might be supported by the connected repository. Either DotCMIS or the repository will 

+    /// throw an exception if an unsupported operation is called. 

+    /// The capabilities of the repository can be discovered by evaluating the repository info 

+    /// (see <see cref="RepositoryInfo"/>).

+    /// </para>

+    /// <para>

+    /// Almost all methods might throw exceptions derived from <see cref="DotCMIS.Exceptions.CmisBaseException"/>!

+    /// </para>

+    /// <para>

+    /// (Please refer to the <a href="http://docs.oasis-open.org/cmis/CMIS/v1.0/os/">CMIS specification</a>

+    /// for details about the domain model, terms, concepts, base types, properties, ids and query names, 

+    /// query language, etc.)

+    /// </para>

     /// </summary>

     public interface ISession

     {

@@ -336,17 +350,71 @@
     /// </summary>

     public interface IProperty

     {

+        /// <summary>

+        /// Gets the property id.

+        /// </summary>

         string Id { get; }

+

+        /// <summary>

+        /// Gets the property local name.

+        /// </summary>

         string LocalName { get; }

+

+        /// <summary>

+        /// Gets the property display name.

+        /// </summary>

         string DisplayName { get; }

+

+        /// <summary>

+        /// Gets the property query name.

+        /// </summary>

         string QueryName { get; }

+

+        /// <summary>

+        /// Gets if the property is a multi-value proprty.

+        /// </summary>

         bool IsMultiValued { get; }

+

+        /// <summary>

+        /// Gets the proprty type.

+        /// </summary>

         PropertyType? PropertyType { get; }

+

+        /// <summary>

+        /// Gets the property defintion.

+        /// </summary>

         IPropertyDefinition PropertyDefinition { get; }

+

+        /// <summary>

+        /// Gets the value of the property.

+        /// </summary>

+        /// <remarks>

+        /// If the property is a single-value property the single value is returned.

+        /// If the property is a multi-value property a IList&lt;object&gt; is returned.

+        /// </remarks>

         object Value { get; }

+

+        /// <summary>

+        /// Gets the value list of the property.

+        /// </summary>

+        /// <remarks>

+        /// If the property is a single-value property a list with one or no items is returend.

+        /// </remarks>

         IList<object> Values { get; }

+

+        /// <summary>

+        /// Gets the first value of the value list or <c>null</c> if the list has no values.

+        /// </summary>

         object FirstValue { get; }

+

+        /// <summary>

+        /// Gets a string representation of the first value of the value list.

+        /// </summary>

         string ValueAsString { get; }

+

+        /// <summary>

+        /// Gets a string representation of the value list.

+        /// </summary>

         string ValuesAsString { get; }

     }

 

@@ -430,17 +498,45 @@
     /// </summary>

     public interface ICmisObject : IObjectId, ICmisObjectProperties

     {

-        // object

+        /// <summary>

+        /// Gets the allowable actions if they have been fetched for this object.

+        /// </summary>

         IAllowableActions AllowableActions { get; }

+

+        /// <summary>

+        /// Gets the relationships if they have been fetched for this object.

+        /// </summary>

         IList<IRelationship> Relationships { get; }

+

+        /// <summary>

+        /// Gets the ACL if it has been fetched for this object.

+        /// </summary>

         IAcl Acl { get; }

 

-        // object service

+        /// <summary>

+        /// Deletes this object.

+        /// </summary>

+        /// <param name="allVersions">if this object is a document this parameter defines if just this version or all versions should be deleted</param>

         void Delete(bool allVersions);

+

+        /// <summary>

+        /// Updates the properties that are provided.

+        /// </summary>

+        /// <param name="properties">the properties to update</param>

+        /// <returns>the updated object (a repository might have created a new object)</returns>

         ICmisObject UpdateProperties(IDictionary<string, object> properties);

+

+        /// <summary>

+        /// Updates the properties that are provided.

+        /// </summary>

+        /// <param name="properties">the properties to update</param>

+        /// <param name="refresh">indicates if the object should be refresh after the update</param>

+        /// <returns>the object id of the updated object (a repository might have created a new object)</returns>

         IObjectId UpdateProperties(IDictionary<string, object> properties, bool refresh);

 

-        // renditions

+        /// <summary>

+        /// Gets the renditions if they have been fetched for this object.

+        /// </summary>

         IList<IRendition> Renditions { get; }

 

         // policy service

@@ -456,8 +552,19 @@
         // extensions

         IList<ICmisExtensionElement> GetExtensions(ExtensionLevel level);

 

+        /// <summary>

+        /// Gets the timestamp of the last refresh.

+        /// </summary>

         DateTime RefreshTimestamp { get; }

+

+        /// <summary>

+        /// Reloads the data from the repository.

+        /// </summary>

         void Refresh();

+

+        /// <summary>

+        /// Reloads the data from the repository if the last refresh did not occur within <c>durationInMillis</c>.

+        /// </summary>

         void RefreshIfOld(long durationInMillis);

     }

 

diff --git a/DotCMIS/client/client-objectfactory.cs b/DotCMIS/client/client-objectfactory.cs
index 50863c8..85216f2 100644
--- a/DotCMIS/client/client-objectfactory.cs
+++ b/DotCMIS/client/client-objectfactory.cs
@@ -210,7 +210,13 @@
             // get the type

             if (type == null)

             {

-                string typeId = properties[PropertyIds.ObjectTypeId] as string;

+                object typeIdObject;

+                if (!properties.TryGetValue(PropertyIds.ObjectTypeId, out typeIdObject))

+                {

+                    throw new ArgumentException("Type or type property must be set!");

+                }

+

+                string typeId = typeIdObject as string;

                 if (typeId == null)

                 {

                     throw new ArgumentException("Type or type property must be set!");

diff --git a/DotCMIS/const.cs b/DotCMIS/const.cs
index cade7fe..1f09da3 100644
--- a/DotCMIS/const.cs
+++ b/DotCMIS/const.cs
@@ -66,6 +66,10 @@
         public const string CacheClass = "org.apache.chemistry.dotcmis.cache.classname";

         public const string RepositoryId = "org.apache.chemistry.dotcmis.session.repository.id";

 

+        public const string CacheSizeObjects = "org.apache.chemistry.dotcmis.cache.objects.size";

+        public const string CacheTTLObjects = "org.apache.chemistry.dotcmis.cache.objects.ttl";

+        public const string CacheSizePathToId = "org.apache.chemistry.dotcmis.cache.pathtoid.size";

+        public const string CacheTTLPathToId = "org.apache.chemistry.dotcmis.cache.pathtoid.ttl";

         public const string CachePathOmit = "org.apache.chemistry.dotcmis.cache.path.omit";

     }

 

diff --git a/DotCMIS/util.cs b/DotCMIS/util.cs
new file mode 100644
index 0000000..6264482
--- /dev/null
+++ b/DotCMIS/util.cs
@@ -0,0 +1,132 @@
+/*

+ * 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;

+using System.Collections.Generic;

+using System.Linq;

+using System.Text;

+

+namespace DotCMIS.Util

+{

+    /// <summary>

+    /// LRU cache implementation. Not thread safe!

+    /// </summary>

+    internal class LRUCache<K, V>

+    {

+        private int capacity;

+        private TimeSpan ttl;

+        private Dictionary<K, LinkedListNode<LRUCacheItem<K, V>>> cacheDict = new Dictionary<K, LinkedListNode<LRUCacheItem<K, V>>>();

+        private LinkedList<LRUCacheItem<K, V>> lruList = new LinkedList<LRUCacheItem<K, V>>();

+

+        public LRUCache(int capacity, TimeSpan ttl)

+        {

+            this.capacity = capacity;

+            this.ttl = ttl;

+        }

+

+        public V Get(K key)

+        {

+            LinkedListNode<LRUCacheItem<K, V>> node;

+            if (cacheDict.TryGetValue(key, out node))

+            {

+                lruList.Remove(node);

+

+                if (node.Value.IsExpired)

+                {

+                    cacheDict.Remove(node.Value.Key);

+                    return default(V);

+                }

+

+                lruList.AddLast(node);

+

+                return node.Value.Value;

+            }

+

+            return default(V);

+        }

+

+        public V GetLatest()

+        {

+            if (lruList.Count == 0)

+            {

+                return default(V);

+            }

+

+            return lruList.First().Value;

+        }

+

+        public void Add(K key, V val)

+        {

+            Remove(key);

+

+            if (cacheDict.Count >= capacity)

+            {

+                RemoveFirst();

+            }

+

+            LRUCacheItem<K, V> cacheItem = new LRUCacheItem<K, V>(key, val, DateTime.UtcNow + ttl);

+            LinkedListNode<LRUCacheItem<K, V>> node = new LinkedListNode<LRUCacheItem<K, V>>(cacheItem);

+

+            lruList.AddLast(node);

+            cacheDict.Add(key, node);

+        }

+

+        protected void RemoveFirst()

+        {

+            LinkedListNode<LRUCacheItem<K, V>> node = lruList.First;

+            lruList.RemoveFirst();

+            cacheDict.Remove(node.Value.Key);

+        }

+

+        public void Remove(K key)

+        {

+            LinkedListNode<LRUCacheItem<K, V>> node;

+            if (cacheDict.TryGetValue(key, out node))

+            {

+                lruList.Remove(node);

+                cacheDict.Remove(node.Value.Key);

+            }

+        }

+

+        public int Count

+        {

+            get { return lruList.Count; }

+        }

+    }

+

+    internal class LRUCacheItem<K, V>

+    {

+        public LRUCacheItem(K key, V value, DateTime expiration)

+        {

+            Key = key;

+            Value = value;

+            Expiration = expiration;

+        }

+

+        public K Key { get; private set; }

+

+        public V Value { get; private set; }

+

+        public DateTime Expiration { get; private set; }

+

+        public bool IsExpired

+        {

+            get { return Value == null || DateTime.UtcNow > Expiration; }

+        }

+    }

+}

diff --git a/DotCMISUnitTest/CacheTest.cs b/DotCMISUnitTest/CacheTest.cs
new file mode 100644
index 0000000..ac63bda
--- /dev/null
+++ b/DotCMISUnitTest/CacheTest.cs
@@ -0,0 +1,208 @@
+/*

+ * 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;

+using System.Collections.Generic;

+using DotCMIS.Client;

+using DotCMIS.Client.Impl.Cache;

+using DotCMIS.Data;

+using DotCMIS.Data.Extensions;

+using DotCMIS.Enums;

+using NUnit.Framework;

+using DotCMIS;

+using System.Threading;

+

+namespace DotCMISUnitTest

+{

+    [TestFixture]

+    class CacheTest

+    {

+        [Test]

+        public void TestCache()

+        {

+            IDictionary<string, string> parameters = new Dictionary<string, string>();

+

+            CmisObjectCache cache = new CmisObjectCache();

+            cache.Initialize(null, parameters);

+

+            string cacheKey1 = "ck1";

+            string cacheKey2 = "ck2";

+

+            // add first object

+            MockObject mock1 = new MockObject("1");

+            cache.Put(mock1, cacheKey1);

+

+            ICmisObject o1 = cache.GetById(mock1.Id, cacheKey1);

+            Assert.NotNull(o1);

+            Assert.AreEqual(mock1.Id, o1.Id);

+            Assert.Null(cache.GetById(mock1.Id, cacheKey2));

+            Assert.Null(cache.GetById("- some id -", cacheKey1));

+

+            // add second object - same id

+            MockObject mock2 = new MockObject("1");

+            cache.Put(mock2, cacheKey2);

+

+            o1 = cache.GetById(mock1.Id, cacheKey1);

+            Assert.NotNull(o1);

+            Assert.AreEqual(mock1.Id, o1.Id);

+

+            ICmisObject o2 = cache.GetById(mock2.Id, cacheKey2);

+            Assert.NotNull(o2);

+            Assert.AreEqual(mock2.Id, o2.Id);

+

+            // add third object - other id

+

+            MockObject mock3 = new MockObject("3");

+            cache.Put(mock3, cacheKey1);

+

+            o1 = cache.GetById(mock1.Id, cacheKey1);

+            Assert.NotNull(o1);

+            Assert.AreEqual(mock1.Id, o1.Id);

+

+            o2 = cache.GetById(mock2.Id, cacheKey2);

+            Assert.NotNull(o2);

+            Assert.AreEqual(mock2.Id, o2.Id);

+

+            ICmisObject o3 = cache.GetById(mock3.Id, cacheKey1);

+            Assert.NotNull(o3);

+            Assert.AreEqual(mock3.Id, o3.Id);

+        }

+

+        [Test]

+        public void TestLRU()

+        {

+            IDictionary<string, string> parameters = new Dictionary<string, string>();

+            parameters[SessionParameter.CacheSizeObjects] = "10";

+

+            CmisObjectCache cache = new CmisObjectCache();

+            cache.Initialize(null, parameters);

+

+            string cacheKey1 = "ck1";

+

+            MockObject[] mocks = new MockObject[10];

+            for (int i = 0; i < 10; i++)

+            {

+                mocks[i] = new MockObject("m" + i);

+                cache.Put(mocks[i], cacheKey1);

+            }

+

+            for (int i = 0; i < 10; i++)

+            {

+                mocks[i] = new MockObject("m" + i);

+                Assert.NotNull(cache.GetById("m" + i, cacheKey1));

+            }

+

+            MockObject newMock = new MockObject("new");

+            cache.Put(newMock, cacheKey1);

+            Assert.NotNull(cache.GetById(newMock.Id, cacheKey1));

+

+            for (int i = 1; i < 10; i++)

+            {

+                mocks[i] = new MockObject("m" + i);

+                Assert.NotNull(cache.GetById("m" + i, cacheKey1));

+            }

+

+            Assert.Null(cache.GetById("m0", cacheKey1));

+        }

+

+        [Test]

+        public void TestExpiration()

+        {

+            IDictionary<string, string> parameters = new Dictionary<string, string>();

+            parameters[SessionParameter.CacheTTLObjects] = "500";

+

+            CmisObjectCache cache = new CmisObjectCache();

+            cache.Initialize(null, parameters);

+

+            string cacheKey1 = "ck1";

+

+            MockObject mock1 = new MockObject("1");

+            cache.Put(mock1, cacheKey1);

+

+            Assert.NotNull(cache.GetById(mock1.Id, cacheKey1));

+

+            Thread.Sleep(TimeSpan.FromSeconds(1));

+

+            Assert.Null(cache.GetById(mock1.Id, cacheKey1));

+        }

+

+        [Test]

+        public void TestPath()

+        {

+            IDictionary<string, string> parameters = new Dictionary<string, string>();

+

+            CmisObjectCache cache = new CmisObjectCache();

+            cache.Initialize(null, parameters);

+

+            string cacheKey1 = "ck1";

+            string path = "/test/path";

+

+            MockObject mock1 = new MockObject("1");

+            cache.PutPath(path, mock1, cacheKey1);

+

+            ICmisObject o1 = cache.GetById(mock1.Id, cacheKey1);

+            Assert.NotNull(o1);

+            Assert.AreEqual(mock1.Id, o1.Id);

+

+            ICmisObject o2 = cache.GetByPath(path, cacheKey1);

+            Assert.NotNull(o2);

+            Assert.AreEqual(mock1.Id, o2.Id);

+

+            Assert.Null(cache.GetByPath("/some/other/path/", cacheKey1));

+        }

+    }

+

+    class MockObject : ICmisObject

+    {

+        public MockObject(string id)

+        {

+            Id = id;

+        }

+

+        public string Id { get; private set; }

+        public IList<IProperty> Properties { get { return null; } }

+        public IProperty this[string propertyId] { get { return null; } }

+        public object GetPropertyValue(string propertyId) { return null; }

+        public string Name { get { return null; } }

+        public string CreatedBy { get { return null; } }

+        public DateTime? CreationDate { get { return null; } }

+        public string LastModifiedBy { get { return null; } }

+        public DateTime? LastModificationDate { get { return null; } }

+        public BaseTypeId BaseTypeId { get { return BaseTypeId.CmisDocument; } }

+        public IObjectType BaseType { get { return null; } }

+        public IObjectType ObjectType { get { return null; } }

+        public string ChangeToken { get { return null; } }

+        public IAllowableActions AllowableActions { get { return null; } }

+        public IList<IRelationship> Relationships { get { return null; } }

+        public IAcl Acl { get { return null; } }

+        public void Delete(bool allVersions) { }

+        public ICmisObject UpdateProperties(IDictionary<string, object> properties) { return null; }

+        public IObjectId UpdateProperties(IDictionary<string, object> properties, bool refresh) { return null; }

+        public IList<IRendition> Renditions { get { return null; } }

+        public void ApplyPolicy(params IObjectId[] policyId) { }

+        public void RemovePolicy(params IObjectId[] policyId) { }

+        public IList<IPolicy> Policies { get { return null; } }

+        public IAcl ApplyAcl(IList<IAce> AddAces, IList<IAce> removeAces, AclPropagation? aclPropagation) { return null; }

+        public IAcl AddAcl(IList<IAce> AddAces, AclPropagation? aclPropagation) { return null; }

+        public IAcl RemoveAcl(IList<IAce> RemoveAces, AclPropagation? aclPropagation) { return null; }

+        public IList<ICmisExtensionElement> GetExtensions(ExtensionLevel level) { return null; }

+        public DateTime RefreshTimestamp { get { return DateTime.Now; } }

+        public void Refresh() { }

+        public void RefreshIfOld(long durationInMillis) { }

+    }

+}

diff --git a/DotCMISUnitTest/DotCMISUnitTest.csproj b/DotCMISUnitTest/DotCMISUnitTest.csproj
index 5e12df9..e7d976c 100644
--- a/DotCMISUnitTest/DotCMISUnitTest.csproj
+++ b/DotCMISUnitTest/DotCMISUnitTest.csproj
@@ -43,6 +43,7 @@
     <Reference Include="System.Xml" />

   </ItemGroup>

   <ItemGroup>

+    <Compile Include="CacheTest.cs" />

     <Compile Include="CRUDTest.cs" />

     <Compile Include="EnumeratorTest.cs" />

     <Compile Include="Properties\AssemblyInfo.cs" />

diff --git a/DotCMISUnitTest/SmokeTest.cs b/DotCMISUnitTest/SmokeTest.cs
index 49822f4..ebc4624 100644
--- a/DotCMISUnitTest/SmokeTest.cs
+++ b/DotCMISUnitTest/SmokeTest.cs
@@ -23,6 +23,11 @@
 using DotCMIS.Enums;

 using NUnit.Framework;

 using System;

+using DotCMIS.Data;

+using DotCMIS.Data.Impl;

+using System.Text;

+using System.IO;

+using DotCMIS.Exceptions;

 

 namespace DotCMISUnitTest

 {

@@ -30,7 +35,7 @@
     class SmokeTest : TestFramework

     {

         [Test]

-        public void TestSession()

+        public void SmokeTestSession()

         {

             Assert.NotNull(Session);

             Assert.NotNull(Session.Binding);

@@ -168,5 +173,80 @@
                 Console.WriteLine(hit.GetPropertyValueById(PropertyIds.Name) + " (" + hit.GetPropertyValueById(PropertyIds.ObjectId) + ")");

             }

         }

+

+        [Test]

+        public void SmokeTestCreateDocument()

+        {

+            IFolder rootFolder = Session.GetRootFolder();

+

+            IDictionary<string, object> properties = new Dictionary<string, object>();

+            properties[PropertyIds.Name] = "test-smoke.txt";

+            properties[PropertyIds.ObjectTypeId] = "cmis:document";

+

+            byte[] content = UTF8Encoding.UTF8.GetBytes("Hello World!");

+

+            ContentStream contentStream = new ContentStream();

+            contentStream.FileName = properties[PropertyIds.Name] as string;

+            contentStream.MimeType = "text/plain";

+            contentStream.Length = content.Length;

+            contentStream.Stream = new MemoryStream(content);

+

+            IDocument doc = rootFolder.CreateDocument(properties, contentStream, null);

+

+            // check doc

+            Assert.NotNull(doc);

+            Assert.NotNull(doc.Id);

+

+            // check versions

+            IList<IDocument> versions = doc.GetAllVersions();

+            Assert.NotNull(versions);

+            Assert.AreEqual(1, versions.Count);

+            Assert.AreEqual(doc.Id, versions[0].Id);

+

+            // check content

+            IContentStream retrievedContentStream = doc.GetContentStream();

+            Assert.NotNull(retrievedContentStream);

+            Assert.NotNull(retrievedContentStream.Stream);

+

+            doc.Delete(true);

+

+            try

+            {

+                doc.Refresh();

+                Assert.Fail("Document shouldn't exist anymore!");

+            }

+            catch (CmisObjectNotFoundException) { }

+        }

+

+        [Test]

+        public void SmokeTestCreateFolder()

+        {

+            IFolder rootFolder = Session.GetRootFolder();

+

+            IDictionary<string, object> properties = new Dictionary<string, object>();

+            properties[PropertyIds.Name] = "test-smoke";

+            properties[PropertyIds.ObjectTypeId] = "cmis:folder";

+

+            IFolder folder = rootFolder.CreateFolder(properties);

+            

+            // check folder

+            Assert.NotNull(folder);

+            Assert.NotNull(folder.Id);

+

+            // check children

+            foreach (ICmisObject cmisObject in folder.GetChildren())

+            {

+                Assert.Fail("Folder shouldn't have children!");

+            }

+

+            folder.Delete(true);

+

+            try

+            {

+                folder.Refresh();

+                Assert.Fail("Folder shouldn't exist anymore!");

+            }

+            catch (CmisObjectNotFoundException) { }

+        }

     }

 }