/// <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"); } }