/// <summary> /// PUT updates the URI-identified element in the inventory. This /// is actually far more flexible than it might at first sound. For /// PUT the URI serves two purposes: /// [1] It identifies the user whose inventory is to be /// processed. /// [2] It optionally specifies a subtree of the inventory /// that is to be used to resolve any relative subtree /// specifications in the entity. If nothing is specified /// then the whole of the private inventory is implied. /// Please note that the subtree specified by the URI is only relevant /// to an entity containing a URI relative specification, i.e. one or /// more elements do not specify parent folder information. These /// elements will be implicitly referenced within the context identified /// by the URI. /// If an element in the entity specifies an explicit parent folder, then /// that parent is effective, regardless of any value specified in the /// URI. If the parent does not exist, then the element, and any dependent /// elements, are ignored. This case is actually detected and handled /// during the reconstitution process. /// </summary> /// <param name=rdata>HTTP service request work area</param> private void DoUpdate(InventoryRequestData rdata) { int count = 0; bool created = false; bool modified = false; // Resolve the inventory node that is to be modified. // rdata already contains information about the current // content of the user's inventory. Object InventoryNode = getInventoryNode(rdata, rdata.root, PARM_PATH, Rest.Fill); // As long as we have a node, then we have something // meaningful to do, unlike POST. So we reconstitute the // subtree before doing anything else. Note that we // etiher got a valid node or we threw an exception. XmlInventoryCollection entity = ReconstituteEntity(rdata); // Incorporate any inlined assets first. Any failures // will terminate the request. if (entity.Assets.Count > 0) { Rest.Log.DebugFormat("{0} Adding {1} assets to server", MsgId, entity.Assets.Count); foreach (AssetBase asset in entity.Assets) { Rest.Log.DebugFormat("{0} Rest asset: {1} {2} {3}", MsgId, asset.ID, asset.Type, asset.Name); // The asset was validated during the collection process Rest.AssetServices.Store(asset); created = true; rdata.appendStatus(String.Format("<p> Created asset {0}, UUID {1} <p>", asset.Name, asset.ID)); if (Rest.DEBUG && Rest.DumpAsset) { Rest.Dump(asset.Data); } } } // The URI specifies either a folder or an item to be updated. // // The root node in the entity will replace the node identified // by the URI. This means the parent will remain the same, but // any or all attributes associated with the named element // will change. // // If the inventory collection contains an element with a zero // parent ID, then this is taken to be the replacement for the // named node. The collection MAY also specify an explicit // parent ID, in this case it MAY identify the same parent as // the current node, or it MAY specify a different parent, // indicating that the folder is being moved in addition to any // other modifications being made. if (typeof(InventoryFolderBase) == InventoryNode.GetType() || typeof(InventoryFolderImpl) == InventoryNode.GetType()) { bool rfound = false; InventoryFolderBase uri = (InventoryFolderBase) InventoryNode; InventoryFolderBase xml = null; // If the entity to be replaced resolved to be the root // directory itself (My Inventory), then make sure that // the supplied data include as appropriately typed and // named folder. Note that we can;t rule out the possibility // of a sub-directory being called "My Inventory", so that // is anticipated. if (uri == rdata.root) { foreach (InventoryFolderBase folder in entity.Folders) { if ((rfound = (folder.Name == PRIVATE_ROOT_NAME))) { if ((rfound = (folder.ParentID == UUID.Zero))) break; } } if (!rfound) { Rest.Log.DebugFormat("{0} {1}: Path <{2}> will result in loss of inventory", MsgId, rdata.method, rdata.path); rdata.Fail(Rest.HttpStatusCodeBadRequest, "invalid inventory structure"); } } // Scan the set of folders in the entity collection for an // entry that matches the context folder. It is assumed that // the only reliable indicator of this is a zero UUID (using // implicit context), or the parent's UUID matches that of the // URI designated node (explicit context). We don't allow // ambiguity in this case because this is POST and we are // supposed to be modifying a specific node. // We assign any element IDs required as an economy; we don't // want to iterate over the fodler set again if it can be // helped. foreach (InventoryFolderBase folder in entity.Folders) { if (folder.ParentID == uri.ParentID || folder.ParentID == UUID.Zero) { folder.ParentID = uri.ParentID; xml = folder; count++; } } // More than one entry is ambiguous. Other folders should be // added using the POST verb. if (count > 1) { Rest.Log.DebugFormat("{0} {1}: Request for <{2}> is ambiguous", MsgId, rdata.method, rdata.path); rdata.Fail(Rest.HttpStatusCodeConflict, "context is ambiguous"); } // Exactly one entry means we ARE replacing the node // identified by the URI. So we delete the old folder // by moving it to the trash and then purging it. // We then add all of the folders and items we // included in the entity. The subtree has been // modified. if (count == 1) { InventoryFolderBase TrashCan = GetTrashCan(rdata); // All went well, so we generate a UUID is one is // needed. if (xml.ID == UUID.Zero) { xml.ID = UUID.Random(); } uri.ParentID = TrashCan.ID; Rest.InventoryServices.MoveFolder(uri); Rest.InventoryServices.PurgeFolder(TrashCan); modified = true; } // Now, regardelss of what they represent, we // integrate all of the elements in the entity. foreach (InventoryFolderBase f in entity.Folders) { rdata.appendStatus(String.Format("<p>Moving folder {0} UUID {1} <p>", f.Name, f.ID)); Rest.InventoryServices.MoveFolder(f); } foreach (InventoryItemBase it in entity.Items) { rdata.appendStatus(String.Format("<p>Storing item {0} UUID {1} <p>", it.Name, it.ID)); Rest.InventoryServices.AddItem(it); } } /// <summary> /// URI specifies an item to be updated /// </summary> /// <remarks> /// The entity must contain a single item node to be /// updated. ID and Folder ID must be correct. /// </remarks> else { InventoryItemBase uri = (InventoryItemBase) InventoryNode; InventoryItemBase xml = null; if (entity.Folders.Count != 0) { Rest.Log.DebugFormat("{0} {1}: Request should not contain any folders <{2}>", MsgId, rdata.method, rdata.path); rdata.Fail(Rest.HttpStatusCodeBadRequest, "folder is not allowed"); } if (entity.Items.Count > 1) { Rest.Log.DebugFormat("{0} {1}: Entity contains too many items <{2}>", MsgId, rdata.method, rdata.path); rdata.Fail(Rest.HttpStatusCodeBadRequest, "too may items"); } xml = entity.Items[0]; if (xml.ID == UUID.Zero) { xml.ID = UUID.Random(); } // If the folder reference has changed, then this item is // being moved. Otherwise we'll just delete the old, and // add in the new. // Delete the old item List<UUID> uuids = new List<UUID>(); uuids.Add(uri.ID); Rest.InventoryServices.DeleteItems(uri.Owner, uuids); // Add the new item to the inventory Rest.InventoryServices.AddItem(xml); rdata.appendStatus(String.Format("<p>Storing item {0} UUID {1} <p>", xml.Name, xml.ID)); } if (created) { rdata.Complete(Rest.HttpStatusCodeCreated); } else { if (modified) { rdata.Complete(Rest.HttpStatusCodeOK); } else { rdata.Complete(Rest.HttpStatusCodeNoContent); } } rdata.Respond(String.Format("Profile {0} : Normal completion", rdata.method)); }
/// <summary> /// Arguably the most damaging REST interface. It deletes the inventory /// item or folder identified by the URI. /// /// We only process if the URI identified node appears to exist /// We do not test for success because we either get a context, /// or an exception is thrown. /// /// Folders are deleted by moving them to another folder and then /// purging that folder. We'll do that by creating a temporary /// sub-folder in the TrashCan and purging that folder's /// contents. If we can't can it, we don't delete it... /// So, if no trashcan is available, the request does nothing. /// Items are summarily deleted. /// /// In the interests of safety, a delete request should normally /// be performed using UUID, as a name might identify several /// elements. /// </summary> /// <param name=rdata>HTTP service request work area</param> private void DoDelete(InventoryRequestData rdata) { Object InventoryNode = getInventoryNode(rdata, rdata.root, PARM_PATH, false); if (typeof(InventoryFolderBase) == InventoryNode.GetType() || typeof(InventoryFolderImpl) == InventoryNode.GetType()) { InventoryFolderBase TrashCan = GetTrashCan(rdata); InventoryFolderBase folder = (InventoryFolderBase) InventoryNode; Rest.Log.DebugFormat("{0} {1}: Folder {2} will be deleted", MsgId, rdata.method, rdata.path); folder.ParentID = TrashCan.ID; Rest.InventoryServices.MoveFolder(folder); Rest.InventoryServices.PurgeFolder(TrashCan); rdata.appendStatus(String.Format("<p>Deleted folder {0} UUID {1} <p>", folder.Name, folder.ID)); } // Deleting items is much more straight forward. else { InventoryItemBase item = (InventoryItemBase) InventoryNode; Rest.Log.DebugFormat("{0} {1}: Item {2} will be deleted", MsgId, rdata.method, rdata.path); List<UUID> uuids = new List<UUID>(); uuids.Add(item.ID); Rest.InventoryServices.DeleteItems(item.Owner, uuids); rdata.appendStatus(String.Format("<p>Deleted item {0} UUID {1} <p>", item.Name, item.ID)); } rdata.Complete(); rdata.Respond(String.Format("Profile {0} : Normal completion", rdata.method)); }
/// <summary> /// In the case of the inventory, and probably in general, /// the distinction between PUT and POST is not always /// easy to discern. The standard is badly worded in places, /// and adding a node to a hierarchy can be viewed as /// an addition, or as a modification to the inventory as /// a whole. This is exacerbated by an unjustified lack of /// consistency across different implementations. /// /// For OpenSim PUT is an update and POST is an addition. This /// is the behavior required by the HTTP specification and /// therefore as required by REST. /// /// The best way to explain the distinction is to /// consider the relationship between the URI and the /// enclosed entity. For PUT, the URI identifies the /// actual entity to be modified or replaced, i.e. the /// enclosed entity. /// /// If the operation is POST,then the URI describes the /// context into which the new entity will be added. /// /// As an example, suppose the URI contains: /// /admin/inventory/Clothing /// /// A PUT request will normally result in some modification of /// the folder or item named "Clothing". Whereas a POST /// request will normally add some new information into the /// content identified by Clothing. It follows from this /// that for POST, the element identified by the URI MUST /// be a folder. /// </summary> /// <summary> /// POST adds new information to the inventory in the /// context identified by the URI. /// </summary> /// <param name=rdata>HTTP service request work area</param> private void DoExtend(InventoryRequestData rdata) { bool created = false; bool modified = false; string newnode = String.Empty; // Resolve the context node specified in the URI. Entity // data will be ADDED beneath this node. rdata already contains // information about the current content of the user's // inventory. Object InventoryNode = getInventoryNode(rdata, rdata.root, PARM_PATH, Rest.Fill); // Processing depends upon the type of inventory node // identified in the URI. This is the CONTEXT for the // change. We either got a context or we threw an // exception. // It follows that we can only add information if the URI // has identified a folder. So only a type of folder is supported // in this case. if (typeof(InventoryFolderBase) == InventoryNode.GetType() || typeof(InventoryFolderImpl) == InventoryNode.GetType()) { // Cast the context node appropriately. InventoryFolderBase context = (InventoryFolderBase) InventoryNode; Rest.Log.DebugFormat("{0} {1}: Resource(s) will be added to folder {2}", MsgId, rdata.method, rdata.path); // Reconstitute the inventory sub-tree from the XML supplied in the entity. // The result is a stand-alone inventory subtree, not yet integrated into the // existing tree. An inventory collection consists of three components: // [1] A (possibly empty) set of folders. // [2] A (possibly empty) set of items. // [3] A (possibly empty) set of assets. // If all of these are empty, then the POST is a harmless no-operation. XmlInventoryCollection entity = ReconstituteEntity(rdata); // Inlined assets can be included in entity. These must be incorporated into // the asset database before we attempt to update the inventory. If anything // fails, return a failure to requestor. if (entity.Assets.Count > 0) { Rest.Log.DebugFormat("{0} Adding {1} assets to server", MsgId, entity.Assets.Count); foreach (AssetBase asset in entity.Assets) { Rest.Log.DebugFormat("{0} Rest asset: {1} {2} {3}", MsgId, asset.ID, asset.Type, asset.Name); Rest.AssetServices.Store(asset); created = true; rdata.appendStatus(String.Format("<p> Created asset {0}, UUID {1} <p>", asset.Name, asset.ID)); if (Rest.DEBUG && Rest.DumpAsset) { Rest.Dump(asset.Data); } } } // Modify the context using the collection of folders and items // returned in the XmlInventoryCollection. foreach (InventoryFolderBase folder in entity.Folders) { InventoryFolderBase found; // If the parentID is zero, then this folder is going // into the root folder identified by the URI. The requestor // may have already set the parent ID explicitly, in which // case we don't have to do it here. if (folder.ParentID == UUID.Zero || folder.ParentID == context.ID) { if (newnode != String.Empty) { Rest.Log.DebugFormat("{0} Too many resources", MsgId); rdata.Fail(Rest.HttpStatusCodeBadRequest, "only one root entity is allowed"); } folder.ParentID = context.ID; newnode = folder.Name; } // Search the existing inventory for an existing entry. If // we have one, we need to decide if it has really changed. // It could just be present as (unnecessary) context, and we // don't want to waste time updating the database in that // case, OR, it could be being moved from another location // in which case an update is most certainly necessary. found = null; foreach (InventoryFolderBase xf in rdata.folders) { // Compare identifying attribute if (xf.ID == folder.ID) { found = xf; break; } } if (found != null && FolderHasChanged(folder,found)) { Rest.Log.DebugFormat("{0} Updating existing folder", MsgId); Rest.InventoryServices.MoveFolder(folder); modified = true; rdata.appendStatus(String.Format("<p> Created folder {0}, UUID {1} <p>", folder.Name, folder.ID)); } else { Rest.Log.DebugFormat("{0} Adding new folder", MsgId); Rest.InventoryServices.AddFolder(folder); created = true; rdata.appendStatus(String.Format("<p> Modified folder {0}, UUID {1} <p>", folder.Name, folder.ID)); } } // Now we repeat a similar process for the items included // in the entity. foreach (InventoryItemBase item in entity.Items) { InventoryItemBase found = null; // If the parentID is zero, then this is going // directly into the root identified by the URI. if (item.Folder == UUID.Zero) { item.Folder = context.ID; } // Determine whether this is a new item or a // replacement definition. foreach (InventoryItemBase xi in rdata.items) { // Compare identifying attribute if (xi.ID == item.ID) { found = xi; break; } } if (found != null && ItemHasChanged(item, found)) { Rest.Log.DebugFormat("{0} Updating item {1} {2} {3} {4} {5}", MsgId, item.ID, item.AssetID, item.InvType, item.AssetType, item.Name); Rest.InventoryServices.UpdateItem(item); modified = true; rdata.appendStatus(String.Format("<p> Modified item {0}, UUID {1} <p>", item.Name, item.ID)); } else { Rest.Log.DebugFormat("{0} Adding item {1} {2} {3} {4} {5}", MsgId, item.ID, item.AssetID, item.InvType, item.AssetType, item.Name); Rest.InventoryServices.AddItem(item); created = true; rdata.appendStatus(String.Format("<p> Created item {0}, UUID {1} <p>", item.Name, item.ID)); } } if (created) { // Must include a location header with a URI that identifies the new resource. rdata.AddHeader(Rest.HttpHeaderLocation,String.Format("http://{0}{1}:{2}/{3}", rdata.hostname, rdata.port,rdata.path,newnode)); rdata.Complete(Rest.HttpStatusCodeCreated); } else { if (modified) { rdata.Complete(Rest.HttpStatusCodeOK); } else { rdata.Complete(Rest.HttpStatusCodeNoContent); } } rdata.Respond(String.Format("Profile {0} : Normal completion", rdata.method)); } else { Rest.Log.DebugFormat("{0} {1}: Resource {2} is not a valid context: {3}", MsgId, rdata.method, rdata.path, InventoryNode.GetType()); rdata.Fail(Rest.HttpStatusCodeBadRequest, "invalid resource context"); } }