]> err.no Git - scalable-opengroupware.org/blob - SoObjects/SOGo/SOGoFolder.m
db7256ef73effa32cc270763eb47f8c5c748306f
[scalable-opengroupware.org] / SoObjects / SOGo / SOGoFolder.m
1 /*
2   Copyright (C) 2004-2005 SKYRIX Software AG
3
4   This file is part of OpenGroupware.org.
5
6   OGo is free software; you can redistribute it and/or modify it under
7   the terms of the GNU Lesser General Public License as published by the
8   Free Software Foundation; either version 2, or (at your option) any
9   later version.
10
11   OGo is distributed in the hope that it will be useful, but WITHOUT ANY
12   WARRANTY; without even the implied warranty of MERCHANTABILITY or
13   FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
14   License for more details.
15
16   You should have received a copy of the GNU Lesser General Public
17   License along with OGo; see the file COPYING.  If not, write to the
18   Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
19   02111-1307, USA.
20 */
21
22 #import <Foundation/NSArray.h>
23 #import <Foundation/NSDate.h>
24 #import <Foundation/NSDictionary.h>
25 #import <Foundation/NSException.h>
26 #import <Foundation/NSKeyValueCoding.h>
27 #import <Foundation/NSURL.h>
28
29 #import <NGObjWeb/NSException+HTTP.h>
30 #import <NGObjWeb/SoObject.h>
31 #import <NGObjWeb/SoObject+SoDAV.h>
32 #import <NGObjWeb/SoSelectorInvocation.h>
33 #import <NGObjWeb/WOContext+SoObjects.h>
34 #import <NGExtensions/NSNull+misc.h>
35 #import <NGExtensions/NSObject+Logs.h>
36 #import <EOControl/EOQualifier.h>
37 #import <GDLAccess/EOAdaptorChannel.h>
38 #import <GDLContentStore/GCSChannelManager.h>
39 #import <GDLContentStore/GCSFolderManager.h>
40 #import <GDLContentStore/GCSFolder.h>
41 #import <GDLContentStore/GCSFolderType.h>
42 #import <GDLContentStore/NSURL+GCS.h>
43 #import <SaxObjC/XMLNamespaces.h>
44
45 #import "NSArray+Utilities.h"
46 #import "NSString+Utilities.h"
47
48 #import "SOGoContentObject.h"
49 #import "SOGoPermissions.h"
50 #import "SOGoUser.h"
51
52 #import "SOGoFolder.h"
53
54 static NSString *defaultUserID = @"<default>";
55
56 @implementation SOGoFolder
57
58 + (int) version
59 {
60   return [super version] + 0 /* v0 */;
61 }
62
63 + (void) initialize
64 {
65   NSAssert2([super version] == 0,
66             @"invalid superclass (%@) version %i !",
67             NSStringFromClass([self superclass]), [super version]);
68 }
69
70 + (id) folderWithSubscriptionReference: (NSString *) reference
71                            inContainer: (id) aContainer
72 {
73   id newFolder;
74   NSArray *elements, *pathElements;
75   NSString *ocsPath, *objectPath, *owner, *ocsName, *folderName;
76
77   elements = [reference componentsSeparatedByString: @":"];
78   owner = [elements objectAtIndex: 0];
79   objectPath = [elements objectAtIndex: 1];
80   pathElements = [objectPath componentsSeparatedByString: @"/"];
81   if ([pathElements count] > 1)
82     ocsName = [pathElements objectAtIndex: 1];
83   else
84     ocsName = @"personal";
85
86   ocsPath = [NSString stringWithFormat: @"/Users/%@/%@/%@",
87                       owner, [pathElements objectAtIndex: 0], ocsName];
88   folderName = [NSString stringWithFormat: @"%@_%@", owner, ocsName];
89   newFolder = [[self alloc] initWithName: folderName
90                             inContainer: aContainer];
91   [newFolder setOCSPath: ocsPath];
92   [newFolder setOwner: owner];
93
94   return newFolder;
95 }
96
97 - (id) init
98 {
99   if ((self = [super init]))
100     {
101       displayName = nil;
102       ocsPath = nil;
103       ocsFolder = nil;
104       aclCache = [NSMutableDictionary new];
105     }
106
107   return self;
108 }
109
110 - (void) dealloc
111 {
112   [ocsFolder release];
113   [ocsPath release];
114   [aclCache release];
115   [displayName release];
116   [super dealloc];
117 }
118
119 /* accessors */
120
121 - (BOOL) isFolderish
122 {
123   return YES;
124 }
125
126 - (void) setOCSPath: (NSString *) _path
127 {
128   if (![ocsPath isEqualToString:_path])
129     {
130       if (ocsPath)
131         [self warnWithFormat: @"GCS path is already set! '%@'", _path];
132       ASSIGN (ocsPath, _path);
133     }
134 }
135
136 - (NSString *) ocsPath
137 {
138   return ocsPath;
139 }
140
141 - (GCSFolderManager *) folderManager
142 {
143   static GCSFolderManager *folderManager = nil;
144
145   if (!folderManager)
146     folderManager = [GCSFolderManager defaultFolderManager];
147
148   return folderManager;
149 }
150
151 - (GCSFolder *) ocsFolderForPath: (NSString *) _path
152 {
153   return [[self folderManager] folderAtPath: _path];
154 }
155
156 - (BOOL) folderIsMandatory
157 {
158   return [nameInContainer isEqualToString: @"personal"];
159 }
160
161 - (void) _setDisplayNameFromRow: (NSDictionary *) row
162 {
163   NSString *currentLogin, *ownerLogin;
164   NSDictionary *ownerIdentity;
165
166   displayName
167     = [NSMutableString stringWithString: [row objectForKey: @"c_foldername"]];
168   currentLogin = [[context activeUser] login];
169   ownerLogin = [self ownerInContext: context];
170   if (![currentLogin isEqualToString: ownerLogin])
171     {
172       ownerIdentity = [[SOGoUser userWithLogin: ownerLogin roles: nil]
173                         primaryIdentity];
174       [displayName appendFormat: @" (%@ <%@>)",
175                    [ownerIdentity objectForKey: @"fullName"],
176                    [ownerIdentity objectForKey: @"email"]];
177     }
178   [displayName retain];
179 }
180
181 - (void) _fetchDisplayName
182 {
183   GCSChannelManager *cm;
184   EOAdaptorChannel *fc;
185   NSURL *folderLocation;
186   NSString *sql;
187   NSArray *attrs;
188   NSDictionary *row;
189
190   cm = [GCSChannelManager defaultChannelManager];
191   folderLocation
192     = [[GCSFolderManager defaultFolderManager] folderInfoLocation];
193   fc = [cm acquireOpenChannelForURL: folderLocation];
194   if (fc)
195     {
196       sql
197         = [NSString stringWithFormat: (@"SELECT c_foldername FROM %@"
198                                        @" WHERE c_path = '%@'"),
199                     [folderLocation gcsTableName], ocsPath];
200       [fc evaluateExpressionX: sql];
201       attrs = [fc describeResults: NO];
202       row = [fc fetchAttributes: attrs withZone: NULL];
203       if (row)
204         [self _setDisplayNameFromRow: row];
205       [fc cancelFetch];
206       [cm releaseChannel: fc];
207     }
208 }
209
210 - (void) setDisplayName: (NSString *) newDisplayName
211 {
212   ASSIGN (displayName, newDisplayName);
213 }
214
215 - (NSString *) displayName
216 {
217   if (!displayName)
218     [self _fetchDisplayName];
219
220   return displayName;
221 }
222
223 - (NSString *) davDisplayName
224 {
225   return [self displayName];
226 }
227
228 - (GCSFolder *) ocsFolder
229 {
230   GCSFolder *folder;
231   NSString *userLogin;
232
233   if (!ocsFolder)
234     {
235       ocsFolder = [self ocsFolderForPath: [self ocsPath]];
236       userLogin = [[context activeUser] login];
237       if (!ocsFolder
238           && [userLogin isEqualToString: [self ownerInContext: context]]
239           && [self folderIsMandatory]
240           && [self create])
241         ocsFolder = [self ocsFolderForPath: [self ocsPath]];
242       [ocsFolder retain];
243     }
244
245   if ([ocsFolder isNotNull])
246     folder = ocsFolder;
247   else
248     folder = nil;
249
250   return folder;
251 }
252
253 - (NSString *) folderType
254 {
255   return @"";
256 }
257
258 - (BOOL) create
259 {
260   NSException *result;
261
262 //   [self dieHard];
263   result = [[self folderManager] createFolderOfType: [self folderType]
264                                  withName: displayName
265                                  atPath: ocsPath];
266
267   return (result == nil);
268 }
269
270 - (NSException *) delete
271 {
272   NSException *error;
273
274   if ([nameInContainer isEqualToString: @"personal"])
275     error = [NSException exceptionWithHTTPStatus: 403
276                          reason: @"folder 'personal' cannot be deleted"];
277   else
278     error = [[self folderManager] deleteFolderAtPath: ocsPath];
279
280   return error;
281 }
282
283 - (void) renameTo: (NSString *) newName
284 {
285   GCSChannelManager *cm;
286   EOAdaptorChannel *fc;
287   NSURL *folderLocation;
288   NSString *sql;
289
290   [displayName release];
291   displayName = nil;
292
293   cm = [GCSChannelManager defaultChannelManager];
294   folderLocation
295     = [[GCSFolderManager defaultFolderManager] folderInfoLocation];
296   fc = [cm acquireOpenChannelForURL: folderLocation];
297   if (fc)
298     {
299       sql
300         = [NSString stringWithFormat: (@"UPDATE %@ SET c_foldername = '%@'"
301                                        @" WHERE c_path = '%@'"),
302                     [folderLocation gcsTableName], newName, ocsPath];
303       [fc evaluateExpressionX: sql];
304       [cm releaseChannel: fc];
305 //       sql = [sql stringByAppendingFormat:@" WHERE %@ = '%@'", 
306 //                  uidColumnName, [self uid]];
307     }
308 }
309
310 - (NSArray *) fetchContentObjectNames
311 {
312   NSArray *fields, *records;
313   
314   fields = [NSArray arrayWithObject: @"c_name"];
315   records = [[self ocsFolder] fetchFields:fields matchingQualifier:nil];
316   if (![records isNotNull])
317     {
318       [self errorWithFormat: @"(%s): fetch failed!", __PRETTY_FUNCTION__];
319       return nil;
320     }
321   if ([records isKindOfClass: [NSException class]])
322     return records;
323   return [records objectsForKey: @"c_name"];
324 }
325
326 - (BOOL) nameExistsInFolder: (NSString *) objectName
327 {
328   NSArray *fields, *records;
329   EOQualifier *qualifier;
330
331   qualifier
332     = [EOQualifier qualifierWithQualifierFormat:
333                      [NSString stringWithFormat: @"c_name='%@'", objectName]];
334
335   fields = [NSArray arrayWithObject: @"c_name"];
336   records = [[self ocsFolder] fetchFields: fields
337                               matchingQualifier: qualifier];
338   return (records
339           && ![records isKindOfClass:[NSException class]]
340           && [records count] > 0);
341 }
342
343 - (void) deleteEntriesWithIds: (NSArray *) ids
344 {
345   unsigned int count, max;
346   NSString *currentID;
347   SOGoContentObject *deleteObject;
348
349   max = [ids count];
350   for (count = 0; count < max; count++)
351     {
352       currentID = [ids objectAtIndex: count];
353       deleteObject = [self lookupName: currentID
354                            inContext: context acquire: NO];
355       [deleteObject delete];
356     }
357 }
358
359 - (NSDictionary *) fetchContentStringsAndNamesOfAllObjects
360 {
361   NSDictionary *files;
362   
363   files = [[self ocsFolder] fetchContentsOfAllFiles];
364   if (![files isNotNull])
365     {
366       [self errorWithFormat:@"(%s): fetch failed!", __PRETTY_FUNCTION__];
367       return nil;
368     }
369   if ([files isKindOfClass:[NSException class]])
370     return files;
371   return files;
372 }
373
374 /* reflection */
375
376 - (NSString *) defaultFilenameExtension
377 {
378   /* 
379      Override to add an extension to a filename
380      
381      Note: be careful with that, needs to be consistent with object lookup!
382   */
383   return nil;
384 }
385
386 - (NSArray *) davResourceType
387 {
388   NSArray *rType, *groupDavCollection;
389
390   if ([self respondsToSelector: @selector (groupDavResourceType)])
391     {
392       groupDavCollection
393         = [NSArray arrayWithObjects: [self groupDavResourceType],
394                    XMLNS_GROUPDAV, nil];
395       rType = [NSArray arrayWithObjects: @"collection", groupDavCollection,
396                        nil];
397     }
398   else
399     rType = [NSArray arrayWithObject: @"collection"];
400
401   return rType;
402 }
403
404 - (NSString *) davContentType
405 {
406   return @"httpd/unix-directory";
407 }
408
409 - (NSArray *) toOneRelationshipKeys
410 {
411   /* toOneRelationshipKeys are the 'files' contained in a folder */
412   NSMutableArray *ma;
413   NSArray  *names;
414   NSString *name, *ext;
415   unsigned i, count;
416   NSRange  r;
417
418   names = [self fetchContentObjectNames];
419   count = [names count];
420   ext = [self defaultFilenameExtension];
421   if (count && [ext length] > 0)
422     {
423       ma = [NSMutableArray arrayWithCapacity: count];
424       for (i = 0; i < count; i++)
425         {
426           name = [names objectAtIndex: i];
427           r = [name rangeOfString: @"."];
428           if (r.length == 0)
429             name = [NSMutableString stringWithFormat: @"%@.%@", name, ext];
430           [ma addObject: name];
431         }
432
433       names = ma;
434     }
435
436   return names;
437 }
438
439 /* acls as a container */
440
441 - (NSArray *) aclUsersForObjectAtPath: (NSArray *) objectPathArray;
442 {
443   EOQualifier *qualifier;
444   NSString *qs;
445   NSArray *records;
446
447   qs = [NSString stringWithFormat: @"c_object = '/%@'",
448                  [objectPathArray componentsJoinedByString: @"/"]];
449   qualifier = [EOQualifier qualifierWithQualifierFormat: qs];
450   records = [[self ocsFolder] fetchAclMatchingQualifier: qualifier];
451
452   return [records valueForKey: @"c_uid"];
453 }
454
455 - (NSArray *) _fetchAclsForUser: (NSString *) uid
456                 forObjectAtPath: (NSString *) objectPath
457 {
458   EOQualifier *qualifier;
459   NSArray *records;
460   NSMutableArray *acls;
461   NSString *qs;
462
463   qs = [NSString stringWithFormat: @"(c_object = '/%@') AND (c_uid = '%@')",
464                  objectPath, uid];
465   qualifier = [EOQualifier qualifierWithQualifierFormat: qs];
466   records = [[self ocsFolder] fetchAclMatchingQualifier: qualifier];
467
468   acls = [NSMutableArray array];
469   if ([records count] > 0)
470     {
471       [acls addObject: SOGoRole_AuthorizedSubscriber];
472       [acls addObjectsFromArray: [records valueForKey: @"c_role"]];
473     }
474
475   return acls;
476 }
477
478 - (void) _cacheRoles: (NSArray *) roles
479              forUser: (NSString *) uid
480      forObjectAtPath: (NSString *) objectPath
481 {
482   NSMutableDictionary *aclsForObject;
483
484   aclsForObject = [aclCache objectForKey: objectPath];
485   if (!aclsForObject)
486     {
487       aclsForObject = [NSMutableDictionary dictionary];
488       [aclCache setObject: aclsForObject
489                 forKey: objectPath];
490     }
491   if (roles)
492     [aclsForObject setObject: roles forKey: uid];
493   else
494     [aclsForObject removeObjectForKey: uid];
495 }
496
497 - (NSArray *) aclsForUser: (NSString *) uid
498           forObjectAtPath: (NSArray *) objectPathArray
499 {
500   NSArray *acls;
501   NSString *objectPath;
502   NSDictionary *aclsForObject;
503
504   objectPath = [objectPathArray componentsJoinedByString: @"/"];
505   aclsForObject = [aclCache objectForKey: objectPath];
506   if (aclsForObject)
507     acls = [aclsForObject objectForKey: uid];
508   else
509     acls = nil;
510   if (!acls)
511     {
512       acls = [self _fetchAclsForUser: uid forObjectAtPath: objectPath];
513       [self _cacheRoles: acls forUser: uid forObjectAtPath: objectPath];
514     }
515
516   if (!([acls count] || [uid isEqualToString: defaultUserID]))
517     acls = [self aclsForUser: defaultUserID
518                  forObjectAtPath: objectPathArray];
519
520   return acls;
521 }
522
523 - (void) removeAclsForUsers: (NSArray *) users
524             forObjectAtPath: (NSArray *) objectPathArray
525 {
526   EOQualifier *qualifier;
527   NSString *uids, *qs, *objectPath;
528   NSMutableDictionary *aclsForObject;
529
530   if ([users count] > 0)
531     {
532       objectPath = [objectPathArray componentsJoinedByString: @"/"];
533       aclsForObject = [aclCache objectForKey: objectPath];
534       if (aclsForObject)
535         [aclsForObject removeObjectsForKeys: users];
536       uids = [users componentsJoinedByString: @"') OR (c_uid = '"];
537       qs = [NSString
538              stringWithFormat: @"(c_object = '/%@') AND ((c_uid = '%@'))",
539              objectPath, uids];
540       qualifier = [EOQualifier qualifierWithQualifierFormat: qs];
541       [[self ocsFolder] deleteAclMatchingQualifier: qualifier];
542     }
543 }
544
545 - (void) _commitRoles: (NSArray *) roles
546                forUID: (NSString *) uid
547             forObject: (NSString *) objectPath
548 {
549   EOAdaptorChannel *channel;
550   GCSFolder *folder;
551   NSEnumerator *userRoles;
552   NSString *SQL, *currentRole;
553
554   folder = [self ocsFolder];
555   channel = [folder acquireAclChannel];
556   userRoles = [roles objectEnumerator];
557   currentRole = [userRoles nextObject];
558   while (currentRole)
559     {
560       SQL = [NSString stringWithFormat: @"INSERT INTO %@"
561                       @" (c_object, c_uid, c_role)"
562                       @" VALUES ('/%@', '%@', '%@')",
563                       [folder aclTableName],
564                       objectPath, uid, currentRole];
565       [channel evaluateExpressionX: SQL];
566       currentRole = [userRoles nextObject];
567     }
568
569   [folder releaseChannel: channel];
570 }
571
572 - (void) setRoles: (NSArray *) roles
573           forUser: (NSString *) uid
574   forObjectAtPath: (NSArray *) objectPathArray
575 {
576   NSString *objectPath;
577   NSMutableArray *newRoles;
578
579   [self removeAclsForUsers: [NSArray arrayWithObject: uid]
580         forObjectAtPath: objectPathArray];
581
582   newRoles = [NSMutableArray arrayWithArray: roles];
583   [newRoles removeObject: SOGoRole_AuthorizedSubscriber];
584   [newRoles removeObject: SOGoRole_None];
585   objectPath = [objectPathArray componentsJoinedByString: @"/"];
586   [self _cacheRoles: newRoles forUser: uid
587         forObjectAtPath: objectPath];
588   if (![newRoles count])
589     [newRoles addObject: SOGoRole_None];
590
591   [self _commitRoles: newRoles forUID: uid forObject: objectPath];
592 }
593
594 /* acls */
595 - (NSArray *) aclUsers
596 {
597   return [self aclUsersForObjectAtPath: [self pathArrayToSOGoObject]];
598 }
599
600 - (NSArray *) aclsForUser: (NSString *) uid
601 {
602   NSMutableArray *acls;
603   NSArray *ownAcls, *containerAcls;
604
605   acls = [NSMutableArray array];
606   ownAcls = [self aclsForUser: uid
607                   forObjectAtPath: [self pathArrayToSOGoObject]];
608   [acls addObjectsFromArray: ownAcls];
609   if ([container respondsToSelector: @selector (aclsForUser:)])
610     {
611       containerAcls = [container aclsForUser: uid];
612       if ([containerAcls count] > 0)
613         {
614           if ([containerAcls containsObject: SOGoRole_ObjectReader])
615             [acls addObject: SOGoRole_ObjectViewer];
616 #warning this should be checked
617           if ([containerAcls containsObject: SOGoRole_ObjectEraser])
618             [acls addObject: SOGoRole_ObjectEraser];
619         }
620     }
621
622   return acls;
623 }
624
625 - (void) setRoles: (NSArray *) roles
626           forUser: (NSString *) uid
627 {
628   return [self setRoles: roles
629                forUser: uid
630                forObjectAtPath: [self pathArrayToSOGoObject]];
631 }
632
633 - (void) removeAclsForUsers: (NSArray *) users
634 {
635   return [self removeAclsForUsers: users
636                forObjectAtPath: [self pathArrayToSOGoObject]];
637 }
638
639 - (NSString *) defaultUserID
640 {
641   return defaultUserID;
642 }
643
644 - (NSString *) httpURLForAdvisoryToUser: (NSString *) uid
645 {
646   return [[self soURL] absoluteString];
647 }
648
649 - (NSString *) resourceURLForAdvisoryToUser: (NSString *) uid
650 {
651   return [[self davURL] absoluteString];
652 }
653
654 - (id) lookupName: (NSString *) lookupName
655         inContext: (id) localContext
656           acquire: (BOOL) acquire
657 {
658   id obj;
659   NSArray *davNamespaces;
660   NSDictionary *davInvocation;
661   NSString *objcMethod;
662
663   obj = [super lookupName: lookupName inContext: localContext
664                acquire: acquire];
665   if (!obj)
666     {
667       davNamespaces = [self davNamespaces];
668       if ([davNamespaces count] > 0)
669         {
670           davInvocation = [lookupName asDavInvocation];
671           if (davInvocation
672               && [davNamespaces
673                    containsObject: [davInvocation objectForKey: @"ns"]])
674             {
675               objcMethod = [[davInvocation objectForKey: @"method"]
676                              davMethodToObjC];
677               obj = [[SoSelectorInvocation alloc]
678                       initWithSelectorNamed:
679                         [NSString stringWithFormat: @"%@:", objcMethod]
680                       addContextParameter: YES];
681               [obj autorelease];
682             }
683         }
684     }
685
686   return obj;
687 }
688
689 - (NSComparisonResult) _compareByOrigin: (SOGoFolder *) otherFolder
690 {
691   NSArray *thisElements, *otherElements;
692   unsigned thisCount, otherCount;
693   NSComparisonResult comparison;
694
695   thisElements = [nameInContainer componentsSeparatedByString: @"_"];
696   otherElements = [[otherFolder nameInContainer]
697                     componentsSeparatedByString: @"_"];
698   thisCount = [thisElements count];
699   otherCount = [otherElements count];
700   if (thisCount == otherCount)
701     {
702       if (thisCount == 1)
703         comparison = NSOrderedSame;
704       else
705         comparison = [[thisElements objectAtIndex: 0]
706                        compare: [otherElements objectAtIndex: 0]];
707     }
708   else
709     {
710       if (thisCount > otherCount)
711         comparison = NSOrderedDescending;
712       else
713         comparison = NSOrderedAscending;
714     }
715
716   return comparison;
717 }
718
719 - (NSComparisonResult) _compareByNameInContainer: (SOGoFolder *) otherFolder
720 {
721   NSString *otherName;
722   NSComparisonResult comparison;
723
724   otherName = [otherFolder nameInContainer];
725   if ([nameInContainer hasSuffix: @"personal"])
726     {
727       if ([otherName hasSuffix: @"personal"])
728         comparison = [nameInContainer compare: otherName];
729       else
730         comparison = NSOrderedAscending;
731     }
732   else
733     {
734       if ([otherName hasSuffix: @"personal"])
735         comparison = NSOrderedDescending;
736       else
737         comparison = NSOrderedSame;
738     }
739
740   return comparison;
741 }
742
743 - (NSComparisonResult) compare: (SOGoFolder *) otherFolder
744 {
745   NSComparisonResult comparison;
746
747   comparison = [self _compareByOrigin: otherFolder];
748   if (comparison == NSOrderedSame)
749     {
750       comparison = [self _compareByNameInContainer: otherFolder];
751       if (comparison == NSOrderedSame)
752         comparison
753           = [[self displayName]
754               localizedCaseInsensitiveCompare: [otherFolder displayName]];
755     }
756
757   return comparison;
758 }
759
760 /* WebDAV */
761
762 - (NSArray *) davNamespaces
763 {
764   return nil;
765 }
766
767 - (BOOL) davIsCollection
768 {
769   return [self isFolderish];
770 }
771
772 /* folder type */
773
774 - (NSString *) outlookFolderClass
775 {
776   [self subclassResponsibility: _cmd];
777
778   return nil;
779 }
780
781 /* description */
782
783 - (void) appendAttributesToDescription: (NSMutableString *) _ms
784 {
785   [super appendAttributesToDescription:_ms];
786   
787   [_ms appendFormat:@" ocs=%@", [self ocsPath]];
788 }
789
790 - (NSString *) loggingPrefix
791 {
792   return [NSString stringWithFormat:@"<0x%08X[%@]:%@>",
793                    self, NSStringFromClass([self class]),
794                    [self nameInContainer]];
795 }
796
797 @end /* SOGoFolder */