aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/OpenSim/Framework/Communications/Cache
diff options
context:
space:
mode:
authorMelanie2010-03-03 02:07:03 +0000
committerMelanie2010-03-03 02:07:03 +0000
commit028a87fe37002e7a0611f66babf1deee46c83804 (patch)
tree387aec499fd60c2012bed8148e6a2ddc847c3d95 /OpenSim/Framework/Communications/Cache
parentRevert "test" (diff)
parentFixes Region.Framework tests. Although these tests don't fail, they need to b... (diff)
downloadopensim-SC-028a87fe37002e7a0611f66babf1deee46c83804.zip
opensim-SC-028a87fe37002e7a0611f66babf1deee46c83804.tar.gz
opensim-SC-028a87fe37002e7a0611f66babf1deee46c83804.tar.bz2
opensim-SC-028a87fe37002e7a0611f66babf1deee46c83804.tar.xz
Merge branch 'master' into careminster-presence-refactor
This brings careminster on the level of master. To be tested
Diffstat (limited to '')
-rw-r--r--OpenSim/Framework/Communications/Cache/CachedUserInfo.cs847
-rw-r--r--OpenSim/Framework/Communications/Cache/UserProfileCacheService.cs277
-rw-r--r--OpenSim/Services/InventoryService/LibraryService.cs (renamed from OpenSim/Framework/Communications/Cache/LibraryRootFolder.cs)62
3 files changed, 44 insertions, 1142 deletions
diff --git a/OpenSim/Framework/Communications/Cache/CachedUserInfo.cs b/OpenSim/Framework/Communications/Cache/CachedUserInfo.cs
deleted file mode 100644
index 6648c36..0000000
--- a/OpenSim/Framework/Communications/Cache/CachedUserInfo.cs
+++ /dev/null
@@ -1,847 +0,0 @@
1/*
2 * Copyright (c) Contributors, http://opensimulator.org/
3 * See CONTRIBUTORS.TXT for a full list of copyright holders.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are met:
7 * * Redistributions of source code must retain the above copyright
8 * notice, this list of conditions and the following disclaimer.
9 * * Redistributions in binary form must reproduce the above copyright
10 * notice, this list of conditions and the following disclaimer in the
11 * documentation and/or other materials provided with the distribution.
12 * * Neither the name of the OpenSimulator Project nor the
13 * names of its contributors may be used to endorse or promote products
14 * derived from this software without specific prior written permission.
15 *
16 * THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY
17 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19 * DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY
20 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
21 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
22 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
23 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 */
27
28using System;
29using System.Collections.Generic;
30using System.Reflection;
31using log4net;
32using OpenMetaverse;
33using OpenSim.Services.Interfaces;
34
35namespace OpenSim.Framework.Communications.Cache
36{
37 internal delegate void AddItemDelegate(InventoryItemBase itemInfo);
38 internal delegate void UpdateItemDelegate(InventoryItemBase itemInfo);
39 internal delegate void DeleteItemDelegate(UUID itemID);
40 internal delegate void QueryItemDelegate(UUID itemID);
41 internal delegate void QueryFolderDelegate(UUID folderID);
42
43 internal delegate void CreateFolderDelegate(string folderName, UUID folderID, ushort folderType, UUID parentID);
44 internal delegate void MoveFolderDelegate(UUID folderID, UUID parentID);
45 internal delegate void PurgeFolderDelegate(UUID folderID);
46 internal delegate void UpdateFolderDelegate(string name, UUID folderID, ushort type, UUID parentID);
47
48 internal delegate void SendInventoryDescendentsDelegate(
49 IClientAPI client, UUID folderID, bool fetchFolders, bool fetchItems);
50
51 public delegate void OnItemReceivedDelegate(UUID itemID);
52 public delegate void OnInventoryReceivedDelegate(UUID userID);
53
54 /// <summary>
55 /// Stores user profile and inventory data received from backend services for a particular user.
56 /// </summary>
57 public class CachedUserInfo
58 {
59 private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
60
61 //// <value>
62 /// Fired when a particular item has been received from the inventory service
63 /// </value>
64 public event OnItemReceivedDelegate OnItemReceived;
65
66 /// <value>
67 /// Fired once the entire inventory has been received for the user
68 /// </value>
69 public event OnInventoryReceivedDelegate OnInventoryReceived;
70
71 /// <summary>
72 /// The comms manager holds references to services (user, grid, inventory, etc.)
73 /// </summary>
74 private readonly IInventoryService m_InventoryService;
75
76 public UserProfileData UserProfile { get { return m_userProfile; } }
77 private UserProfileData m_userProfile;
78
79 /// <summary>
80 /// Have we received the user's inventory from the inventory service?
81 /// </summary>
82 public bool HasReceivedInventory { get { return m_hasReceivedInventory; } }
83 private bool m_hasReceivedInventory;
84
85 /// <summary>
86 /// Inventory requests waiting for receipt of this user's inventory from the inventory service.
87 /// </summary>
88 private readonly IList<IInventoryRequest> m_pendingRequests = new List<IInventoryRequest>();
89
90 /// <summary>
91 /// The root folder of this user's inventory. Returns null if the root folder has not yet been received.
92 /// </summary>
93 public InventoryFolderImpl RootFolder { get { return m_rootFolder; } }
94 private InventoryFolderImpl m_rootFolder;
95
96 public UUID SessionID
97 {
98 get { return m_session_id; }
99 set { m_session_id = value; }
100 }
101 private UUID m_session_id = UUID.Zero;
102
103 /// <summary>
104 /// Constructor
105 /// </summary>
106 /// <param name="commsManager"></param>
107 /// <param name="userProfile"></param>
108 public CachedUserInfo(IInventoryService invService, UserProfileData userProfile)
109 {
110 m_userProfile = userProfile;
111 m_InventoryService = invService;
112 }
113
114 /// <summary>
115 /// This allows a request to be added to be processed once we receive a user's inventory
116 /// from the inventory service. If we already have the inventory, the request
117 /// is executed immediately instead.
118 /// </summary>
119 /// <param name="parent"></param>
120 protected void AddRequest(IInventoryRequest request)
121 {
122 lock (m_pendingRequests)
123 {
124 if (HasReceivedInventory)
125 {
126 request.Execute();
127 }
128 else
129 {
130 m_pendingRequests.Add(request);
131 }
132 }
133 }
134
135 /// <summary>
136 /// Helper function for InventoryReceive() - Store a folder temporarily until we've received entire folder list
137 /// </summary>
138 /// <param name="folder"></param>
139 private void AddFolderToDictionary(InventoryFolderImpl folder, IDictionary<UUID, IList<InventoryFolderImpl>> dictionary)
140 {
141 UUID parentFolderId = folder.ParentID;
142
143 if (dictionary.ContainsKey(parentFolderId))
144 {
145 dictionary[parentFolderId].Add(folder);
146 }
147 else
148 {
149 IList<InventoryFolderImpl> folders = new List<InventoryFolderImpl>();
150 folders.Add(folder);
151 dictionary[parentFolderId] = folders;
152 }
153 }
154
155 /// <summary>
156 /// Recursively, in depth-first order, add all the folders we've received (stored
157 /// in a dictionary indexed by parent ID) into the tree that describes user folder
158 /// heirarchy
159 /// Any folder that is resolved into the tree is also added to resolvedFolderDictionary,
160 /// indexed by folder ID.
161 /// </summary>
162 /// <param name="parentId">
163 /// A <see cref="UUID"/>
164 /// </param>
165 private void ResolveReceivedFolders(InventoryFolderImpl parentFolder,
166 IDictionary<UUID, IList<InventoryFolderImpl>> receivedFolderDictionary,
167 IDictionary<UUID, InventoryFolderImpl> resolvedFolderDictionary)
168 {
169 if (receivedFolderDictionary.ContainsKey(parentFolder.ID))
170 {
171 List<InventoryFolderImpl> resolvedFolders = new List<InventoryFolderImpl>(); // Folders we've resolved with this invocation
172 foreach (InventoryFolderImpl folder in receivedFolderDictionary[parentFolder.ID])
173 {
174 if (parentFolder.ContainsChildFolder(folder.ID))
175 {
176 m_log.WarnFormat(
177 "[INVENTORY CACHE]: Received folder {0} {1} from inventory service which has already been received",
178 folder.Name, folder.ID);
179 }
180 else
181 {
182 if (resolvedFolderDictionary.ContainsKey(folder.ID))
183 {
184 m_log.WarnFormat(
185 "[INVENTORY CACHE]: Received folder {0} {1} from inventory service has already been received but with different parent",
186 folder.Name, folder.ID);
187 }
188 else
189 {
190 resolvedFolders.Add(folder);
191 resolvedFolderDictionary[folder.ID] = folder;
192 parentFolder.AddChildFolder(folder);
193 }
194 }
195 } // foreach (folder in pendingCategorizationFolders[parentFolder.ID])
196
197 receivedFolderDictionary.Remove(parentFolder.ID);
198 foreach (InventoryFolderImpl folder in resolvedFolders)
199 ResolveReceivedFolders(folder, receivedFolderDictionary, resolvedFolderDictionary);
200 } // if (receivedFolderDictionary.ContainsKey(parentFolder.ID))
201 }
202
203 /// <summary>
204 /// Drop all cached inventory.
205 /// </summary>
206 public void DropInventory()
207 {
208 m_log.Debug("[INVENTORY CACHE]: DropInventory called");
209 // Make sure there aren't pending requests around when we do this
210 // FIXME: There is still a race condition where an inventory operation can be requested (since these aren't being locked).
211 // Will have to extend locking to exclude this very soon.
212 lock (m_pendingRequests)
213 {
214 m_hasReceivedInventory = false;
215 m_rootFolder = null;
216 }
217 }
218
219 /// <summary>
220 /// Fetch inventory for this user.
221 /// </summary>
222 /// This has to be executed as a separate step once user information is retreived.
223 /// This will occur synchronously if the inventory service is in the same process as this class, and
224 /// asynchronously otherwise.
225 public void FetchInventory()
226 {
227 m_InventoryService.GetUserInventory(UserProfile.ID, InventoryReceive);
228 }
229
230 /// <summary>
231 /// Callback invoked when the inventory is received from an async request to the inventory service
232 /// </summary>
233 /// <param name="userID"></param>
234 /// <param name="inventoryCollection"></param>
235 public void InventoryReceive(ICollection<InventoryFolderImpl> folders, ICollection<InventoryItemBase> items)
236 {
237 // FIXME: Exceptions thrown upwards never appear on the console. Could fix further up if these
238 // are simply being swallowed
239
240 try
241 {
242 // collection of all received folders, indexed by their parent ID
243 IDictionary<UUID, IList<InventoryFolderImpl>> receivedFolders =
244 new Dictionary<UUID, IList<InventoryFolderImpl>>();
245
246 // collection of all folders that have been placed into the folder heirarchy starting at m_rootFolder
247 // This dictonary exists so we don't have to do an InventoryFolderImpl.FindFolder(), which is O(n) on the
248 // number of folders in our inventory.
249 // Maybe we should make this structure a member so we can skip InventoryFolderImpl.FindFolder() calls later too?
250 IDictionary<UUID, InventoryFolderImpl> resolvedFolders =
251 new Dictionary<UUID, InventoryFolderImpl>();
252
253 // Take all received folders, find the root folder, and put ther rest into
254 // the pendingCategorizationFolders collection
255 foreach (InventoryFolderImpl folder in folders)
256 AddFolderToDictionary(folder, receivedFolders);
257
258 if (!receivedFolders.ContainsKey(UUID.Zero))
259 throw new Exception("Database did not return a root inventory folder");
260 else
261 {
262 IList<InventoryFolderImpl> rootFolderList = receivedFolders[UUID.Zero];
263 m_rootFolder = rootFolderList[0];
264 resolvedFolders[m_rootFolder.ID] = m_rootFolder;
265 if (rootFolderList.Count > 1)
266 {
267 for (int i = 1; i < rootFolderList.Count; i++)
268 {
269 m_log.WarnFormat(
270 "[INVENTORY CACHE]: Discarding extra root folder {0}. Using previously received root folder {1}",
271 rootFolderList[i].ID, RootFolder.ID);
272 }
273 }
274 receivedFolders.Remove(UUID.Zero);
275 }
276
277 // Now take the pendingCategorizationFolders collection, and turn that into a tree,
278 // with the root being RootFolder
279 if (RootFolder != null)
280 ResolveReceivedFolders(RootFolder, receivedFolders, resolvedFolders);
281
282 // Generate a warning for folders that are not part of the heirarchy
283 foreach (KeyValuePair<UUID, IList<InventoryFolderImpl>> folderList in receivedFolders)
284 {
285 foreach (InventoryFolderImpl folder in folderList.Value)
286 m_log.WarnFormat("[INVENTORY CACHE]: Malformed Database: Unresolved Pending Folder {0}", folder.Name);
287 }
288
289 // Take all ther received items and put them into the folder tree heirarchy
290 foreach (InventoryItemBase item in items) {
291 InventoryFolderImpl folder = resolvedFolders.ContainsKey(item.Folder) ? resolvedFolders[item.Folder] : null;
292 ItemReceive(item, folder);
293 }
294 }
295 catch (Exception e)
296 {
297 m_log.ErrorFormat("[INVENTORY CACHE]: Error processing inventory received from inventory service, {0}", e);
298 }
299
300 // Deal with pending requests
301 lock (m_pendingRequests)
302 {
303 // We're going to change inventory status within the lock to avoid a race condition
304 // where requests are processed after the AddRequest() method has been called.
305 m_hasReceivedInventory = true;
306
307 foreach (IInventoryRequest request in m_pendingRequests)
308 {
309 request.Execute();
310 }
311 }
312
313 if (OnInventoryReceived != null)
314 OnInventoryReceived(UserProfile.ID);
315 }
316
317 /// <summary>
318 /// Callback invoked when an item is received from an async request to the inventory service.
319 ///
320 /// We're assuming here that items are always received after all the folders
321 /// received.
322 /// If folder is null, we will search for it starting from RootFolder (an O(n) operation),
323 /// otherwise we'll just put it into folder
324 /// </summary>
325 /// <param name="folderInfo"></param>
326 private void ItemReceive(InventoryItemBase itemInfo, InventoryFolderImpl folder)
327 {
328 // m_log.DebugFormat(
329 // "[INVENTORY CACHE]: Received item {0} {1} for user {2}",
330 // itemInfo.Name, itemInfo.ID, userID);
331
332 if (folder == null && RootFolder != null)
333 folder = RootFolder.FindFolder(itemInfo.Folder);
334
335 if (null == folder)
336 {
337 m_log.WarnFormat(
338 "Received item {0} {1} but its folder {2} does not exist",
339 itemInfo.Name, itemInfo.ID, itemInfo.Folder);
340
341 return;
342 }
343
344 lock (folder.Items)
345 {
346 folder.Items[itemInfo.ID] = itemInfo;
347 }
348
349 if (OnItemReceived != null)
350 OnItemReceived(itemInfo.ID);
351 }
352
353 /// <summary>
354 /// Create a folder in this agent's inventory.
355 /// </summary>
356 ///
357 /// If the inventory service has not yet delievered the inventory
358 /// for this user then the request will be queued.
359 ///
360 /// <param name="parentID"></param>
361 /// <returns></returns>
362 public bool CreateFolder(string folderName, UUID folderID, ushort folderType, UUID parentID)
363 {
364 // m_log.DebugFormat(
365 // "[AGENT INVENTORY]: Creating inventory folder {0} {1} for {2} {3}", folderID, folderName, remoteClient.Name, remoteClient.AgentId);
366
367 if (m_hasReceivedInventory)
368 {
369 InventoryFolderImpl parentFolder = RootFolder.FindFolder(parentID);
370
371 if (null == parentFolder)
372 {
373 m_log.WarnFormat(
374 "[AGENT INVENTORY]: Tried to create folder {0} {1} but the parent {2} does not exist",
375 folderName, folderID, parentID);
376
377 return false;
378 }
379
380 InventoryFolderImpl createdFolder = parentFolder.CreateChildFolder(folderID, folderName, folderType);
381
382 if (createdFolder != null)
383 {
384 InventoryFolderBase createdBaseFolder = new InventoryFolderBase();
385 createdBaseFolder.Owner = createdFolder.Owner;
386 createdBaseFolder.ID = createdFolder.ID;
387 createdBaseFolder.Name = createdFolder.Name;
388 createdBaseFolder.ParentID = createdFolder.ParentID;
389 createdBaseFolder.Type = createdFolder.Type;
390 createdBaseFolder.Version = createdFolder.Version;
391
392 m_InventoryService.AddFolder(createdBaseFolder);
393
394 return true;
395 }
396 else
397 {
398 m_log.WarnFormat(
399 "[AGENT INVENTORY]: Tried to create folder {0} {1} but the folder already exists",
400 folderName, folderID);
401
402 return false;
403 }
404 }
405 else
406 {
407 AddRequest(
408 new InventoryRequest(
409 Delegate.CreateDelegate(typeof(CreateFolderDelegate), this, "CreateFolder"),
410 new object[] { folderName, folderID, folderType, parentID }));
411
412 return true;
413 }
414 }
415
416 /// <summary>
417 /// Handle a client request to update the inventory folder
418 /// </summary>
419 ///
420 /// If the inventory service has not yet delievered the inventory
421 /// for this user then the request will be queued.
422 ///
423 /// FIXME: We call add new inventory folder because in the data layer, we happen to use an SQL REPLACE
424 /// so this will work to rename an existing folder. Needless to say, to rely on this is very confusing,
425 /// and needs to be changed.
426 ///
427 /// <param name="folderID"></param>
428 /// <param name="type"></param>
429 /// <param name="name"></param>
430 /// <param name="parentID"></param>
431 public bool UpdateFolder(string name, UUID folderID, ushort type, UUID parentID)
432 {
433 // m_log.DebugFormat(
434 // "[AGENT INVENTORY]: Updating inventory folder {0} {1} for {2} {3}", folderID, name, remoteClient.Name, remoteClient.AgentId);
435
436 if (m_hasReceivedInventory)
437 {
438 InventoryFolderImpl folder = RootFolder.FindFolder(folderID);
439
440 // Delegate movement if updated parent id isn't the same as the existing parentId
441 if (folder.ParentID != parentID)
442 MoveFolder(folderID, parentID);
443
444 InventoryFolderBase baseFolder = new InventoryFolderBase();
445 baseFolder.Owner = m_userProfile.ID;
446 baseFolder.ID = folderID;
447 baseFolder.Name = name;
448 baseFolder.ParentID = parentID;
449 baseFolder.Type = (short)type;
450 baseFolder.Version = RootFolder.Version;
451
452 m_InventoryService.UpdateFolder(baseFolder);
453
454 folder.Name = name;
455 folder.Type = (short)type;
456 }
457 else
458 {
459 AddRequest(
460 new InventoryRequest(
461 Delegate.CreateDelegate(typeof(UpdateFolderDelegate), this, "UpdateFolder"),
462 new object[] { name, folderID, type, parentID }));
463 }
464
465 return true;
466 }
467
468 /// <summary>
469 /// Handle an inventory folder move request from the client.
470 ///
471 /// If the inventory service has not yet delievered the inventory
472 /// for this user then the request will be queued.
473 /// </summary>
474 ///
475 /// <param name="folderID"></param>
476 /// <param name="parentID"></param>
477 /// <returns>
478 /// true if the delete was successful, or if it was queued pending folder receipt
479 /// false if the folder to be deleted did not exist.
480 /// </returns>
481 public bool MoveFolder(UUID folderID, UUID parentID)
482 {
483 // m_log.DebugFormat(
484 // "[AGENT INVENTORY]: Moving inventory folder {0} into folder {1} for {2} {3}",
485 // parentID, remoteClient.Name, remoteClient.Name, remoteClient.AgentId);
486
487 if (m_hasReceivedInventory)
488 {
489 InventoryFolderBase baseFolder = new InventoryFolderBase();
490 baseFolder.Owner = m_userProfile.ID;
491 baseFolder.ID = folderID;
492 baseFolder.ParentID = parentID;
493
494 m_InventoryService.MoveFolder(baseFolder);
495
496 InventoryFolderImpl folder = RootFolder.FindFolder(folderID);
497 InventoryFolderImpl parentFolder = RootFolder.FindFolder(parentID);
498 if (parentFolder != null && folder != null)
499 {
500 InventoryFolderImpl oldParentFolder = RootFolder.FindFolder(folder.ParentID);
501
502 if (oldParentFolder != null)
503 {
504 oldParentFolder.RemoveChildFolder(folderID);
505 parentFolder.AddChildFolder(folder);
506 }
507 else
508 {
509 return false;
510 }
511 }
512 else
513 {
514 return false;
515 }
516
517 return true;
518 }
519 else
520 {
521 AddRequest(
522 new InventoryRequest(
523 Delegate.CreateDelegate(typeof(MoveFolderDelegate), this, "MoveFolder"),
524 new object[] { folderID, parentID }));
525
526 return true;
527 }
528 }
529
530 /// <summary>
531 /// This method will delete all the items and folders in the given folder.
532 /// </summary>
533 /// If the inventory service has not yet delievered the inventory
534 /// for this user then the request will be queued.
535 ///
536 /// <param name="folderID"></param>
537 public bool PurgeFolder(UUID folderID)
538 {
539 // m_log.InfoFormat("[AGENT INVENTORY]: Purging folder {0} for {1} uuid {2}",
540 // folderID, remoteClient.Name, remoteClient.AgentId);
541
542 if (m_hasReceivedInventory)
543 {
544 InventoryFolderImpl purgedFolder = RootFolder.FindFolder(folderID);
545
546 if (purgedFolder != null)
547 {
548 // XXX Nasty - have to create a new object to hold details we already have
549 InventoryFolderBase purgedBaseFolder = new InventoryFolderBase();
550 purgedBaseFolder.Owner = purgedFolder.Owner;
551 purgedBaseFolder.ID = purgedFolder.ID;
552 purgedBaseFolder.Name = purgedFolder.Name;
553 purgedBaseFolder.ParentID = purgedFolder.ParentID;
554 purgedBaseFolder.Type = purgedFolder.Type;
555 purgedBaseFolder.Version = purgedFolder.Version;
556
557 m_InventoryService.PurgeFolder(purgedBaseFolder);
558
559 purgedFolder.Purge();
560
561 return true;
562 }
563 }
564 else
565 {
566 AddRequest(
567 new InventoryRequest(
568 Delegate.CreateDelegate(typeof(PurgeFolderDelegate), this, "PurgeFolder"),
569 new object[] { folderID }));
570
571 return true;
572 }
573
574 return false;
575 }
576
577 /// <summary>
578 /// Add an item to the user's inventory.
579 /// </summary>
580 /// If the item has no folder set (i.e. it is UUID.Zero), then it is placed in the most appropriate folder
581 /// for that type.
582 /// <param name="itemInfo"></param>
583 public void AddItem(InventoryItemBase item)
584 {
585 if (m_hasReceivedInventory)
586 {
587 if (item.Folder == UUID.Zero)
588 {
589 InventoryFolderImpl f = FindFolderForType(item.AssetType);
590 if (f != null)
591 item.Folder = f.ID;
592 else
593 item.Folder = RootFolder.ID;
594 }
595 ItemReceive(item, null);
596
597 m_InventoryService.AddItem(item);
598 }
599 else
600 {
601 AddRequest(
602 new InventoryRequest(
603 Delegate.CreateDelegate(typeof(AddItemDelegate), this, "AddItem"),
604 new object[] { item }));
605 }
606 }
607
608 /// <summary>
609 /// Update an item in the user's inventory
610 /// </summary>
611 /// <param name="userID"></param>
612 /// <param name="itemInfo"></param>
613 public void UpdateItem(InventoryItemBase item)
614 {
615 if (m_hasReceivedInventory)
616 {
617 m_InventoryService.UpdateItem(item);
618 }
619 else
620 {
621 AddRequest(
622 new InventoryRequest(
623 Delegate.CreateDelegate(typeof(UpdateItemDelegate), this, "UpdateItem"),
624 new object[] { item }));
625 }
626 }
627
628 /// <summary>
629 /// Delete an item from the user's inventory
630 ///
631 /// If the inventory service has not yet delievered the inventory
632 /// for this user then the request will be queued.
633 /// </summary>
634 /// <param name="itemID"></param>
635 /// <returns>
636 /// true on a successful delete or a if the request is queued.
637 /// Returns false on an immediate failure
638 /// </returns>
639 public bool DeleteItem(UUID itemID)
640 {
641 if (m_hasReceivedInventory)
642 {
643 // XXX For historical reasons (grid comms), we need to retrieve the whole item in order to delete, even though
644 // really only the item id is required.
645 InventoryItemBase item = RootFolder.FindItem(itemID);
646
647 if (null == item)
648 {
649 m_log.WarnFormat("[AGENT INVENTORY]: Tried to delete item {0} which does not exist", itemID);
650
651 return false;
652 }
653
654 if (RootFolder.DeleteItem(item.ID))
655 {
656 List<UUID> uuids = new List<UUID>();
657 uuids.Add(itemID);
658 return m_InventoryService.DeleteItems(this.UserProfile.ID, uuids);
659 }
660 }
661 else
662 {
663 AddRequest(
664 new InventoryRequest(
665 Delegate.CreateDelegate(typeof(DeleteItemDelegate), this, "DeleteItem"),
666 new object[] { itemID }));
667
668 return true;
669 }
670
671 return false;
672 }
673
674 /// <summary>
675 /// Send details of the inventory items and/or folders in a given folder to the client.
676 /// </summary>
677 /// <param name="client"></param>
678 /// <param name="folderID"></param>
679 /// <param name="fetchFolders"></param>
680 /// <param name="fetchItems"></param>
681 /// <returns>true if the request was queued or successfully processed, false otherwise</returns>
682 public bool SendInventoryDecendents(IClientAPI client, UUID folderID, int version, bool fetchFolders, bool fetchItems)
683 {
684 if (m_hasReceivedInventory)
685 {
686 InventoryFolderImpl folder;
687
688 if ((folder = RootFolder.FindFolder(folderID)) != null)
689 {
690 // m_log.DebugFormat(
691 // "[AGENT INVENTORY]: Found folder {0} for client {1}",
692 // folderID, remoteClient.AgentId);
693
694 client.SendInventoryFolderDetails(
695 client.AgentId, folderID, folder.RequestListOfItems(),
696 folder.RequestListOfFolders(), version, fetchFolders, fetchItems);
697
698 return true;
699 }
700 else
701 {
702 m_log.WarnFormat(
703 "[AGENT INVENTORY]: Could not find folder {0} requested by user {1} {2}",
704 folderID, client.Name, client.AgentId);
705
706 return false;
707 }
708 }
709 else
710 {
711 AddRequest(
712 new InventoryRequest(
713 Delegate.CreateDelegate(typeof(SendInventoryDescendentsDelegate), this, "SendInventoryDecendents", false, false),
714 new object[] { client, folderID, fetchFolders, fetchItems }));
715
716 return true;
717 }
718 }
719
720 /// <summary>
721 /// Find an appropriate folder for the given asset type
722 /// </summary>
723 /// <param name="type"></param>
724 /// <returns>null if no appropriate folder exists</returns>
725 public InventoryFolderImpl FindFolderForType(int type)
726 {
727 if (RootFolder == null)
728 return null;
729
730 return RootFolder.FindFolderForType(type);
731 }
732
733 // Load additional items that other regions have put into the database
734 // The item will be added tot he local cache. Returns true if the item
735 // was found and can be sent to the client
736 //
737 public bool QueryItem(InventoryItemBase item)
738 {
739 if (m_hasReceivedInventory)
740 {
741 InventoryItemBase invItem = RootFolder.FindItem(item.ID);
742
743 if (invItem != null)
744 {
745 // Item is in local cache, just update client
746 //
747 return true;
748 }
749
750 InventoryItemBase itemInfo = null;
751
752 itemInfo = m_InventoryService.GetItem(item);
753
754 if (itemInfo != null)
755 {
756 InventoryFolderImpl folder = RootFolder.FindFolder(itemInfo.Folder);
757 ItemReceive(itemInfo, folder);
758 return true;
759 }
760
761 return false;
762 }
763 else
764 {
765 AddRequest(
766 new InventoryRequest(
767 Delegate.CreateDelegate(typeof(QueryItemDelegate), this, "QueryItem"),
768 new object[] { item.ID }));
769
770 return true;
771 }
772 }
773
774 public bool QueryFolder(InventoryFolderBase folder)
775 {
776 if (m_hasReceivedInventory)
777 {
778 InventoryFolderBase invFolder = RootFolder.FindFolder(folder.ID);
779
780 if (invFolder != null)
781 {
782 // Folder is in local cache, just update client
783 //
784 return true;
785 }
786
787 InventoryFolderBase folderInfo = null;
788
789 folderInfo = m_InventoryService.GetFolder(folder);
790
791 if (folderInfo != null)
792 {
793 InventoryFolderImpl createdFolder = RootFolder.CreateChildFolder(folderInfo.ID, folderInfo.Name, (ushort)folderInfo.Type);
794
795 createdFolder.Version = folderInfo.Version;
796 createdFolder.Owner = folderInfo.Owner;
797 createdFolder.ParentID = folderInfo.ParentID;
798
799 return true;
800 }
801
802 return false;
803 }
804 else
805 {
806 AddRequest(
807 new InventoryRequest(
808 Delegate.CreateDelegate(typeof(QueryFolderDelegate), this, "QueryFolder"),
809 new object[] { folder.ID }));
810
811 return true;
812 }
813 }
814 }
815
816 /// <summary>
817 /// Should be implemented by callers which require a callback when the user's inventory is received
818 /// </summary>
819 public interface IInventoryRequest
820 {
821 /// <summary>
822 /// This is the method executed once we have received the user's inventory by which the request can be fulfilled.
823 /// </summary>
824 void Execute();
825 }
826
827 /// <summary>
828 /// Generic inventory request
829 /// </summary>
830 class InventoryRequest : IInventoryRequest
831 {
832 private Delegate m_delegate;
833 private Object[] m_args;
834
835 internal InventoryRequest(Delegate delegat, Object[] args)
836 {
837 m_delegate = delegat;
838 m_args = args;
839 }
840
841 public void Execute()
842 {
843 if (m_delegate != null)
844 m_delegate.DynamicInvoke(m_args);
845 }
846 }
847}
diff --git a/OpenSim/Framework/Communications/Cache/UserProfileCacheService.cs b/OpenSim/Framework/Communications/Cache/UserProfileCacheService.cs
deleted file mode 100644
index acae4b1..0000000
--- a/OpenSim/Framework/Communications/Cache/UserProfileCacheService.cs
+++ /dev/null
@@ -1,277 +0,0 @@
1/*
2 * Copyright (c) Contributors, http://opensimulator.org/
3 * See CONTRIBUTORS.TXT for a full list of copyright holders.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are met:
7 * * Redistributions of source code must retain the above copyright
8 * notice, this list of conditions and the following disclaimer.
9 * * Redistributions in binary form must reproduce the above copyright
10 * notice, this list of conditions and the following disclaimer in the
11 * documentation and/or other materials provided with the distribution.
12 * * Neither the name of the OpenSimulator Project nor the
13 * names of its contributors may be used to endorse or promote products
14 * derived from this software without specific prior written permission.
15 *
16 * THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY
17 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19 * DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY
20 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
21 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
22 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
23 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 */
27
28using System.Collections.Generic;
29using System.Reflection;
30using log4net;
31using OpenMetaverse;
32using OpenSim.Services.Interfaces;
33
34namespace OpenSim.Framework.Communications.Cache
35{
36 /// <summary>
37 /// Holds user profile information and retrieves it from backend services.
38 /// </summary>
39 public class UserProfileCacheService
40 {
41 private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
42
43 /// <value>
44 /// Standard format for names.
45 /// </value>
46 public const string NAME_FORMAT = "{0} {1}";
47
48 /// <summary>
49 /// The comms manager holds references to services (user, grid, inventory, etc.)
50 /// </summary>
51 private readonly CommunicationsManager m_commsManager;
52
53 /// <summary>
54 /// User profiles indexed by UUID
55 /// </summary>
56 private readonly Dictionary<UUID, CachedUserInfo> m_userProfilesById
57 = new Dictionary<UUID, CachedUserInfo>();
58
59 /// <summary>
60 /// User profiles indexed by name
61 /// </summary>
62 private readonly Dictionary<string, CachedUserInfo> m_userProfilesByName
63 = new Dictionary<string, CachedUserInfo>();
64
65 /// <summary>
66 /// The root library folder.
67 /// </summary>
68 public readonly InventoryFolderImpl LibraryRoot;
69
70 private IInventoryService m_InventoryService;
71
72 /// <summary>
73 /// Constructor
74 /// </summary>
75 /// <param name="commsManager"></param>
76 /// <param name="libraryRootFolder"></param>
77 public UserProfileCacheService(CommunicationsManager commsManager, LibraryRootFolder libraryRootFolder)
78 {
79 m_commsManager = commsManager;
80 LibraryRoot = libraryRootFolder;
81 }
82
83 public void SetInventoryService(IInventoryService invService)
84 {
85 m_InventoryService = invService;
86 }
87
88 /// <summary>
89 /// A new user has moved into a region in this instance so retrieve their profile from the user service.
90 /// </summary>
91 ///
92 /// It isn't strictly necessary to make this call since user data can be lazily requested later on. However,
93 /// it might be helpful in order to avoid an initial response delay later on
94 ///
95 /// <param name="userID"></param>
96 public void AddNewUser(UUID userID)
97 {
98 if (userID == UUID.Zero)
99 return;
100
101 //m_log.DebugFormat("[USER CACHE]: Adding user profile for {0}", userID);
102 GetUserDetails(userID);
103 }
104
105 /// <summary>
106 /// Remove this user's profile cache.
107 /// </summary>
108 /// <param name="userID"></param>
109 /// <returns>true if the user was successfully removed, false otherwise</returns>
110 public bool RemoveUser(UUID userId)
111 {
112 if (!RemoveFromCaches(userId))
113 {
114 m_log.WarnFormat(
115 "[USER CACHE]: Tried to remove the profile of user {0}, but this was not in the scene", userId);
116
117 return false;
118 }
119
120 return true;
121 }
122
123 /// <summary>
124 /// Get details of the given user.
125 /// </summary>
126 /// If the user isn't in cache then the user is requested from the profile service.
127 /// <param name="userID"></param>
128 /// <returns>null if no user details are found</returns>
129 public CachedUserInfo GetUserDetails(string fname, string lname)
130 {
131 lock (m_userProfilesByName)
132 {
133 CachedUserInfo userInfo;
134
135 if (m_userProfilesByName.TryGetValue(string.Format(NAME_FORMAT, fname, lname), out userInfo))
136 {
137 return userInfo;
138 }
139 else
140 {
141 UserProfileData userProfile = m_commsManager.UserService.GetUserProfile(fname, lname);
142
143 if (userProfile != null)
144 {
145
146 if ((userProfile.UserAssetURI == null || userProfile.UserAssetURI == "") && m_commsManager.NetworkServersInfo != null)
147 userProfile.UserAssetURI = m_commsManager.NetworkServersInfo.AssetURL;
148 if ((userProfile.UserInventoryURI == null || userProfile.UserInventoryURI == "") && m_commsManager.NetworkServersInfo != null)
149 userProfile.UserInventoryURI = m_commsManager.NetworkServersInfo.InventoryURL;
150
151 return AddToCaches(userProfile);
152 }
153 else
154 return null;
155 }
156 }
157 }
158
159 /// <summary>
160 /// Get details of the given user.
161 /// </summary>
162 /// If the user isn't in cache then the user is requested from the profile service.
163 /// <param name="userID"></param>
164 /// <returns>null if no user details are found</returns>
165 public CachedUserInfo GetUserDetails(UUID userID)
166 {
167 if (userID == UUID.Zero)
168 return null;
169
170 lock (m_userProfilesById)
171 {
172 if (m_userProfilesById.ContainsKey(userID))
173 {
174 return m_userProfilesById[userID];
175 }
176 else
177 {
178 UserProfileData userProfile = m_commsManager.UserService.GetUserProfile(userID);
179 if (userProfile != null)
180 {
181
182 if ((userProfile.UserAssetURI == null || userProfile.UserAssetURI == "") && m_commsManager.NetworkServersInfo != null)
183 userProfile.UserAssetURI = m_commsManager.NetworkServersInfo.AssetURL;
184 if ((userProfile.UserInventoryURI == null || userProfile.UserInventoryURI == "") && m_commsManager.NetworkServersInfo != null)
185 userProfile.UserInventoryURI = m_commsManager.NetworkServersInfo.InventoryURL;
186
187 return AddToCaches(userProfile);
188 }
189 else
190 return null;
191 }
192 }
193 }
194
195 /// <summary>
196 /// Update an existing profile
197 /// </summary>
198 /// <param name="userProfile"></param>
199 /// <returns>true if a user profile was found to update, false otherwise</returns>
200 // Commented out for now. The implementation needs to be improved by protecting against race conditions,
201 // probably by making sure that the update doesn't use the UserCacheInfo.UserProfile directly (possibly via
202 // returning a read only class from the cache).
203// public bool StoreProfile(UserProfileData userProfile)
204// {
205// lock (m_userProfilesById)
206// {
207// CachedUserInfo userInfo = GetUserDetails(userProfile.ID);
208//
209// if (userInfo != null)
210// {
211// userInfo.m_userProfile = userProfile;
212// m_commsManager.UserService.UpdateUserProfile(userProfile);
213//
214// return true;
215// }
216// }
217//
218// return false;
219// }
220
221 /// <summary>
222 /// Populate caches with the given user profile
223 /// </summary>
224 /// <param name="userProfile"></param>
225 protected CachedUserInfo AddToCaches(UserProfileData userProfile)
226 {
227 CachedUserInfo createdUserInfo = new CachedUserInfo(m_InventoryService, userProfile);
228
229 lock (m_userProfilesById)
230 {
231 m_userProfilesById[createdUserInfo.UserProfile.ID] = createdUserInfo;
232
233 lock (m_userProfilesByName)
234 {
235 m_userProfilesByName[createdUserInfo.UserProfile.Name] = createdUserInfo;
236 }
237 }
238
239 return createdUserInfo;
240 }
241
242 /// <summary>
243 /// Remove profile belong to the given uuid from the caches
244 /// </summary>
245 /// <param name="userUuid"></param>
246 /// <returns>true if there was a profile to remove, false otherwise</returns>
247 protected bool RemoveFromCaches(UUID userId)
248 {
249 lock (m_userProfilesById)
250 {
251 if (m_userProfilesById.ContainsKey(userId))
252 {
253 CachedUserInfo userInfo = m_userProfilesById[userId];
254 m_userProfilesById.Remove(userId);
255
256 lock (m_userProfilesByName)
257 {
258 m_userProfilesByName.Remove(userInfo.UserProfile.Name);
259 }
260
261 return true;
262 }
263 }
264
265 return false;
266 }
267
268 /// <summary>
269 /// Preloads User data into the region cache. Modules may use this service to add non-standard clients
270 /// </summary>
271 /// <param name="userData"></param>
272 public void PreloadUserCache(UserProfileData userData)
273 {
274 AddToCaches(userData);
275 }
276 }
277}
diff --git a/OpenSim/Framework/Communications/Cache/LibraryRootFolder.cs b/OpenSim/Services/InventoryService/LibraryService.cs
index 74ba0a5..383f311 100644
--- a/OpenSim/Framework/Communications/Cache/LibraryRootFolder.cs
+++ b/OpenSim/Services/InventoryService/LibraryService.cs
@@ -30,20 +30,32 @@ using System.Collections.Generic;
30using System.IO; 30using System.IO;
31using System.Reflection; 31using System.Reflection;
32using System.Xml; 32using System.Xml;
33
34using OpenSim.Framework;
35using OpenSim.Services.Base;
36using OpenSim.Services.Interfaces;
37
33using log4net; 38using log4net;
34using Nini.Config; 39using Nini.Config;
35using OpenMetaverse; 40using OpenMetaverse;
36 41
37namespace OpenSim.Framework.Communications.Cache 42namespace OpenSim.Services.InventoryService
38{ 43{
39 /// <summary> 44 /// <summary>
40 /// Basically a hack to give us a Inventory library while we don't have a inventory server 45 /// Basically a hack to give us a Inventory library while we don't have a inventory server
41 /// once the server is fully implemented then should read the data from that 46 /// once the server is fully implemented then should read the data from that
42 /// </summary> 47 /// </summary>
43 public class LibraryRootFolder : InventoryFolderImpl 48 public class LibraryService : ServiceBase, ILibraryService
44 { 49 {
45 private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); 50 private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
46 51
52 private InventoryFolderImpl m_LibraryRootFolder;
53
54 public InventoryFolderImpl LibraryRootFolder
55 {
56 get { return m_LibraryRootFolder; }
57 }
58
47 private UUID libOwner = new UUID("11111111-1111-0000-0000-000100bba000"); 59 private UUID libOwner = new UUID("11111111-1111-0000-0000-000100bba000");
48 60
49 /// <summary> 61 /// <summary>
@@ -52,17 +64,31 @@ namespace OpenSim.Framework.Communications.Cache
52 /// </summary> 64 /// </summary>
53 protected Dictionary<UUID, InventoryFolderImpl> libraryFolders 65 protected Dictionary<UUID, InventoryFolderImpl> libraryFolders
54 = new Dictionary<UUID, InventoryFolderImpl>(); 66 = new Dictionary<UUID, InventoryFolderImpl>();
55 67
56 public LibraryRootFolder(string pLibrariesLocation) 68 public LibraryService(IConfigSource config)
69 : base(config)
57 { 70 {
58 Owner = libOwner; 71 string pLibrariesLocation = Path.Combine("inventory", "Libraries.xml");
59 ID = new UUID("00000112-000f-0000-0000-000100bba000"); 72 string pLibName = "OpenSim Library";
60 Name = "OpenSim Library"; 73
61 ParentID = UUID.Zero; 74 IConfig libConfig = config.Configs["LibraryService"];
62 Type = (short) 8; 75 if (libConfig != null)
63 Version = (ushort) 1; 76 {
77 pLibrariesLocation = libConfig.GetString("DefaultLibrary", pLibrariesLocation);
78 pLibName = libConfig.GetString("LibraryName", pLibName);
79 }
80
81 m_log.Debug("[LIBRARY]: Starting library service...");
82
83 m_LibraryRootFolder = new InventoryFolderImpl();
84 m_LibraryRootFolder.Owner = libOwner;
85 m_LibraryRootFolder.ID = new UUID("00000112-000f-0000-0000-000100bba000");
86 m_LibraryRootFolder.Name = pLibName;
87 m_LibraryRootFolder.ParentID = UUID.Zero;
88 m_LibraryRootFolder.Type = (short)8;
89 m_LibraryRootFolder.Version = (ushort)1;
64 90
65 libraryFolders.Add(ID, this); 91 libraryFolders.Add(m_LibraryRootFolder.ID, m_LibraryRootFolder);
66 92
67 LoadLibraries(pLibrariesLocation); 93 LoadLibraries(pLibrariesLocation);
68 } 94 }
@@ -126,9 +152,9 @@ namespace OpenSim.Framework.Communications.Cache
126 { 152 {
127 InventoryFolderImpl folderInfo = new InventoryFolderImpl(); 153 InventoryFolderImpl folderInfo = new InventoryFolderImpl();
128 154
129 folderInfo.ID = new UUID(config.GetString("folderID", ID.ToString())); 155 folderInfo.ID = new UUID(config.GetString("folderID", m_LibraryRootFolder.ID.ToString()));
130 folderInfo.Name = config.GetString("name", "unknown"); 156 folderInfo.Name = config.GetString("name", "unknown");
131 folderInfo.ParentID = new UUID(config.GetString("parentFolderID", ID.ToString())); 157 folderInfo.ParentID = new UUID(config.GetString("parentFolderID", m_LibraryRootFolder.ID.ToString()));
132 folderInfo.Type = (short)config.GetInt("type", 8); 158 folderInfo.Type = (short)config.GetInt("type", 8);
133 159
134 folderInfo.Owner = libOwner; 160 folderInfo.Owner = libOwner;
@@ -160,9 +186,9 @@ namespace OpenSim.Framework.Communications.Cache
160 InventoryItemBase item = new InventoryItemBase(); 186 InventoryItemBase item = new InventoryItemBase();
161 item.Owner = libOwner; 187 item.Owner = libOwner;
162 item.CreatorId = libOwner.ToString(); 188 item.CreatorId = libOwner.ToString();
163 item.ID = new UUID(config.GetString("inventoryID", ID.ToString())); 189 item.ID = new UUID(config.GetString("inventoryID", m_LibraryRootFolder.ID.ToString()));
164 item.AssetID = new UUID(config.GetString("assetID", item.ID.ToString())); 190 item.AssetID = new UUID(config.GetString("assetID", item.ID.ToString()));
165 item.Folder = new UUID(config.GetString("folderID", ID.ToString())); 191 item.Folder = new UUID(config.GetString("folderID", m_LibraryRootFolder.ID.ToString()));
166 item.Name = config.GetString("name", String.Empty); 192 item.Name = config.GetString("name", String.Empty);
167 item.Description = config.GetString("description", item.Name); 193 item.Description = config.GetString("description", item.Name);
168 item.InvType = config.GetInt("inventoryType", 0); 194 item.InvType = config.GetInt("inventoryType", 0);
@@ -230,11 +256,11 @@ namespace OpenSim.Framework.Communications.Cache
230 /// methods in the superclass 256 /// methods in the superclass
231 /// </summary> 257 /// </summary>
232 /// <returns></returns> 258 /// <returns></returns>
233 public Dictionary<UUID, InventoryFolderImpl> RequestSelfAndDescendentFolders() 259 public Dictionary<UUID, InventoryFolderImpl> GetAllFolders()
234 { 260 {
235 Dictionary<UUID, InventoryFolderImpl> fs = new Dictionary<UUID, InventoryFolderImpl>(); 261 Dictionary<UUID, InventoryFolderImpl> fs = new Dictionary<UUID, InventoryFolderImpl>();
236 fs.Add(ID, this); 262 fs.Add(m_LibraryRootFolder.ID, m_LibraryRootFolder);
237 List<InventoryFolderImpl> fis = TraverseFolder(this); 263 List<InventoryFolderImpl> fis = TraverseFolder(m_LibraryRootFolder);
238 foreach (InventoryFolderImpl f in fis) 264 foreach (InventoryFolderImpl f in fis)
239 { 265 {
240 fs.Add(f.ID, f); 266 fs.Add(f.ID, f);