diff options
author | Dr Scofield | 2008-07-02 09:02:30 +0000 |
---|---|---|
committer | Dr Scofield | 2008-07-02 09:02:30 +0000 |
commit | d40bea4a8e09be1f8e87cf41405aaa60fa8826cb (patch) | |
tree | 91ef9356d8b284ac6fa5f0d588fedebe723b69ad /OpenSim/ApplicationPlugins/Rest/Inventory/RestHandler.cs | |
parent | Mantis#1643. Thank you Melanie for a patch that: (diff) | |
download | opensim-SC-d40bea4a8e09be1f8e87cf41405aaa60fa8826cb.zip opensim-SC-d40bea4a8e09be1f8e87cf41405aaa60fa8826cb.tar.gz opensim-SC-d40bea4a8e09be1f8e87cf41405aaa60fa8826cb.tar.bz2 opensim-SC-d40bea4a8e09be1f8e87cf41405aaa60fa8826cb.tar.xz |
From: Alan M Webb <awebb@vnet.ibm.com>
This adds REST services for inventory access. It also allows inventory
uploads.
Diffstat (limited to 'OpenSim/ApplicationPlugins/Rest/Inventory/RestHandler.cs')
-rw-r--r-- | OpenSim/ApplicationPlugins/Rest/Inventory/RestHandler.cs | 547 |
1 files changed, 547 insertions, 0 deletions
diff --git a/OpenSim/ApplicationPlugins/Rest/Inventory/RestHandler.cs b/OpenSim/ApplicationPlugins/Rest/Inventory/RestHandler.cs new file mode 100644 index 0000000..0a0bf3f --- /dev/null +++ b/OpenSim/ApplicationPlugins/Rest/Inventory/RestHandler.cs | |||
@@ -0,0 +1,547 @@ | |||
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 OpenSim 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 | */ | ||
28 | |||
29 | using System; | ||
30 | using System.Collections.Generic; | ||
31 | using System.Reflection; | ||
32 | using OpenSim.Framework; | ||
33 | using OpenSim.Framework.Servers; | ||
34 | using OpenSim.ApplicationPlugins.Rest; | ||
35 | using Mono.Addins; | ||
36 | |||
37 | [assembly : Addin] | ||
38 | [assembly : AddinDependency("OpenSim", "0.5")] | ||
39 | |||
40 | namespace OpenSim.ApplicationPlugins.Rest.Inventory | ||
41 | { | ||
42 | |||
43 | [Extension("/OpenSim/Startup")] | ||
44 | |||
45 | public class RestHandler : RestPlugin, IHttpAgentHandler | ||
46 | { | ||
47 | |||
48 | #region local static state | ||
49 | |||
50 | /// <summary> | ||
51 | /// This static initializer scans the assembly for classes that | ||
52 | /// export the IRest interface and builds a list of them. These | ||
53 | /// are later activated by the handler. To add a new handler it | ||
54 | /// is only necessary to create a new services class that implements | ||
55 | /// the IRest interface, and recompile the handler. This gives | ||
56 | /// all of the build-time flexibility of a modular approach | ||
57 | /// while not introducing yet-another module loader. Note that | ||
58 | /// multiple assembles can still be built, each with its own set | ||
59 | /// of handlers. | ||
60 | /// </summary> | ||
61 | |||
62 | private static bool handlersLoaded = false; | ||
63 | private static List<Type> classes = new List<Type>(); | ||
64 | private static List<IRest> handlers = new List<IRest>(); | ||
65 | private static Type[] parms = new Type[1]; | ||
66 | private static Object[] args = new Object[1]; | ||
67 | |||
68 | static RestHandler() | ||
69 | { | ||
70 | Module[] mods = Assembly.GetExecutingAssembly().GetModules(); | ||
71 | foreach (Module m in mods) | ||
72 | { | ||
73 | Type[] types = m.GetTypes(); | ||
74 | foreach (Type t in types) | ||
75 | { | ||
76 | if (t.GetInterface("IRest") != null) | ||
77 | { | ||
78 | classes.Add(t); | ||
79 | } | ||
80 | } | ||
81 | } | ||
82 | } | ||
83 | |||
84 | #endregion local static state | ||
85 | |||
86 | #region local instance state | ||
87 | |||
88 | /// <remarks> | ||
89 | /// The handler delegate is not noteworthy. The allocator allows | ||
90 | /// a given handler to optionally subclass the base RequestData | ||
91 | /// structure to carry any locally required per-request state | ||
92 | /// needed. | ||
93 | /// </remarks> | ||
94 | internal delegate void RestMethodHandler(RequestData rdata); | ||
95 | internal delegate RequestData RestMethodAllocator(OSHttpRequest request, OSHttpResponse response); | ||
96 | |||
97 | // Handler tables: both stream and REST are supported | ||
98 | |||
99 | internal Dictionary<string,RestMethodHandler> pathHandlers = new Dictionary<string,RestMethodHandler>(); | ||
100 | internal Dictionary<string,RestMethodAllocator> pathAllocators = new Dictionary<string,RestMethodAllocator>(); | ||
101 | internal Dictionary<string,RestStreamHandler> streamHandlers = new Dictionary<string,RestStreamHandler>(); | ||
102 | |||
103 | /// <summary> | ||
104 | /// This routine loads all of the handlers discovered during | ||
105 | /// instance initialization. Each handler is responsible for | ||
106 | /// registering itself with this handler. | ||
107 | /// I was not able to make this code work in a constructor. | ||
108 | /// </summary> | ||
109 | private void LoadHandlers() | ||
110 | { | ||
111 | lock(handlers) | ||
112 | { | ||
113 | if (!handlersLoaded) | ||
114 | { | ||
115 | parms[0] = this.GetType(); | ||
116 | args[0] = this; | ||
117 | |||
118 | ConstructorInfo ci; | ||
119 | Object ht; | ||
120 | |||
121 | foreach (Type t in classes) | ||
122 | { | ||
123 | ci = t.GetConstructor(parms); | ||
124 | ht = ci.Invoke(args); | ||
125 | handlers.Add((IRest)ht); | ||
126 | } | ||
127 | handlersLoaded = true; | ||
128 | } | ||
129 | } | ||
130 | } | ||
131 | |||
132 | #endregion local instance state | ||
133 | |||
134 | #region overriding properties | ||
135 | |||
136 | // Used to differentiate the message header. | ||
137 | |||
138 | public override string Name | ||
139 | { | ||
140 | get { return "HANDLER"; } | ||
141 | } | ||
142 | |||
143 | // Used to partition the configuration space. | ||
144 | |||
145 | public override string ConfigName | ||
146 | { | ||
147 | get { return "RestHandler"; } | ||
148 | } | ||
149 | |||
150 | // We have to rename these because we want | ||
151 | // to be able to share the values with other | ||
152 | // classes in our assembly and the base | ||
153 | // names are protected. | ||
154 | |||
155 | internal string MsgId | ||
156 | { | ||
157 | get { return base.MsgID; } | ||
158 | } | ||
159 | |||
160 | internal string RequestId | ||
161 | { | ||
162 | get { return base.RequestID; } | ||
163 | } | ||
164 | |||
165 | #endregion overriding properties | ||
166 | |||
167 | #region overriding methods | ||
168 | |||
169 | /// <summary> | ||
170 | /// This method is called by OpenSimMain immediately after loading the | ||
171 | /// plugin and after basic server setup, but before running any server commands. | ||
172 | /// </summary> | ||
173 | /// <remarks> | ||
174 | /// Note that entries MUST be added to the active configuration files before | ||
175 | /// the plugin can be enabled. | ||
176 | /// </remarks> | ||
177 | public override void Initialise(OpenSimBase openSim) | ||
178 | { | ||
179 | try | ||
180 | { | ||
181 | |||
182 | /// <remarks> | ||
183 | /// This plugin will only be enabled if the broader | ||
184 | /// REST plugin mechanism is enabled. | ||
185 | /// </remarks> | ||
186 | |||
187 | Rest.Log.InfoFormat("{0} Plugin is initializing", MsgID); | ||
188 | |||
189 | base.Initialise(openSim); | ||
190 | |||
191 | if (!IsEnabled) | ||
192 | { | ||
193 | Rest.Log.WarnFormat("{0} Plugins are disabled", MsgID); | ||
194 | return; | ||
195 | } | ||
196 | |||
197 | Rest.Log.InfoFormat("{0} Plugin will be enabled", MsgID); | ||
198 | |||
199 | /// <remarks> | ||
200 | /// These are stored in static variables to make | ||
201 | /// them easy to reach from anywhere in the assembly. | ||
202 | /// </remarks> | ||
203 | |||
204 | Rest.main = openSim; | ||
205 | Rest.Plugin = this; | ||
206 | Rest.Comms = App.CommunicationsManager; | ||
207 | Rest.UserServices = Rest.Comms.UserService; | ||
208 | Rest.InventoryServices = Rest.Comms.InventoryService; | ||
209 | Rest.AssetServices = Rest.Comms.AssetCache; | ||
210 | Rest.Config = Config; | ||
211 | Rest.Prefix = Prefix; | ||
212 | Rest.GodKey = GodKey; | ||
213 | |||
214 | Rest.Authenticate = Rest.Config.GetBoolean("authenticate",true); | ||
215 | Rest.Secure = Rest.Config.GetBoolean("secured",true); | ||
216 | Rest.ExtendedEscape = Rest.Config.GetBoolean("extended-escape",true); | ||
217 | Rest.Realm = Rest.Config.GetString("realm","OpenSim REST"); | ||
218 | Rest.DumpAsset = Rest.Config.GetBoolean("dump-asset",false); | ||
219 | Rest.DumpLineSize = Rest.Config.GetInt("dump-line-size",32); | ||
220 | |||
221 | Rest.Log.InfoFormat("{0} Authentication is {1}required", MsgId, | ||
222 | (Rest.Authenticate ? "" : "not ")); | ||
223 | |||
224 | Rest.Log.InfoFormat("{0} Security is {1}enabled", MsgId, | ||
225 | (Rest.Authenticate ? "" : "not ")); | ||
226 | |||
227 | Rest.Log.InfoFormat("{0} Extended URI escape processing is {1}enabled", MsgId, | ||
228 | (Rest.ExtendedEscape ? "" : "not ")); | ||
229 | |||
230 | Rest.Log.InfoFormat("{0} Dumping of asset data is {1}enabled", MsgId, | ||
231 | (Rest.DumpAsset ? "" : "not ")); | ||
232 | |||
233 | if (Rest.DumpAsset) | ||
234 | { | ||
235 | Rest.Log.InfoFormat("{0} Dump {1} bytes per line", MsgId, | ||
236 | Rest.DumpLineSize); | ||
237 | } | ||
238 | |||
239 | // Load all of the handlers present in the | ||
240 | // assembly | ||
241 | |||
242 | // In principle, as we're an application plug-in, | ||
243 | // most of what needs to be done could be done using | ||
244 | // static resources, however the Open Sim plug-in | ||
245 | // model makes this an instance, so that's what we | ||
246 | // need to be. | ||
247 | // There is only one Communications manager per | ||
248 | // server, and by inference, only one each of the | ||
249 | // user, asset, and inventory servers. So we can cache | ||
250 | // those using a static initializer. | ||
251 | // We move all of this processing off to another | ||
252 | // services class to minimize overlap between function | ||
253 | // and infrastructure. | ||
254 | |||
255 | LoadHandlers(); | ||
256 | |||
257 | /// <remarks> | ||
258 | /// The intention of a post construction initializer | ||
259 | /// is to allow for setup that is dependent upon other | ||
260 | /// activities outside of the agency. We don't currently | ||
261 | /// have any, but the design allows for it. | ||
262 | /// </remarks> | ||
263 | |||
264 | foreach (IRest handler in handlers) | ||
265 | { | ||
266 | handler.Initialize(); | ||
267 | } | ||
268 | |||
269 | /// <remarks> | ||
270 | /// Now that everything is setup we can proceed and | ||
271 | /// add this agent to the HTTP server's handler list | ||
272 | /// </remarks> | ||
273 | |||
274 | if (!AddAgentHandler(Rest.Name,this)) | ||
275 | { | ||
276 | Rest.Log.ErrorFormat("{0} Unable to activate handler interface", MsgId); | ||
277 | foreach (IRest handler in handlers) | ||
278 | { | ||
279 | handler.Close(); | ||
280 | } | ||
281 | } | ||
282 | |||
283 | } | ||
284 | catch (Exception e) | ||
285 | { | ||
286 | Rest.Log.ErrorFormat("{0} Plugin initialization has failed: {1}", MsgID, e.Message); | ||
287 | } | ||
288 | |||
289 | } | ||
290 | |||
291 | /// <summary> | ||
292 | /// In the interests of efficiency, and because we cannot determine whether | ||
293 | /// or not this instance will actually be harvested, we clobber the only | ||
294 | /// anchoring reference to the working state for this plug-in. What the | ||
295 | /// call to close does is irrelevant to this class beyond knowing that it | ||
296 | /// can nullify the reference when it returns. | ||
297 | /// To make sure everything is copacetic we make sure the primary interface | ||
298 | /// is disabled by deleting the handler from the HTTP server tables. | ||
299 | /// </summary> | ||
300 | public override void Close() | ||
301 | { | ||
302 | |||
303 | Rest.Log.InfoFormat("{0} Plugin is terminating", MsgID); | ||
304 | |||
305 | try | ||
306 | { | ||
307 | RemoveAgentHandler(Rest.Name, this); | ||
308 | } | ||
309 | catch (KeyNotFoundException){} | ||
310 | |||
311 | foreach (IRest handler in handlers) | ||
312 | { | ||
313 | handler.Close(); | ||
314 | } | ||
315 | |||
316 | } | ||
317 | |||
318 | #endregion overriding methods | ||
319 | |||
320 | #region interface methods | ||
321 | |||
322 | /// <summary> | ||
323 | /// This method is called by the server to match the client, it could | ||
324 | /// just return true if we only want one such handler. For now we | ||
325 | /// match any explicitly specified client. | ||
326 | /// </summary> | ||
327 | public bool Match(OSHttpRequest request, OSHttpResponse response) | ||
328 | { | ||
329 | string path = request.RawUrl; | ||
330 | foreach (string key in pathHandlers.Keys) | ||
331 | { | ||
332 | if (path.StartsWith(key)) | ||
333 | { | ||
334 | return ( path.Length == key.Length || | ||
335 | path.Substring(key.Length,1) == Rest.UrlPathSeparator); | ||
336 | } | ||
337 | } | ||
338 | |||
339 | path = String.Format("{0}{1}{2}", request.HttpMethod, Rest.UrlMethodSeparator, path); | ||
340 | foreach (string key in streamHandlers.Keys) | ||
341 | { | ||
342 | if (path.StartsWith(key)) | ||
343 | { | ||
344 | return true; | ||
345 | } | ||
346 | } | ||
347 | |||
348 | return false; | ||
349 | } | ||
350 | |||
351 | /// <summary> | ||
352 | /// Preconditions: | ||
353 | /// [1] request != null and is a valid request object | ||
354 | /// [2] response != null and is a valid response object | ||
355 | /// Behavior is undefined if preconditions are not satisfied. | ||
356 | /// </summary> | ||
357 | public bool Handle(OSHttpRequest request, OSHttpResponse response) | ||
358 | { | ||
359 | bool handled; | ||
360 | base.MsgID = base.RequestID; | ||
361 | |||
362 | if (Rest.DEBUG) | ||
363 | { | ||
364 | Rest.Log.DebugFormat("{0} ENTRY", MsgId); | ||
365 | Rest.Log.DebugFormat("{0} Agent: {1}", MsgId, request.UserAgent); | ||
366 | Rest.Log.DebugFormat("{0} Method: {1}", MsgId, request.HttpMethod); | ||
367 | |||
368 | for (int i = 0; i < request.Headers.Count; i++) | ||
369 | { | ||
370 | Rest.Log.DebugFormat("{0} Header [{1}] : <{2}> = <{3}>", | ||
371 | MsgId, i, request.Headers.GetKey(i), request.Headers.Get(i)); | ||
372 | } | ||
373 | Rest.Log.DebugFormat("{0} URI: {1}", MsgId, request.RawUrl); | ||
374 | } | ||
375 | |||
376 | // If a path handler worked we're done, otherwise try any | ||
377 | // available stream handlers too. | ||
378 | |||
379 | try | ||
380 | { | ||
381 | handled = FindPathHandler(request, response) || | ||
382 | FindStreamHandler(request, response); | ||
383 | } | ||
384 | catch (Exception e) | ||
385 | { | ||
386 | // A raw exception indicates that something we weren't expecting has | ||
387 | // happened. This should always reflect a shortcoming in the plugin, | ||
388 | // or a failure to satisfy the preconditions. | ||
389 | Rest.Log.ErrorFormat("{0} Plugin error: {1}", MsgId, e.Message); | ||
390 | handled = true; | ||
391 | } | ||
392 | |||
393 | Rest.Log.DebugFormat("{0} EXIT", MsgId); | ||
394 | |||
395 | return handled; | ||
396 | |||
397 | } | ||
398 | |||
399 | #endregion interface methods | ||
400 | |||
401 | /// <summary> | ||
402 | /// If there is a stream handler registered that can handle the | ||
403 | /// request, then fine. If the request is not matched, do | ||
404 | /// nothing. | ||
405 | /// </summary> | ||
406 | |||
407 | private bool FindStreamHandler(OSHttpRequest request, OSHttpResponse response) | ||
408 | { | ||
409 | RequestData rdata = new RequestData(request, response, String.Empty); | ||
410 | |||
411 | string bestMatch = null; | ||
412 | string path = String.Format("{0}:{1}", rdata.method, rdata.path); | ||
413 | |||
414 | Rest.Log.DebugFormat("{0} Checking for stream handler for <{1}>", MsgId, path); | ||
415 | |||
416 | foreach (string pattern in streamHandlers.Keys) | ||
417 | { | ||
418 | if (path.StartsWith(pattern)) | ||
419 | { | ||
420 | if (String.IsNullOrEmpty(bestMatch) || pattern.Length > bestMatch.Length) | ||
421 | { | ||
422 | bestMatch = pattern; | ||
423 | } | ||
424 | } | ||
425 | } | ||
426 | |||
427 | // Handle using the best match available | ||
428 | |||
429 | if (!String.IsNullOrEmpty(bestMatch)) | ||
430 | { | ||
431 | Rest.Log.DebugFormat("{0} Stream-based handler matched with <{1}>", MsgId, bestMatch); | ||
432 | RestStreamHandler handler = streamHandlers[bestMatch]; | ||
433 | rdata.buffer = handler.Handle(rdata.path, rdata.request.InputStream, rdata.request, rdata.response); | ||
434 | rdata.AddHeader(rdata.response.ContentType,handler.ContentType); | ||
435 | rdata.Respond("FindStreamHandler Completion"); | ||
436 | } | ||
437 | |||
438 | return rdata.handled; | ||
439 | |||
440 | } | ||
441 | |||
442 | // Preserves the original handler's semantics | ||
443 | |||
444 | public new void AddStreamHandler(string httpMethod, string path, RestMethod method) | ||
445 | { | ||
446 | |||
447 | if (!IsEnabled) | ||
448 | { | ||
449 | return; | ||
450 | } | ||
451 | |||
452 | if (!path.StartsWith(Rest.Prefix)) | ||
453 | { | ||
454 | path = String.Format("{0}{1}", Rest.Prefix, path); | ||
455 | } | ||
456 | |||
457 | path = String.Format("{0}{1}{2}", httpMethod, Rest.UrlMethodSeparator, path); | ||
458 | |||
459 | // Conditionally add to the list | ||
460 | |||
461 | if (!streamHandlers.ContainsKey(path)) | ||
462 | { | ||
463 | streamHandlers.Add(path, new RestStreamHandler(httpMethod, path, method)); | ||
464 | Rest.Log.DebugFormat("{0} Added handler for {1}", MsgID, path); | ||
465 | } | ||
466 | else | ||
467 | { | ||
468 | Rest.Log.WarnFormat("{0} Ignoring duplicate handler for {1}", MsgID, path); | ||
469 | } | ||
470 | |||
471 | } | ||
472 | |||
473 | |||
474 | internal bool FindPathHandler(OSHttpRequest request, OSHttpResponse response) | ||
475 | { | ||
476 | |||
477 | RequestData rdata = null; | ||
478 | string bestMatch = null; | ||
479 | |||
480 | if (!IsEnabled) | ||
481 | { | ||
482 | return false; | ||
483 | } | ||
484 | |||
485 | // Conditionally add to the list | ||
486 | |||
487 | Rest.Log.DebugFormat("{0} Checking for path handler for <{1}>", MsgId, request.RawUrl); | ||
488 | |||
489 | foreach (string pattern in pathHandlers.Keys) | ||
490 | { | ||
491 | if (request.RawUrl.StartsWith(pattern)) | ||
492 | { | ||
493 | if (String.IsNullOrEmpty(bestMatch) || pattern.Length > bestMatch.Length) | ||
494 | { | ||
495 | bestMatch = pattern; | ||
496 | } | ||
497 | } | ||
498 | } | ||
499 | |||
500 | if (!String.IsNullOrEmpty(bestMatch)) | ||
501 | { | ||
502 | |||
503 | rdata = pathAllocators[bestMatch](request, response); | ||
504 | |||
505 | Rest.Log.DebugFormat("{0} Path based REST handler matched with <{1}>", MsgId, bestMatch); | ||
506 | |||
507 | try | ||
508 | { | ||
509 | pathHandlers[bestMatch](rdata); | ||
510 | } | ||
511 | |||
512 | // A plugin generated error indicates a request-related error | ||
513 | // that has been handled by the plugin. | ||
514 | |||
515 | catch (RestException r) | ||
516 | { | ||
517 | Rest.Log.WarnFormat("{0} Request failed: {1}", MsgId, r.Message); | ||
518 | } | ||
519 | |||
520 | } | ||
521 | |||
522 | return (rdata == null) ? false : rdata.handled; | ||
523 | |||
524 | } | ||
525 | |||
526 | internal void AddPathHandler(RestMethodHandler mh, string path, RestMethodAllocator ra) | ||
527 | { | ||
528 | if (pathHandlers.ContainsKey(path)) | ||
529 | { | ||
530 | Rest.Log.DebugFormat("{0} Replacing handler for <${1}>", MsgId, path); | ||
531 | pathHandlers.Remove(path); | ||
532 | } | ||
533 | |||
534 | if (pathAllocators.ContainsKey(path)) | ||
535 | { | ||
536 | Rest.Log.DebugFormat("{0} Replacing allocator for <${1}>", MsgId, path); | ||
537 | pathAllocators.Remove(path); | ||
538 | } | ||
539 | |||
540 | Rest.Log.DebugFormat("{0} Adding path handler for {1}", MsgId, path); | ||
541 | |||
542 | pathHandlers.Add(path, mh); | ||
543 | pathAllocators.Add(path, ra); | ||
544 | |||
545 | } | ||
546 | } | ||
547 | } | ||