/admin/inventory/Arthur Dent
//
// Indicating that this is an inventory request for
// an avatar named Arthur Dent. This is ALL that is
// required to designate a GET for an entire
// inventory.
//
// Do we have at least a user agent name?
if (rdata.Parameters.Length < 1)
{
Rest.Log.WarnFormat("{0} Inventory: No user agent identifier specified", MsgId);
rdata.Fail(Rest.HttpStatusCodeBadRequest, "no user identity specified");
}
// The first parameter MUST be the agent identification, either an UUID
// or a space-separated First-name Last-Name specification. We check for
// an UUID first, if anyone names their character using a valid UUID
// that identifies another existing avatar will cause this a problem...
try
{
rdata.uuid = new UUID(rdata.Parameters[PARM_USERID]);
Rest.Log.DebugFormat("{0} UUID supplied", MsgId);
rdata.userProfile = Rest.UserServices.GetUserProfile(rdata.uuid);
}
catch
{
string[] names = rdata.Parameters[PARM_USERID].Split(Rest.CA_SPACE);
if (names.Length == 2)
{
Rest.Log.DebugFormat("{0} Agent Name supplied [2]", MsgId);
rdata.userProfile = Rest.UserServices.GetUserProfile(names[0],names[1]);
}
else
{
Rest.Log.WarnFormat("{0} A Valid UUID or both first and last names must be specified", MsgId);
rdata.Fail(Rest.HttpStatusCodeBadRequest, "invalid user identity");
}
}
// If the user profile is null then either the server is broken, or the
// user is not known. We always assume the latter case.
if (rdata.userProfile != null)
{
Rest.Log.DebugFormat("{0} Profile obtained for agent {1} {2}",
MsgId, rdata.userProfile.FirstName, rdata.userProfile.SurName);
}
else
{
Rest.Log.WarnFormat("{0} No profile for {1}", MsgId, rdata.path);
rdata.Fail(Rest.HttpStatusCodeNotFound, "unrecognized user identity");
}
// If we get to here, then we have effectively validated the user's
// identity. Now we need to get the inventory. If the server does not
// have the inventory, we reject the request with an appropriate explanation.
//
// Note that inventory retrieval is an asynchronous event, we use the rdata
// class instance as the basis for our synchronization.
//
rdata.uuid = rdata.userProfile.ID;
if (Rest.InventoryServices.HasInventoryForUser(rdata.uuid))
{
rdata.root = Rest.InventoryServices.GetRootFolder(rdata.uuid);
Rest.Log.DebugFormat("{0} Inventory Root retrieved for {1} {2}",
MsgId, rdata.userProfile.FirstName, rdata.userProfile.SurName);
Rest.InventoryServices.GetUserInventory(rdata.uuid, rdata.GetUserInventory);
Rest.Log.DebugFormat("{0} Inventory catalog requested for {1} {2}",
MsgId, rdata.userProfile.FirstName, rdata.userProfile.SurName);
lock (rdata)
{
if (!rdata.HaveInventory)
{
rdata.startWD(1000);
rdata.timeout = false;
Monitor.Wait(rdata);
}
}
if (rdata.timeout)
{
Rest.Log.WarnFormat("{0} Inventory not available for {1} {2}. No response from service.",
MsgId, rdata.userProfile.FirstName, rdata.userProfile.SurName);
rdata.Fail(Rest.HttpStatusCodeServerError, "inventory server not responding");
}
if (rdata.root == null)
{
Rest.Log.WarnFormat("{0} Inventory is not available [1] for agent {1} {2}",
MsgId, rdata.userProfile.FirstName, rdata.userProfile.SurName);
rdata.Fail(Rest.HttpStatusCodeServerError, "inventory retrieval failed");
}
}
else
{
Rest.Log.WarnFormat("{0} Inventory is not locally available for agent {1} {2}",
MsgId, rdata.userProfile.FirstName, rdata.userProfile.SurName);
rdata.Fail(Rest.HttpStatusCodeNotFound, "no local inventory for user");
}
// If we get here, then we have successfully retrieved the user's information
// and inventory information is now available locally.
switch (rdata.method)
{
case Rest.HEAD : // Do the processing, set the status code, suppress entity
DoGet(rdata);
rdata.buffer = null;
break;
case Rest.GET : // Do the processing, set the status code, return entity
DoGet(rdata);
break;
case Rest.PUT : // Update named element
DoUpdate(rdata);
break;
case Rest.POST : // Add new information to identified context.
DoExtend(rdata);
break;
case Rest.DELETE : // Delete information
DoDelete(rdata);
break;
default :
Rest.Log.WarnFormat("{0} Method {1} not supported for {2}",
MsgId, rdata.method, rdata.path);
rdata.Fail(Rest.HttpStatusCodeMethodNotAllowed,
String.Format("{0} not supported", rdata.method));
break;
}
}
#endregion Interface
#region method-specific processing
///
/// This method implements GET processing for inventory.
/// Any remaining parameters are used to locate the
/// corresponding subtree based upon node name.
///
/// HTTP service request work area
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));
}
///
/// 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.
///
///
/// POST adds new information to the inventory in the
/// context identified by the URI.
///
/// HTTP service request work area
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(" Created asset {0}, UUID {1}
",
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("
Created folder {0}, UUID {1}
",
folder.Name, folder.ID));
}
else
{
Rest.Log.DebugFormat("{0} Adding new folder", MsgId);
Rest.InventoryServices.AddFolder(folder);
created = true;
rdata.appendStatus(String.Format("
Modified folder {0}, UUID {1}
",
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("
Modified item {0}, UUID {1}
", 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("
Created item {0}, UUID {1}
", 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");
}
}
///
/// 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.
///
/// HTTP service request work area
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("
Created asset {0}, UUID {1}
", 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("
Moving folder {0} UUID {1}
", f.Name, f.ID));
Rest.InventoryServices.MoveFolder(f);
}
foreach (InventoryItemBase it in entity.Items)
{
rdata.appendStatus(String.Format("
Storing item {0} UUID {1}
", it.Name, it.ID));
Rest.InventoryServices.AddItem(it);
}
}
///
/// URI specifies an item to be updated
///
///
/// The entity must contain a single item node to be
/// updated. ID and Folder ID must be correct.
///
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
Rest.InventoryServices.DeleteItem(uri);
// Add the new item to the inventory
Rest.InventoryServices.AddItem(xml);
rdata.appendStatus(String.Format("
Storing item {0} UUID {1}
", 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));
}
///
/// 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.
///
/// HTTP service request work area
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("
Deleted folder {0} UUID {1}
", 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);
Rest.InventoryServices.DeleteItem(item);
rdata.appendStatus(String.Format("
Deleted item {0} UUID {1}
", item.Name, item.ID));
}
rdata.Complete();
rdata.Respond(String.Format("Profile {0} : Normal completion", rdata.method));
}
#endregion method-specific processing
///
/// 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.
///
///
/// HTTP service request work area
/// The folder to be searched (parent)
/// URI parameter index
/// Should missing path members be created?
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 */
}
///
/// This routine traverse the inventory's structure until the end-point identified
/// in the URI is reached, the remainder of the inventory (if any) is then formatted
/// and returned to the requestor.
///
/// Note that this method is only interested in those folder that match elements of
/// the URI supplied by the requestor, so once a match is fund, the processing does
/// not need to consider any further elements.
///
/// Only the last element in the URI should identify an item.
///
/// HTTP service request work area
/// The folder to be searched (parent)
/// URI parameter index
private void traverse(InventoryRequestData rdata, InventoryFolderBase folder, int pi)
{
Rest.Log.DebugFormat("{0} Traverse[initial] : {1} {2} [{3}]", MsgId, folder.ID, folder.Name, pi);
if (rdata.folders != null)
{
// If there was only one parameter (avatar name), then the entire
// inventory is being requested.
if (rdata.Parameters.Length == 1)
{
formatInventory(rdata, rdata.root, String.Empty);
}
// Has the client specified the root directory name explicitly?
// if yes, then we just absorb the reference, because the folder
// we start looking in for a match *is* the root directory. If there
// are more parameters remaining we tarverse, otehrwise it's time
// to format. Otherwise,we consider the "My Inventory" to be implied
// and we just traverse normally.
else if (folder.ID.ToString() == rdata.Parameters[pi] ||
folder.Name == rdata.Parameters[pi])
{
// Length is -1 because the avatar name is a parameter
if (pi<(rdata.Parameters.Length-1))
{
traverseInventory(rdata, folder, pi+1);
}
else
{
formatInventory(rdata, folder, String.Empty);
}
}
else
{
traverseInventory(rdata, folder, pi);
}
return;
}
}
///
/// 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.
///
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]));
}
///
/// This method generates XML that describes an instance of InventoryFolderBase.
/// It recurses as necessary to reflect a folder hierarchy, and calls formatItem
/// to generate XML for any items encountered along the way.
/// The indentation parameter is solely for the benefit of trace record
/// formatting.
///
/// HTTP service request work area
/// The folder to be searched (parent)
/// pretty print indentation
private void formatInventory(InventoryRequestData rdata, InventoryFolderBase folder, string indent)
{
if (Rest.DEBUG)
{
Rest.Log.DebugFormat("{0} Folder : {1} {2} {3} type = {4}",
MsgId, folder.ID, indent, folder.Name, folder.Type);
indent += "\t";
}
// Start folder item
rdata.writer.WriteStartElement(String.Empty,"Folder",String.Empty);
rdata.writer.WriteAttributeString("name",String.Empty,folder.Name);
rdata.writer.WriteAttributeString("uuid",String.Empty,folder.ID.ToString());
rdata.writer.WriteAttributeString("parent",String.Empty,folder.ParentID.ToString());
rdata.writer.WriteAttributeString("owner",String.Empty,folder.Owner.ToString());
rdata.writer.WriteAttributeString("type",String.Empty,folder.Type.ToString());
rdata.writer.WriteAttributeString("version",String.Empty,folder.Version.ToString());
if (rdata.folders != null)
{
foreach (InventoryFolderBase f in rdata.folders)
{
if (f.ParentID == folder.ID)
{
formatInventory(rdata, f, indent);
}
}
}
if (rdata.items != null)
{
foreach (InventoryItemBase i in rdata.items)
{
if (i.Folder == folder.ID)
{
formatItem(rdata, i, indent);
}
}
}
// End folder item
rdata.writer.WriteEndElement();
}
///
/// This method generates XML that describes an instance of InventoryItemBase.
///
/// HTTP service request work area
/// The item to be formatted
/// Pretty print indentation
private void formatItem(InventoryRequestData rdata, InventoryItemBase i, string indent)
{
Rest.Log.DebugFormat("{0} Item : {1} {2} {3} Type = {4}, AssetType = {5}",
MsgId, i.ID, indent, i.Name, i.InvType, i.AssetType);
rdata.writer.WriteStartElement(String.Empty, "Item", String.Empty);
rdata.writer.WriteAttributeString("name", String.Empty, i.Name);
rdata.writer.WriteAttributeString("desc", String.Empty, i.Description);
rdata.writer.WriteAttributeString("uuid", String.Empty, i.ID.ToString());
rdata.writer.WriteAttributeString("folder", String.Empty, i.Folder.ToString());
rdata.writer.WriteAttributeString("owner", String.Empty, i.Owner.ToString());
rdata.writer.WriteAttributeString("creator", String.Empty, i.CreatorId);
rdata.writer.WriteAttributeString("creationdate", String.Empty, i.CreationDate.ToString());
rdata.writer.WriteAttributeString("invtype", String.Empty, i.InvType.ToString());
rdata.writer.WriteAttributeString("assettype", String.Empty, i.AssetType.ToString());
rdata.writer.WriteAttributeString("groupowned", String.Empty, i.GroupOwned.ToString());
rdata.writer.WriteAttributeString("groupid", String.Empty, i.GroupID.ToString());
rdata.writer.WriteAttributeString("saletype", String.Empty, i.SaleType.ToString());
rdata.writer.WriteAttributeString("saleprice", String.Empty, i.SalePrice.ToString());
rdata.writer.WriteAttributeString("flags", String.Empty, i.Flags.ToString());
rdata.writer.WriteStartElement(String.Empty, "Permissions", String.Empty);
rdata.writer.WriteAttributeString("current", String.Empty, i.CurrentPermissions.ToString("X"));
rdata.writer.WriteAttributeString("next", String.Empty, i.NextPermissions.ToString("X"));
rdata.writer.WriteAttributeString("group", String.Empty, i.GroupPermissions.ToString("X"));
rdata.writer.WriteAttributeString("everyone", String.Empty, i.EveryOnePermissions.ToString("X"));
rdata.writer.WriteAttributeString("base", String.Empty, i.BasePermissions.ToString("X"));
rdata.writer.WriteEndElement();
rdata.writer.WriteElementString("Asset", i.AssetID.ToString());
rdata.writer.WriteEndElement();
}
///
/// 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.
///
/// HTTP service request work area
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;
}
///
/// Make sure that an unchanged folder is not unnecessarily
/// processed.
///
/// Folder obtained from enclosed entity
/// Folder obtained from the user's inventory
private bool FolderHasChanged(InventoryFolderBase newf, InventoryFolderBase oldf)
{
return (newf.Name != oldf.Name
|| newf.ParentID != oldf.ParentID
|| newf.Owner != oldf.Owner
|| newf.Type != oldf.Type
|| newf.Version != oldf.Version
);
}
///
/// Make sure that an unchanged item is not unnecessarily
/// processed.
///
/// Item obtained from enclosed entity
/// Item obtained from the user's inventory
private bool ItemHasChanged(InventoryItemBase newf, InventoryItemBase oldf)
{
return (newf.Name != oldf.Name
|| newf.Folder != oldf.Folder
|| newf.Description != oldf.Description
|| newf.Owner != oldf.Owner
|| newf.CreatorId != oldf.CreatorId
|| newf.AssetID != oldf.AssetID
|| newf.GroupID != oldf.GroupID
|| newf.GroupOwned != oldf.GroupOwned
|| newf.InvType != oldf.InvType
|| newf.AssetType != oldf.AssetType
);
}
///
/// This method is called by PUT and POST to create an XmlInventoryCollection
/// instance that reflects the content of the entity supplied on the request.
/// Any elements in the completed collection whose UUID is zero, are
/// considered to be located relative to the end-point identified int he
/// URI. In this way, an entire sub-tree can be conveyed in a single REST
/// PUT or POST request.
///
/// A new instance of XmlInventoryCollection is created and, if the request
/// has an entity, it is more completely initialized. thus, if no entity was
/// provided the collection is valid, but empty.
///
/// The entity is then scanned and each tag is processed to produce the
/// appropriate inventory elements. At the end f the scan, teh XmlInventoryCollection
/// will reflect the subtree described by the entity.
///
/// This is a very flexible mechanism, the entity may contain arbitrary,
/// discontiguous tree fragments, or may contain single element. The caller is
/// responsible for integrating this collection (and ensuring that any
/// missing parent IDs are resolved).
///
/// HTTP service request work area
internal XmlInventoryCollection ReconstituteEntity(InventoryRequestData rdata)
{
Rest.Log.DebugFormat("{0} Reconstituting entity", MsgId);
XmlInventoryCollection ic = new XmlInventoryCollection();
if (rdata.request.HasEntityBody)
{
Rest.Log.DebugFormat("{0} Entity present", MsgId);
ic.init(rdata);
try
{
while (ic.xml.Read())
{
switch (ic.xml.NodeType)
{
case XmlNodeType.Element:
Rest.Log.DebugFormat("{0} StartElement: <{1}>",
MsgId, ic.xml.Name);
switch (ic.xml.Name)
{
case "Folder":
Rest.Log.DebugFormat("{0} Processing {1} element",
MsgId, ic.xml.Name);
CollectFolder(ic);
break;
case "Item":
Rest.Log.DebugFormat("{0} Processing {1} element",
MsgId, ic.xml.Name);
CollectItem(ic);
break;
case "Asset":
Rest.Log.DebugFormat("{0} Processing {1} element",
MsgId, ic.xml.Name);
CollectAsset(ic);
break;
case "Permissions":
Rest.Log.DebugFormat("{0} Processing {1} element",
MsgId, ic.xml.Name);
CollectPermissions(ic);
break;
default:
Rest.Log.DebugFormat("{0} Ignoring {1} element",
MsgId, ic.xml.Name);
break;
}
// This stinks, but the ReadElement call above not only reads
// the imbedded data, but also consumes the end tag for Asset
// and moves the element pointer on to the containing Item's
// element-end, however, if there was a permissions element
// following, it would get us to the start of that..
if (ic.xml.NodeType == XmlNodeType.EndElement &&
ic.xml.Name == "Item")
{
Validate(ic);
}
break;
case XmlNodeType.EndElement :
switch (ic.xml.Name)
{
case "Folder":
Rest.Log.DebugFormat("{0} Completing {1} element",
MsgId, ic.xml.Name);
ic.Pop();
break;
case "Item":
Rest.Log.DebugFormat("{0} Completing {1} element",
MsgId, ic.xml.Name);
Validate(ic);
break;
case "Asset":
Rest.Log.DebugFormat("{0} Completing {1} element",
MsgId, ic.xml.Name);
break;
case "Permissions":
Rest.Log.DebugFormat("{0} Completing {1} element",
MsgId, ic.xml.Name);
break;
default:
Rest.Log.DebugFormat("{0} Ignoring {1} element",
MsgId, ic.xml.Name);
break;
}
break;
default:
Rest.Log.DebugFormat("{0} Ignoring: <{1}>:<{2}>",
MsgId, ic.xml.NodeType, ic.xml.Value);
break;
}
}
}
catch (XmlException e)
{
Rest.Log.WarnFormat("{0} XML parsing error: {1}", MsgId, e.Message);
throw e;
}
catch (Exception e)
{
Rest.Log.WarnFormat("{0} Unexpected XML parsing error: {1}", MsgId, e.Message);
throw e;
}
}
else
{
Rest.Log.DebugFormat("{0} Entity absent", MsgId);
}
if (Rest.DEBUG)
{
Rest.Log.DebugFormat("{0} Reconstituted entity", MsgId);
Rest.Log.DebugFormat("{0} {1} assets", MsgId, ic.Assets.Count);
Rest.Log.DebugFormat("{0} {1} folder", MsgId, ic.Folders.Count);
Rest.Log.DebugFormat("{0} {1} items", MsgId, ic.Items.Count);
}
return ic;
}
///
/// This method creates an inventory Folder from the
/// information supplied in the request's entity.
/// A folder instance is created and initialized to reflect
/// default values. These values are then overridden
/// by information supplied in the entity.
/// If context was not explicitly provided, then the
/// appropriate ID values are determined.
///
private void CollectFolder(XmlInventoryCollection ic)
{
Rest.Log.DebugFormat("{0} Interpret folder element", MsgId);
InventoryFolderBase result = new InventoryFolderBase();
// Default values
result.Name = String.Empty;
result.ID = UUID.Zero;
result.Owner = ic.UserID;
result.ParentID = UUID.Zero; // Context
result.Type = (short) AssetType.Folder;
result.Version = 1;
if (ic.xml.HasAttributes)
{
for (int i = 0; i < ic.xml.AttributeCount; i++)
{
ic.xml.MoveToAttribute(i);
switch (ic.xml.Name)
{
case "name":
result.Name = ic.xml.Value;
break;
case "uuid":
result.ID = new UUID(ic.xml.Value);
break;
case "parent":
result.ParentID = new UUID(ic.xml.Value);
break;
case "owner":
result.Owner = new UUID(ic.xml.Value);
break;
case "type":
result.Type = Int16.Parse(ic.xml.Value);
break;
case "version":
result.Version = UInt16.Parse(ic.xml.Value);
break;
default:
Rest.Log.DebugFormat("{0} Folder: unrecognized attribute: {1}:{2}",
MsgId, ic.xml.Name, ic.xml.Value);
ic.Fail(Rest.HttpStatusCodeBadRequest, String.Format("unrecognized attribute <{0}>",
ic.xml.Name));
break;
}
}
}
ic.xml.MoveToElement();
// The client is relying upon the reconstitution process
// to determine the parent's UUID based upon context. This
// is necessary where a new folder may have been
// introduced.
if (result.ParentID == UUID.Zero)
{
result.ParentID = ic.Parent();
}
else
{
bool found = false;
foreach (InventoryFolderBase parent in ic.rdata.folders)
{
if (parent.ID == result.ParentID)
{
found = true;
break;
}
}
if (!found)
{
Rest.Log.ErrorFormat("{0} Invalid parent ID ({1}) in folder {2}",
MsgId, ic.Item.Folder, result.ID);
ic.Fail(Rest.HttpStatusCodeBadRequest, "invalid parent");
}
}
// This is a new folder, so no existing UUID is available
// or appropriate
if (result.ID == UUID.Zero)
{
result.ID = UUID.Random();
}
// Treat this as a new context. Any other information is
// obsolete as a consequence.
ic.Push(result);
}
///
/// This method is called to handle the construction of an Item
/// instance from the supplied request entity. It is called
/// whenever an Item start tag is detected.
/// An instance of an Item is created and initialized to default
/// values. These values are then overridden from values supplied
/// as attributes to the Item element.
/// This item is then stored in the XmlInventoryCollection and
/// will be verified by Validate.
/// All context is reset whenever the effective folder changes
/// or an item is successfully validated.
///
private void CollectItem(XmlInventoryCollection ic)
{
Rest.Log.DebugFormat("{0} Interpret item element", MsgId);
InventoryItemBase result = new InventoryItemBase();
result.Name = String.Empty;
result.Description = String.Empty;
result.ID = UUID.Zero;
result.Folder = UUID.Zero;
result.Owner = ic.UserID;
result.CreatorId = ic.UserID.ToString();
result.AssetID = UUID.Zero;
result.GroupID = UUID.Zero;
result.GroupOwned = false;
result.InvType = (int) InventoryType.Unknown;
result.AssetType = (int) AssetType.Unknown;
if (ic.xml.HasAttributes)
{
for (int i = 0; i < ic.xml.AttributeCount; i++)
{
ic.xml.MoveToAttribute(i);
switch (ic.xml.Name)
{
case "name":
result.Name = ic.xml.Value;
break;
case "desc":
result.Description = ic.xml.Value;
break;
case "uuid":
result.ID = new UUID(ic.xml.Value);
break;
case "folder":
result.Folder = new UUID(ic.xml.Value);
break;
case "owner":
result.Owner = new UUID(ic.xml.Value);
break;
case "invtype":
result.InvType = Int32.Parse(ic.xml.Value);
break;
case "creator":
result.CreatorId = ic.xml.Value;
break;
case "assettype":
result.AssetType = Int32.Parse(ic.xml.Value);
break;
case "groupowned":
result.GroupOwned = Boolean.Parse(ic.xml.Value);
break;
case "groupid":
result.GroupID = new UUID(ic.xml.Value);
break;
case "flags":
result.Flags = UInt32.Parse(ic.xml.Value);
break;
case "creationdate":
result.CreationDate = Int32.Parse(ic.xml.Value);
break;
case "saletype":
result.SaleType = Byte.Parse(ic.xml.Value);
break;
case "saleprice":
result.SalePrice = Int32.Parse(ic.xml.Value);
break;
default:
Rest.Log.DebugFormat("{0} Item: Unrecognized attribute: {1}:{2}",
MsgId, ic.xml.Name, ic.xml.Value);
ic.Fail(Rest.HttpStatusCodeBadRequest, String.Format("unrecognized attribute",
ic.xml.Name));
break;
}
}
}
ic.xml.MoveToElement();
ic.Push(result);
}
///
/// This method assembles an asset instance from the
/// information supplied in the request's entity. It is
/// called as a result of detecting a start tag for a
/// type of Asset.
/// The information is collected locally, and an asset
/// instance is created only if the basic XML parsing
/// completes successfully.
/// Default values for all parts of the asset are
/// established before overriding them from the supplied
/// XML.
/// If an asset has inline=true as an attribute, then
/// the element contains the data representing the
/// asset. This is saved as the data component.
/// inline=false means that the element's payload is
/// simply the UUID of the asset referenced by the
/// item being constructed.
/// An asset, if created is stored in the
/// XmlInventoryCollection
///
private void CollectAsset(XmlInventoryCollection ic)
{
Rest.Log.DebugFormat("{0} Interpret asset element", MsgId);
string name = String.Empty;
string desc = String.Empty;
sbyte type = (sbyte) AssetType.Unknown;
bool temp = false;
bool local = false;
// This is not a persistent attribute
bool inline = false;
UUID uuid = UUID.Zero;
// Attribute is optional
if (ic.xml.HasAttributes)
{
for (int i = 0; i < ic.xml.AttributeCount; i++)
{
ic.xml.MoveToAttribute(i);
switch (ic.xml.Name)
{
case "name" :
name = ic.xml.Value;
break;
case "type" :
type = SByte.Parse(ic.xml.Value);
break;
case "description" :
desc = ic.xml.Value;
break;
case "temporary" :
temp = Boolean.Parse(ic.xml.Value);
break;
case "uuid" :
uuid = new UUID(ic.xml.Value);
break;
case "inline" :
inline = Boolean.Parse(ic.xml.Value);
break;
case "local" :
local = Boolean.Parse(ic.xml.Value);
break;
default :
Rest.Log.DebugFormat("{0} Asset: Unrecognized attribute: {1}:{2}",
MsgId, ic.xml.Name, ic.xml.Value);
ic.Fail(Rest.HttpStatusCodeBadRequest,
String.Format("unrecognized attribute <{0}>", ic.xml.Name));
break;
}
}
}
ic.xml.MoveToElement();
// If this is a reference to an existing asset, just store the
// asset ID into the item.
if (!inline)
{
if (ic.Item != null)
{
ic.Item.AssetID = new UUID(ic.xml.ReadElementContentAsString());
Rest.Log.DebugFormat("{0} Asset ID supplied: {1}", MsgId, ic.Item.AssetID);
}
else
{
Rest.Log.DebugFormat("{0} LLUID unimbedded asset must be inline", MsgId);
ic.Fail(Rest.HttpStatusCodeBadRequest, "no context for asset");
}
}
// Otherwise, generate an asset ID, store that into the item, and
// create an entry in the asset list for the inlined asset. But
// only if the size is non-zero.
else
{
AssetBase asset = null;
string b64string = null;
// Generate a UUID if none were given, and generally none should
// be. Ever.
if (uuid == UUID.Zero)
{
uuid = UUID.Random();
}
// Create AssetBase entity to hold the inlined asset
asset = new AssetBase(uuid, name);
asset.Description = desc;
asset.Type = type; // type == 0 == texture
asset.Local = local;
asset.Temporary = temp;
b64string = ic.xml.ReadElementContentAsString();
Rest.Log.DebugFormat("{0} Data length is {1}", MsgId, b64string.Length);
Rest.Log.DebugFormat("{0} Data content starts with: \n\t<{1}>", MsgId,
b64string.Substring(0, b64string.Length > 132 ? 132 : b64string.Length));
asset.Data = Convert.FromBase64String(b64string);
// Ensure the asset always has some kind of data component
if (asset.Data == null)
{
asset.Data = new byte[1];
}
// If this is in the context of an item, establish
// a link with the item in context.
if (ic.Item != null && ic.Item.AssetID == UUID.Zero)
{
ic.Item.AssetID = uuid;
}
ic.Push(asset);
}
}
///
/// Store any permissions information provided by the request.
/// This overrides the default permissions set when the
/// XmlInventoryCollection object was created.
///
private void CollectPermissions(XmlInventoryCollection ic)
{
if (ic.xml.HasAttributes)
{
for (int i = 0; i < ic.xml.AttributeCount; i++)
{
ic.xml.MoveToAttribute(i);
switch (ic.xml.Name)
{
case "current":
ic.CurrentPermissions = UInt32.Parse(ic.xml.Value, NumberStyles.HexNumber);
break;
case "next":
ic.NextPermissions = UInt32.Parse(ic.xml.Value, NumberStyles.HexNumber);
break;
case "group":
ic.GroupPermissions = UInt32.Parse(ic.xml.Value, NumberStyles.HexNumber);
break;
case "everyone":
ic.EveryOnePermissions = UInt32.Parse(ic.xml.Value, NumberStyles.HexNumber);
break;
case "base":
ic.BasePermissions = UInt32.Parse(ic.xml.Value, NumberStyles.HexNumber);
break;
default:
Rest.Log.DebugFormat("{0} Permissions: invalid attribute {1}:{2}",
MsgId,ic.xml.Name, ic.xml.Value);
ic.Fail(Rest.HttpStatusCodeBadRequest,
String.Format("invalid attribute <{0}>", ic.xml.Name));
break;
}
}
}
ic.xml.MoveToElement();
}
///
/// This method is called whenever an Item has been successfully
/// reconstituted from the request's entity.
/// It uses the information curren tin the XmlInventoryCollection
/// to complete the item's specification, including any implied
/// context and asset associations.
/// It fails the request if any necessary item or asset information
/// is missing.
///
private void Validate(XmlInventoryCollection ic)
{
// There really should be an item present if we've
// called validate. So fail if there is not.
if (ic.Item == null)
{
Rest.Log.ErrorFormat("{0} Unable to parse request", MsgId);
ic.Fail(Rest.HttpStatusCodeBadRequest, "request parse error");
}
// Every item is required to have a name (via REST anyway)
if (ic.Item.Name == String.Empty)
{
Rest.Log.ErrorFormat("{0} An item name MUST be specified", MsgId);
ic.Fail(Rest.HttpStatusCodeBadRequest, "item name required");
}
// An item MUST have an asset ID. AssetID should never be zero
// here. It should always get set from the information stored
// when the Asset element was processed.
if (ic.Item.AssetID == UUID.Zero)
{
Rest.Log.ErrorFormat("{0} Unable to complete request", MsgId);
Rest.Log.InfoFormat("{0} Asset information is missing", MsgId);
ic.Fail(Rest.HttpStatusCodeBadRequest, "asset information required");
}
// If the item is new, then assign it an ID
if (ic.Item.ID == UUID.Zero)
{
ic.Item.ID = UUID.Random();
}
// If the context is being implied, obtain the current
// folder item's ID. If it was specified explicitly, make
// sure that theparent folder exists.
if (ic.Item.Folder == UUID.Zero)
{
ic.Item.Folder = ic.Parent();
}
else
{
bool found = false;
foreach (InventoryFolderBase parent in ic.rdata.folders)
{
if (parent.ID == ic.Item.Folder)
{
found = true;
break;
}
}
if (!found)
{
Rest.Log.ErrorFormat("{0} Invalid parent ID ({1}) in item {2}",
MsgId, ic.Item.Folder, ic.Item.ID);
ic.Fail(Rest.HttpStatusCodeBadRequest, "parent information required");
}
}
// If this is an inline asset being constructed in the context
// of a new Item, then use the itm's name here too.
if (ic.Asset != null)
{
if (ic.Asset.Name == String.Empty)
ic.Asset.Name = ic.Item.Name;
if (ic.Asset.Description == String.Empty)
ic.Asset.Description = ic.Item.Description;
}
// Assign permissions
ic.Item.CurrentPermissions = ic.CurrentPermissions;
ic.Item.EveryOnePermissions = ic.EveryOnePermissions;
ic.Item.BasePermissions = ic.BasePermissions;
ic.Item.GroupPermissions = ic.GroupPermissions;
ic.Item.NextPermissions = ic.NextPermissions;
// If no type was specified for this item, we can attempt to
// infer something from the file type maybe. This is NOT as
// good as having type be specified in the XML.
if (ic.Item.AssetType == (int) AssetType.Unknown ||
ic.Item.InvType == (int) InventoryType.Unknown)
{
Rest.Log.DebugFormat("{0} Attempting to infer item type", MsgId);
string[] parts = ic.Item.Name.Split(Rest.CA_PERIOD);
if (Rest.DEBUG)
{
for (int i = 0; i < parts.Length; i++)
{
Rest.Log.DebugFormat("{0} Name part {1} : {2}",
MsgId, i, parts[i]);
}
}
// If the associated item name is multi-part, then maybe
// the last part will indicate the item type - if we're
// lucky.
if (parts.Length > 1)
{
Rest.Log.DebugFormat("{0} File type is {1}",
MsgId, parts[parts.Length - 1]);
switch (parts[parts.Length - 1])
{
case "jpeg2000" :
case "jpeg-2000" :
case "jpg2000" :
case "jpg-2000" :
Rest.Log.DebugFormat("{0} Type {1} inferred",
MsgId, parts[parts.Length-1]);
if (ic.Item.AssetType == (int) AssetType.Unknown)
ic.Item.AssetType = (int) AssetType.ImageJPEG;
if (ic.Item.InvType == (int) InventoryType.Unknown)
ic.Item.InvType = (int) InventoryType.Texture;
break;
case "jpg" :
case "jpeg" :
Rest.Log.DebugFormat("{0} Type {1} inferred",
MsgId, parts[parts.Length - 1]);
if (ic.Item.AssetType == (int) AssetType.Unknown)
ic.Item.AssetType = (int) AssetType.ImageJPEG;
if (ic.Item.InvType == (int) InventoryType.Unknown)
ic.Item.InvType = (int) InventoryType.Texture;
break;
case "tga" :
if (parts[parts.Length - 2].IndexOf("_texture") != -1)
{
if (ic.Item.AssetType == (int) AssetType.Unknown)
ic.Item.AssetType = (int) AssetType.TextureTGA;
if (ic.Item.InvType == (int) AssetType.Unknown)
ic.Item.InvType = (int) InventoryType.Texture;
}
else
{
if (ic.Item.AssetType == (int) AssetType.Unknown)
ic.Item.AssetType = (int) AssetType.ImageTGA;
if (ic.Item.InvType == (int) InventoryType.Unknown)
ic.Item.InvType = (int) InventoryType.Snapshot;
}
break;
default :
Rest.Log.DebugFormat("{0} Asset/Inventory type could not be inferred for {1}",
MsgId,ic.Item.Name);
break;
}
}
}
/// If this is a TGA remember the fact
if (ic.Item.AssetType == (int) AssetType.TextureTGA ||
ic.Item.AssetType == (int) AssetType.ImageTGA)
{
Bitmap temp;
Stream tgadata = new MemoryStream(ic.Asset.Data);
temp = LoadTGAClass.LoadTGA(tgadata);
try
{
ic.Asset.Data = OpenJPEG.EncodeFromImage(temp, true);
}
catch (DllNotFoundException)
{
Rest.Log.ErrorFormat("OpenJpeg is not installed correctly on this system. Asset Data is emtpy for {0}", ic.Item.Name);
ic.Asset.Data = new Byte[0];
}
catch (IndexOutOfRangeException)
{
Rest.Log.ErrorFormat("OpenJpeg was unable to encode this. Asset Data is emtpy for {0}", ic.Item.Name);
ic.Asset.Data = new Byte[0];
}
catch (Exception)
{
Rest.Log.ErrorFormat("OpenJpeg was unable to encode this. Asset Data is emtpy for {0}", ic.Item.Name);
ic.Asset.Data = new Byte[0];
}
}
ic.reset();
}
#region Inventory RequestData extension
internal class InventoryRequestData : RequestData
{
///
/// These are the inventory specific request/response state
/// extensions.
///
internal UUID uuid = UUID.Zero;
internal bool HaveInventory = false;
internal ICollection folders = null;
internal ICollection items = null;
internal UserProfileData userProfile = null;
internal InventoryFolderBase root = null;
internal bool timeout = false;
internal Timer watchDog = new Timer();
internal InventoryRequestData(OSHttpRequest request, OSHttpResponse response, string prefix)
: base(request, response, prefix)
{
}
internal void startWD(double interval)
{
Rest.Log.DebugFormat("{0} Setting watchdog", MsgId);
watchDog.Elapsed += new ElapsedEventHandler(OnTimeOut);
watchDog.Interval = interval;
watchDog.AutoReset = false;
watchDog.Enabled = true;
watchDog.Start();
}
internal void stopWD()
{
Rest.Log.DebugFormat("{0} Reset watchdog", MsgId);
watchDog.Stop();
}
///
/// This is the callback method required by the inventory watchdog. The
/// requestor issues an inventory request and then blocks until the
/// request completes, or this method signals the monitor.
///
private void OnTimeOut(object sender, ElapsedEventArgs args)
{
Rest.Log.DebugFormat("{0} Asynchronous inventory update timed-out", MsgId);
// InventoryRequestData rdata = (InventoryRequestData) sender;
lock (this)
{
this.folders = null;
this.items = null;
this.HaveInventory = false;
this.timeout = true;
Monitor.Pulse(this);
}
}
///
/// This is the callback method required by inventory services. The
/// requestor issues an inventory request and then blocks until this
/// method signals the monitor.
///
internal void GetUserInventory(ICollection folders, ICollection items)
{
Rest.Log.DebugFormat("{0} Asynchronously updating inventory data", MsgId);
lock (this)
{
if (watchDog.Enabled)
{
this.stopWD();
}
this.folders = folders;
this.items = items;
this.HaveInventory = true;
this.timeout = false;
Monitor.Pulse(this);
}
}
}
#endregion Inventory RequestData extension
///
/// This class is used to record and manage the hierarchy
/// constructed from the entity supplied in the request for
/// PUT and POST.
///
internal class XmlInventoryCollection : InventoryCollection
{
internal InventoryRequestData rdata;
private Stack stk;
internal List Assets;
internal InventoryItemBase Item;
internal AssetBase Asset;
internal XmlReader xml;
internal /*static*/ const uint DefaultCurrent = 0x7FFFFFFF;
internal /*static*/ const uint DefaultNext = 0x82000;
internal /*static*/ const uint DefaultBase = 0x7FFFFFFF;
internal /*static*/ const uint DefaultEveryOne = 0x0;
internal /*static*/ const uint DefaultGroup = 0x0;
internal uint CurrentPermissions = 0x00;
internal uint NextPermissions = 0x00;
internal uint BasePermissions = 0x00;
internal uint EveryOnePermissions = 0x00;
internal uint GroupPermissions = 0x00;
internal XmlInventoryCollection()
{
Folders = new List();
Items = new List();
Assets = new List();
}
internal void init(InventoryRequestData p_rdata)
{
rdata = p_rdata;
UserID = rdata.uuid;
stk = new Stack();
rdata.initXmlReader();
xml = rdata.reader;
initPermissions();
}
internal void initPermissions()
{
CurrentPermissions = DefaultCurrent;
NextPermissions = DefaultNext;
BasePermissions = DefaultBase;
GroupPermissions = DefaultGroup;
EveryOnePermissions = DefaultEveryOne;
}
internal UUID Parent()
{
if (stk.Count != 0)
{
return stk.Peek().ID;
}
else
{
return UUID.Zero;
}
}
internal void Push(InventoryFolderBase folder)
{
stk.Push(folder);
Folders.Add(folder);
reset();
}
internal void Push(InventoryItemBase item)
{
Item = item;
Items.Add(item);
}
internal void Push(AssetBase asset)
{
Asset = asset;
Assets.Add(asset);
}
internal void Pop()
{
stk.Pop();
reset();
}
internal void reset()
{
Item = null;
Asset = null;
initPermissions();
}
internal void Fail(int code, string addendum)
{
rdata.Fail(code, addendum);
}
}
}
}