/// <summary> /// This method implements GET processing for inventory. /// Any remaining parameters are used to locate the /// corresponding subtree based upon node name. /// </summary> /// <param name=rdata>HTTP service request work area</param> // private void DoGet(InventoryRequestData rdata) // { // rdata.initXmlWriter(); // // rdata.writer.WriteStartElement(String.Empty,"Inventory",String.Empty); // // // If there are additional parameters, then these represent // // a path relative to the root of the inventory. This path // // must be traversed before we format the sub-tree thus // // identified. // // traverse(rdata, rdata.root, PARM_PATH); // // // Close all open elements // // rdata.writer.WriteFullEndElement(); // // // Indicate a successful request // // rdata.Complete(); // // // Send the response to the user. The body will be implicitly // // constructed from the result of the XML writer. // // rdata.Respond(String.Format("Inventory {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"); // } // } /// <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)); // } #endregion method-specific processing /// <summary> /// This method is called to obtain the OpenSim inventory object identified /// by the supplied URI. This may be either an Item or a Folder, so a suitably /// ambiguous return type is employed (Object). This method recurses as /// necessary to process the designated hierarchy. /// /// If we reach the end of the URI then we return the contextual folder to /// our caller. /// /// If we are not yet at the end of the URI we attempt to find a child folder /// and if we succeed we recurse. /// /// If this is the last node, then we look to see if this is an item. If it is, /// we return that item. /// /// If we reach the end of an inventory path and the URI si not yet exhausted, /// then if 'fill' is specified, we create the intermediate nodes. /// /// Otherwise we fail the request on the ground of an invalid URI. /// /// An ambiguous request causes the request to fail. /// /// </summary> /// <param name=rdata>HTTP service request work area</param> /// <param name=folder>The folder to be searched (parent)</param> /// <param name=pi>URI parameter index</param> /// <param name=fill>Should missing path members be created?</param> private Object getInventoryNode(InventoryRequestData rdata, InventoryFolderBase folder, int pi, bool fill) { InventoryFolderBase foundf = null; int fk = 0; Rest.Log.DebugFormat("{0} Searching folder {1} {2} [{3}]", MsgId, folder.ID, folder.Name, pi); // We have just run off the end of the parameter sequence if (pi >= rdata.Parameters.Length) { return folder; } // There are more names in the parameter sequence, // look for the folder named by param[pi] as a // child of the folder supplied as an argument. // Note that a UUID may have been supplied as the // identifier (it is the ONLY guaranteed unambiguous // option. if (rdata.folders != null) { foreach (InventoryFolderBase f in rdata.folders) { // Look for the present node in the directory list if (f.ParentID == folder.ID && (f.Name == rdata.Parameters[pi] || f.ID.ToString() == rdata.Parameters[pi])) { foundf = f; fk++; } } } // If more than one node matched, then the path, as specified // is ambiguous. if (fk > 1) { Rest.Log.DebugFormat("{0} {1}: Request for {2} is ambiguous", MsgId, rdata.method, rdata.path); rdata.Fail(Rest.HttpStatusCodeConflict, "request is ambiguous"); } // If we find a match, then the method // increment the parameter index, and calls itself // passing the found folder as the new context. if (foundf != null) { return getInventoryNode(rdata, foundf, pi+1, fill); } // No folders that match. Perhaps this parameter identifies an item? If // it does, then it MUST also be the last name in the sequence. if (pi == rdata.Parameters.Length-1) { if (rdata.items != null) { int k = 0; InventoryItemBase li = null; foreach (InventoryItemBase i in rdata.items) { if (i.Folder == folder.ID && (i.Name == rdata.Parameters[pi] || i.ID.ToString() == rdata.Parameters[pi])) { li = i; k++; } } if (k == 1) { return li; } else if (k > 1) { Rest.Log.DebugFormat("{0} {1}: Request for {2} is ambiguous", MsgId, rdata.method, rdata.path); rdata.Fail(Rest.HttpStatusCodeConflict, "request is ambiguous"); } } } // If fill is enabled, then we must create the missing intermediate nodes. // And of course, even this is not straightforward. All intermediate nodes // are obviously folders, but the last node may be a folder or an item. if (fill) { } // No fill, so abandon the request Rest.Log.DebugFormat("{0} {1}: Resource {2} not found", MsgId, rdata.method, rdata.path); rdata.Fail(Rest.HttpStatusCodeNotFound, String.Format("resource {0}:{1} not found", rdata.method, rdata.path)); return null; /* Never reached */ }
/// <summary> /// This method creates a "trashcan" folder to support folder and item /// deletions by this interface. The xisting trash folder is found and /// this folder is created within it. It is called "tmp" to indicate to /// the client that it is OK to delete this folder. The REST interface /// will recreate the folder on an as-required basis. /// If the trash can cannot be created, then by implication the request /// that required it cannot be completed, and it fails accordingly. /// </summary> /// <param name=rdata>HTTP service request work area</param> private InventoryFolderBase GetTrashCan(InventoryRequestData rdata) { InventoryFolderBase TrashCan = null; foreach (InventoryFolderBase f in rdata.folders) { if (f.Name == "Trash") { foreach (InventoryFolderBase t in rdata.folders) { if (t.Name == "tmp") { TrashCan = t; } } if (TrashCan == null) { TrashCan = new InventoryFolderBase(); TrashCan.Name = "tmp"; TrashCan.ID = UUID.Random(); TrashCan.Version = 1; TrashCan.Type = (short) AssetType.TrashFolder; TrashCan.ParentID = f.ID; TrashCan.Owner = f.Owner; Rest.InventoryServices.AddFolder(TrashCan); } } } if (TrashCan == null) { Rest.Log.DebugFormat("{0} No Trash Can available", MsgId); rdata.Fail(Rest.HttpStatusCodeServerError, "unable to create trash can"); } return TrashCan; }
/// <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> /// This is the recursive method. I've separated them in this way so that /// we do not have to waste cycles on any first-case-only processing. /// </summary> private void traverseInventory(InventoryRequestData rdata, InventoryFolderBase folder, int pi) { int fk = 0; InventoryFolderBase ffound = null; InventoryItemBase ifound = null; Rest.Log.DebugFormat("{0} Traverse Folder : {1} {2} [{3}]", MsgId, folder.ID, folder.Name, pi); foreach (InventoryFolderBase f in rdata.folders) { if (f.ParentID == folder.ID && (f.Name == rdata.Parameters[pi] || f.ID.ToString() == rdata.Parameters[pi])) { fk++; ffound = f; } } // If this is the last element in the parameter sequence, then // it is reasonable to check for an item. All intermediate nodes // MUST be folders. if (pi == rdata.Parameters.Length-1) { // Only if there are any items, and there pretty much always are. if (rdata.items != null) { foreach (InventoryItemBase i in rdata.items) { if (i.Folder == folder.ID && (i.Name == rdata.Parameters[pi] || i.ID.ToString() == rdata.Parameters[pi])) { fk++; ifound = i; } } } } if (fk == 1) { if (ffound != null) { if (pi < rdata.Parameters.Length-1) { traverseInventory(rdata, ffound, pi+1); } else { formatInventory(rdata, ffound, String.Empty); } return; } else { // Fetching an Item has a special significance. In this // case we also want to fetch the associated asset. // To make it interesting, we'll do this via redirection. string asseturl = String.Format("http://{0}:{1}/{2}{3}{4}", rdata.hostname, rdata.port, "admin/assets",Rest.UrlPathSeparator,ifound.AssetID.ToString()); rdata.Redirect(asseturl,Rest.PERMANENT); Rest.Log.DebugFormat("{0} Never Reached", MsgId); } } else if (fk > 1) { rdata.Fail(Rest.HttpStatusCodeConflict, String.Format("ambiguous element ({0}) in path specified: <{1}>", pi, rdata.path)); } Rest.Log.DebugFormat("{0} Inventory does not contain item/folder: <{1}>", MsgId, rdata.path); rdata.Fail(Rest.HttpStatusCodeNotFound,String.Format("no such item/folder : {0}", rdata.Parameters[pi])); }
/// <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"); } }