/* * Copyright (c) 2007-2008, openmetaverse.org * All rights reserved. * * - Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * - Redistributions of source code must retain the above copyright notice, this * list of conditions and the following disclaimer. * - Neither the name of the openmetaverse.org nor the names * of its contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ using System; using System.Collections.Generic; using System.Collections; using System.Text; using System.Threading; namespace OpenMetaverse { /// /// Inventory is responsible for managing inventory items and folders. /// It updates the InventoryFolders and InventoryItems with new FolderData /// and ItemData received from the InventoryManager. Updates to inventory /// folders/items that are not explicitly managed (via the Manage method) /// are ignored. /// /// When the FolderData and/or ItemData indicates an inventory change that is not /// reflected locally, then the Inventory will update the local inventory state /// accordingly. Under normal circumstances (when inventory is modified /// by the local client via the InventoryBase, InventoryFolder and InventoryItem /// interfaces) it doesn't have to do this. /// public class Inventory : IEnumerable { /// /// Delegate for . /// /// Inventory that was updated. public delegate void InventoryUpdate(InventoryBase inventory); /// /// Triggered when a managed inventory item or folder is updated. /// public event InventoryUpdate OnInventoryUpdate; /// /// Retrieves a managed InventoryBase from the Inventory. /// Returns null if the UUID isn't managed by this Inventory. /// /// The UUID of the InventoryBase to retrieve. /// A managed InventoryBase. public InventoryBase this[UUID uuid] { get { InventoryBase item; if (Items.TryGetValue(uuid, out item)) return item; return null; } } protected Dictionary Items; private InventoryManager _Manager; private UUID _Owner; /// /// The owner of this inventory. Inventorys can only manage items /// owned by the same agent. /// public UUID Owner { get { return _Owner; } private set { _Owner = value; } } private UUID _RootUUID; public UUID RootUUID { get { return _RootUUID; } private set { _RootUUID = value; } } public InventoryFolder RootFolder { get { return this[RootUUID] as InventoryFolder; } } /// /// Creates a new Inventory. Remote updates are sent via the manager /// passed to this constructor. All folders contained within the InventorySkeleton /// are automatically managed. The inventory takes on the owner of the skeleton. /// /// Manager for remote updates. /// Skeleton of folders, inventory owner. public Inventory(InventoryManager manager, InventorySkeleton skeleton) : this (manager, skeleton.Owner, skeleton.RootUUID) { Items = new Dictionary(skeleton.Folders.Length); foreach (FolderData folder in skeleton.Folders) { Manage(folder); } } /// /// Creates a new inventory. Remote updates are sent via the manager /// passed to this constructor. This creates an empty inventory, with no managed items. /// /// Manager for remote updates. /// Owner of this inventory. /// public Inventory(InventoryManager manager, UUID owner, UUID root) { _Manager = manager; Owner = owner; _RootUUID = root; if (Items == null) Items = new Dictionary(); RegisterInventoryCallbacks(); } /// /// Registers InventoryManager callbacks for inventory updates. /// protected virtual void RegisterInventoryCallbacks() { _Manager.OnFolderUpdate += new InventoryManager.FolderUpdate(manager_OnFolderUpdate); _Manager.OnItemUpdate += new InventoryManager.ItemUpdate(manager_OnItemUpdate); _Manager.OnAssetUpdate += new InventoryManager.AssetUpdate(manager_OnAssetUpdate); _Manager.OnItemCreated += new InventoryManager.ItemCreatedCallback(_Manager_OnItemCreated); } void _Manager_OnItemCreated(bool success, ItemData itemData) { if (Items.ContainsKey(itemData.ParentUUID)) Manage(itemData); } /// /// Updates the AssetUUID of a managed InventoryItem's ItemData. /// If the InventoryItem is not managed, the update is ignored. /// /// UUID of the item to update. /// The item's new asset UUID. protected void manager_OnAssetUpdate(UUID itemID, UUID newAssetID) { InventoryBase b; if (Items.TryGetValue(itemID, out b)) { if (b is InventoryItem) { (b as InventoryItem).Data.AssetUUID = newAssetID; } } } /// /// Updates the ItemData of a managed InventoryItem. This may /// change local inventory state if the local inventory is not /// consistant with the new data. (Parent change, rename, etc) /// If the item is not managed, the update is ignored. /// /// The updated ItemData. protected void manager_OnItemUpdate(ItemData itemData) { InventoryBase item; if (Items.TryGetValue(itemData.UUID, out item)) { if (item is InventoryItem) { Update(item as InventoryItem, itemData); } } else { // Check if it's a child of a managed folder. if (Items.ContainsKey(itemData.ParentUUID)) Manage(itemData); } } /// /// Updates the FolderData of a managed InventoryFolder. This /// may change local inventory state if the local inventory is not /// consistant with the new data (Parent change, rename, etc) /// If the folder is not managed, the update is ignored. /// /// The updated FolderData. protected void manager_OnFolderUpdate(FolderData folderData) { InventoryBase folder; if (Items.TryGetValue(folderData.UUID, out folder)) { if (folder is InventoryFolder) { Update(folder as InventoryFolder, folderData); } } else { // Check if it's a child of a managed folder. if (Items.ContainsKey(folderData.ParentUUID)) Manage(folderData); } } /// /// Wraps the ItemData in a new InventoryItem. /// You may override this method to use your own subclass of /// InventoryItem. /// /// The ItemData to wrap. /// A new InventoryItem wrapper for the ItemData. protected virtual InventoryItem WrapItemData(ItemData data) { return new InventoryItem(_Manager, this, data); } /// /// Wraps the FolderData in a new InventoryFolder. /// You may override this method to use your own subclass of /// InventoryFolder. /// /// The FolderData to wrap. /// A new InventoryFolder wrapper for the FolderData. protected virtual InventoryFolder WrapFolderData(FolderData data) { return new InventoryFolder(_Manager, this, data); } /// /// Attempts to fetch and manage an inventory item from its item UUID. /// This method will block until the item's ItemData is fetched from /// the remote inventory. If the item is already managed by the inventory /// returns the local managed InventoryItem wrapper. /// /// The ItemID of the inventory item to fetch. /// Managed InventoryItem, null if fetch fails. public InventoryItem Manage(UUID itemID) { InventoryBase ib; if (Items.TryGetValue(itemID, out ib)) { // This method shouldn't really be used for retrieving items. return ib as InventoryItem; } else { ItemData item; if (_Manager.FetchItem(itemID, Owner, TimeSpan.FromSeconds(30), out item)) { return Manage(item); } else { return null; } } } /// /// Explicitly manage the inventory item given its ItemData. /// If the item isn't managed, a new wrapper for it is created /// and it is added to the local inventory. /// If the item is already managed, this method returns its wrapper, /// updating it with the ItemData passed to this method. /// /// The ItemData of the item to manage. /// The managed InventoryItem wrapper. public InventoryItem Manage(ItemData item) { if (item.OwnerID == Owner) { InventoryBase b; if (Items.TryGetValue(item.UUID, out b)) { Update(b as InventoryItem, item); return b as InventoryItem; } else { InventoryItem wrapper = WrapItemData(item); Items.Add(item.UUID, wrapper); return wrapper; } } else { return null; } } /// /// Explicitly manage the inventory folder given its FolderData. /// If the folder isn't managed, a new wrapper for it is created, /// it is added to the local inventory, and any known children of /// the folder are added to the folder's Contents. /// If the folder is already managed, this method returns the folder's /// wrapper, updating it with the FolderData passed to this method. /// /// The FolderData of the folder to manage. /// The managed InventoryFolder wrapper. public InventoryFolder Manage(FolderData folder) { if (folder.OwnerID == Owner) { InventoryBase b; if (Items.TryGetValue(folder.UUID, out b)) { Update(b as InventoryFolder, folder); return b as InventoryFolder; } else { InventoryFolder wrapper = WrapFolderData(folder); Items.Add(folder.UUID, wrapper); lock (Items) { // Folder is now managed, update its contents with known children. foreach (InventoryBase item in Items.Values) { if (item.ParentUUID == folder.UUID) { wrapper.AddChild(item); } } } return wrapper; } } else { return null; } } /// /// Unmanages an inventory item or folder. The item or folder will no /// longer be automatically updated. /// /// public void Unmanage(InventoryBase item) { Items.Remove(item.UUID); } protected void Update(InventoryItem item, ItemData update) { if (item.Data != update) { // Check for parent change: if (item.Data.ParentUUID != update.ParentUUID) { item.LocalMove(update.ParentUUID, this[update.ParentUUID] as InventoryFolder); } item.Data = update; FireInventoryUpdate(item); } } protected void Update(InventoryFolder folder, FolderData update) { if (folder.Data != update) { // Check for parent change: if (folder.Data.ParentUUID != update.ParentUUID) { folder.LocalMove(update.ParentUUID, this[update.ParentUUID] as InventoryFolder); } folder.Data = update; FireInventoryUpdate(folder); } } protected void FireInventoryUpdate(InventoryBase updatedInventory) { if (OnInventoryUpdate != null) { try { OnInventoryUpdate(updatedInventory); } catch (Exception e) { Logger.Log(e.Message, Helpers.LogLevel.Error, e); } } } public List InventoryFromPath(string[] path) { return InventoryFromPath(path, RootFolder); } public List InventoryFromPath(string[] path, InventoryFolder baseFolder) { if (path == null || path.Length == 0) { List one = new List(1); one.Add(baseFolder); return one; } Dictionary goal = new Dictionary(); List results = new List(); Stack agenda = new Stack(); goal.Add(baseFolder, 0); agenda.Push(baseFolder); while (agenda.Count > 0) { InventoryFolder currentFolder = agenda.Pop() as InventoryFolder; int currentLevel = goal[currentFolder]; foreach (InventoryBase child in currentFolder) { if (child.Name == path[currentLevel]) { if (currentLevel == path.Length - 1) { results.Add(child); } else { if (child is InventoryFolder) { agenda.Push(child); goal.Add(child, currentLevel + 1); } } } } } return results; } #region IEnumerable Members public IEnumerator GetEnumerator() { foreach (KeyValuePair kvp in Items) { yield return kvp.Value; } } #endregion #region IEnumerable Members IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } #endregion } public abstract class InventoryBase { /// /// InventoryBase's item UUID as obtained from ItemData or FolderData. /// public abstract UUID UUID { get; } /// /// InventoryBase parent's item UUID, as obtained from ItemData or FolderData. /// Setting this will not modify the remote inventory, it will only modify the local /// ItemData or FolderData struct. /// public abstract UUID ParentUUID { get; set; } /// /// Inventory base's name, as obtained from ItemData or FolderData. /// Setting this will not modify the remote inventory, it will only modify /// the local ItemData or FolderData struct. /// public abstract string Name { get; set; } /// /// Inventory base's owner ID, as obtained from ItemData or FolderData. /// public abstract UUID OwnerUUID { get; } /// /// Gets the parent InventoryFolder referenced by ParentUUID. Returns null /// if parent is not managed by the Inventory. /// public InventoryFolder Parent { get { return Inventory[ParentUUID] as InventoryFolder; } } private Inventory _Inventory; protected Inventory Inventory { get { return _Inventory; } private set { _Inventory = value; } } private InventoryManager _Manager; protected InventoryManager Manager { get { return _Manager; } private set { _Manager = value; } } public InventoryBase(InventoryManager manager, Inventory inventory) { Inventory = inventory; Manager = manager; } /// /// Moves this InventoryBase to a new folder. Updates local and /// remote inventory. /// /// The folder to move this InventoryBase to. public virtual void Move(InventoryFolder destination) { if (destination.UUID != ParentUUID) { if (Parent != null) { Parent.RemoveChild(this); } ParentUUID = destination.UUID; destination.AddChild(this); } // Subclass will call the InventoryManager method. } /// /// Removes this InventoryBase from the local Inventory and its parent's /// Contents. /// protected internal virtual void LocalRemove() { if (Parent != null) Parent.RemoveChild(this); Inventory.Unmanage(this); } /// /// Changes the parent of this InventoryBase. Removing it from old parent's contents /// and adding it to new parent's contents. /// /// The UUID of the new parent. /// The InventoryFolder of the new parent. (may be null, if unmanaged) protected internal virtual void LocalMove(UUID newParentUUID, InventoryFolder newParent) { if (ParentUUID != newParentUUID) { if (Parent != null) { Parent.RemoveChild(this); } if (newParent != null) { newParent.AddChild(this); } ParentUUID = newParentUUID; } } /// /// Removes this InventoryBase from the remote and local inventory. /// public virtual void Remove() { LocalRemove(); // Subclass will call the InventoryManager method. } /// /// Renames this InventoryBase, without moving it. /// /// The InventoryBase's new name. public virtual void Rename(string newName) { Name = newName; // Subclass will call InventoryManager method. } public abstract void Give(UUID recipiant, bool particleEffect); public void Give(UUID recipiant) { Give(recipiant, false); } public override bool Equals(object obj) { return (obj is InventoryBase) ? (obj as InventoryBase).UUID == UUID : false; } public override int GetHashCode() { return UUID.GetHashCode(); } } public class InventoryItem : InventoryBase { public ItemData Data; private Asset _Asset; public virtual Asset Asset { get { return _Asset; } protected set { _Asset = value; } } public InventoryItem(InventoryManager manager, Inventory inv, UUID uuid, InventoryType type) : this(manager, inv, new ItemData(uuid, type)) { } public InventoryItem(InventoryManager manager, Inventory inv, ItemData data) : base(manager, inv) { Data = data; if (Parent != null) Parent.AddChild(this); } #region InventoryBase Members public override UUID UUID { get { return Data.UUID; } } public override UUID ParentUUID { get { return Data.ParentUUID; } set { Data.ParentUUID = value; } } public override string Name { get { return Data.Name; } set { Data.Name = value; } } public override UUID OwnerUUID { get { return Data.OwnerID; } } /// /// Requests that a copy of this item be made and placed in the /// folder. This method is not synchronous, and returns immediately. The callback is called /// with the new item's inventory data. /// /// The InventoryFolder to copy this item to. /// The callback to call when the copy is complete. public void Copy(InventoryFolder destination, InventoryManager.ItemCopiedCallback callback) { Manager.RequestCopyItem(UUID, destination.UUID, Data.Name, callback); } /// /// Synchronously requests a copy of this item be made and placed in the /// folder. The copy is automatically managed. /// /// Location for the new copy. /// Amount of time to wait for a server response. /// A managed InventoryItem if copy successful, null if not. public InventoryItem Copy(InventoryFolder destination, TimeSpan timeout) { ItemData copy; if (Manager.CopyItem(UUID, destination.UUID, Name, timeout, out copy)) return Inventory.Manage(copy) as InventoryItem; else return null; } public override void Move(InventoryFolder destination) { base.Move(destination); Manager.MoveItem(UUID, destination.UUID); } public override void Remove() { base.Remove(); Manager.RemoveItem(UUID); } public override void Rename(string newName) { base.Rename(newName); Manager.RenameItem(UUID, ParentUUID, newName); } public override void Give(UUID recipiant, bool particleEffect) { Manager.GiveItem(UUID, Data.Name, Data.AssetType, recipiant, particleEffect); } #endregion /// /// Updates the remote inventory item with the local inventory /// ItemData. /// public void Update() { Manager.RequestUpdateItem(Data); } } /// /// /// public class InventoryFolder : InventoryBase, IEnumerable { /// /// Delegate for InventoryFolder.OnContentsRetrieved /// /// The folder whose contents were retrieved. public delegate void ContentsRetrieved(InventoryFolder folder); /// /// Triggered when the InventoryFolder.Contents dictionary /// is updated from the remote inventory. /// public event ContentsRetrieved OnContentsRetrieved; public FolderData Data; /// /// The local contents of this InventoryFolder. This returns a copy of the /// internal collection, so incurs a memory and CPU penalty. Consider enumerating /// directly over the InventoryFolder. /// protected Dictionary _Contents; public List Contents { get { lock (_Contents) { return new List(_Contents.Values); } } } /// /// true if all the folder's contents have been downloaded and managed. /// false otherwise. /// public bool IsStale { get { return Data.DescendentCount == 0 || _Contents.Count != Data.DescendentCount; } } public InventoryFolder(InventoryManager manager, Inventory inv, UUID uuid) : this(manager, inv, new FolderData(uuid)) { } public InventoryFolder(InventoryManager manager, Inventory inv, FolderData data) : base(manager, inv) { Data = data; if (data.DescendentCount > 0) _Contents = new Dictionary(data.DescendentCount); else _Contents = new Dictionary(); if (Parent != null) Parent.AddChild(this); } protected internal void AddChild(InventoryBase child) { lock (_Contents) { _Contents[child.UUID] = child; } } protected internal void RemoveChild(InventoryBase child) { lock (_Contents) { _Contents.Remove(child.UUID); } } #region InventoryBase Members public override UUID UUID { get { return Data.UUID; } } public override UUID ParentUUID { get { return Data.ParentUUID; } set { Data.ParentUUID = value; } } public override string Name { get { return Data.Name; } set { Data.Name = value; } } public override UUID OwnerUUID { get { return Data.OwnerID; } } public override void Move(InventoryFolder destination) { base.Move(destination); Manager.MoveFolder(UUID, destination.UUID); } public override void Remove() { base.Remove(); Manager.RemoveFolder(UUID); } protected internal override void LocalRemove() { // Recursively remove all children. // First we need to copy the children into our own list, because // calling LocalRemove on the child causes the child to modify // our Contents dictionary (through our RemoveChild method) // and C# doesn't like iterating through a modified collection. List children = new List(_Contents.Count); lock (_Contents) { foreach (KeyValuePair child in _Contents) { children.Add(child.Value); } } // Now actually do the removal: foreach (InventoryBase child in children) { child.LocalRemove(); } base.LocalRemove(); } public override void Rename(string newName) { base.Rename(newName); Manager.RenameFolder(UUID, ParentUUID, newName); } public override void Give(UUID recipiant, bool particleEffect) { // Attempt to use local copy of contents, so we dont block waiting to // download contents. if (!IsStale) { List itemContents = new List(_Contents.Count); foreach (InventoryBase ib in this) { if (ib is InventoryItem) { itemContents.Add((ib as InventoryItem).Data); } } //FIXME: Will we ever want to pass anything other then AssetType.Folder? Manager.GiveFolder(UUID, Name, AssetType.Folder, recipiant, particleEffect, itemContents); } else { Manager.GiveFolder(UUID, Name, AssetType.Folder, recipiant, particleEffect); } } #endregion /// /// Empties the folder, remotely and locally removing all items /// in the folder RECURSIVELY. Be careful with this! /// public void Empty() { Manager.RemoveDescendants(UUID); List children = null; lock (_Contents) { // We need to copy the collection before removing, see comment // in LocalRemove method. children = new List(_Contents.Values); } foreach (InventoryBase child in children) { child.LocalRemove(); } } /// /// Retrieves the contents of this folder from the remote inventory. /// This method is synchronous, and blocks until the contents are retrieved or /// the timeout has expired. The contents are written to the /// InventoryFolder.Contents dictionary. If the method times out, /// InventoryFolder.Contents is left unchanged. /// The contents retrieved (if successful) are automatically managed. /// /// TimeSpan to wait for a reply. /// true if the contents were retrieved, false if timed out. public bool DownloadContents(TimeSpan timeout) { List items; List folders; bool success = Manager.FolderContents(UUID, Data.OwnerID, true, true, InventorySortOrder.ByName, timeout, out items, out folders); if (success) { ContentsFromData(items, folders); Data.DescendentCount = Contents.Count; } return success; } /// /// Asynchronously requests the folder's contents from the remote inventory. /// The InventoryFolder.OnContentsRetrieved event /// is raised when he new contents are written to the /// InventoryFolder.Contents Dictionary. /// The contents retrieved are automatically managed. /// public void RequestContents() { InventoryManager.FolderContentsCallback callback = delegate(UUID folder, List items, List folders) { if (folder != UUID) return; ContentsFromData(items, folders); Data.DescendentCount = Contents.Count; if (OnContentsRetrieved != null) { try { OnContentsRetrieved(this); } catch (Exception e) { Logger.Log(e.Message, Helpers.LogLevel.Error, e); } } }; Manager.RequestFolderContents(UUID, Data.OwnerID, true, true, InventorySortOrder.ByName, callback); } private void ContentsFromData(List items, List folders) { Dictionary contents = new Dictionary(items.Count + folders.Count); foreach (ItemData item in items) { contents.Add(item.UUID, Inventory.Manage(item)); } foreach (FolderData folder in folders) { contents.Add(folder.UUID, Inventory.Manage(folder)); } lock (_Contents) { _Contents = contents; } } #region IEnumerable Members /// /// Enumerates over the local contents of this folder. /// Consider calling GetContents or RequestContents before enumerating /// to synchronize the local folder contents with the remote folder contents. /// /// An enumerator for this InventoryFolder. public IEnumerator GetEnumerator() { foreach (KeyValuePair child in _Contents) { yield return child.Value; } } #endregion #region IEnumerable Members /// /// /// /// IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } #endregion } }