1/*
2  Copyright (C) 2002-2005 SKYRIX Software AG
3
4  This file is part of SOPE.
5
6  SOPE 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  SOPE 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 SOPE; 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#include "SoSubscriptionManager.h"
23#include "SoSubscription.h"
24#include "SoObject.h"
25#import <EOControl/EOControl.h>
26#include "common.h"
27
28@implementation SoSubscriptionManager
29
30static BOOL           debugOn         = NO;
31static NSTimeInterval defaultLifeTime = 3600;
32static int            expirationInterval = 10 * 60 /* every 10 minutes */;
33
34static SoSubscriptionManager *sm = nil;
35
36+ (id)sharedSubscriptionManager {
37  if (sm == nil) {
38    debugOn = [[NSUserDefaults standardUserDefaults]
39                boolForKey:@"SoSubscriptionManagerDebugEnabled"];
40
41    sm = [[SoSubscriptionManager alloc] init];
42  }
43  return sm;
44}
45
46- (id)init {
47  if ((self = [super init])) {
48    self->idToSubscription = [[NSMutableDictionary alloc] init];
49
50    [[NSNotificationCenter defaultCenter]
51      addObserver:self selector:@selector(trackChange:)
52      name:@"SoObjectChanged" object:nil];
53  }
54  return self;
55}
56- (void)dealloc {
57  [[NSNotificationCenter defaultCenter] removeObserver:self];
58  [self->idToSubscription release];
59  [super dealloc];
60}
61
62/* subscriptions */
63
64- (NSString *)nextSubscriptionID {
65  static int i = 1000;
66  i++;
67  return [NSString stringWithFormat:@"%i", i];
68}
69
70- (NSString *)subscribeURL:(NSURL *)_url forObserver:(NSURL *)_callback
71  type:(NSString *)_type delay:(NSTimeInterval)_delay
72  lifetime:(NSTimeInterval)_lifetime
73{
74  /* subscribe httpu://127.0.0.1:14970/ for 3600s (type update, delay 30s) */
75  SoSubscription *s;
76  NSString *sid;
77
78  if (_lifetime == 0.0) _lifetime = defaultLifeTime;
79  sid = [self nextSubscriptionID];
80
81  if (debugOn) {
82    [self debugWithFormat:@"subscribe url %@", _url];
83    [self debugWithFormat:@"  observer:   %@", _callback];
84    [self debugWithFormat:@"  type:       %@", _type];
85    [self debugWithFormat:@"  delay:      %i", (int)_delay];
86    [self debugWithFormat:@"SID:          %@", sid];
87  }
88
89  s = [[SoSubscription alloc] initWithID:sid
90                              url:_url observer:_callback type:_type
91                              delay:_delay lifetime:_lifetime];
92  [self->idToSubscription setObject:s forKey:sid];
93  [s autorelease];
94
95  [self debugWithFormat:@"expires: %@", [s expirationDate]];
96
97  if (s != nil && self->timer == nil) {
98    self->timer = [[NSTimer scheduledTimerWithTimeInterval:expirationInterval
99                            target:self
100                            selector:@selector(performExpirationCheck:)
101                            userInfo:nil
102                            repeats:YES]
103                            retain];
104  }
105
106  return [s subscriptionID];
107}
108
109- (NSString *)renewSubscription:(NSString *)_sid onURL:(NSURL *)_url {
110  SoSubscription *s;
111
112  if (debugOn)
113    [self debugWithFormat:@"renew subscription %@, url: %@", _sid, _url];
114
115  if ((s = [self->idToSubscription objectForKey:_sid]) == nil) {
116    [self logWithFormat:
117            @"attempt to renew non-existing subscription '%@'", _sid];
118    return nil;
119  }
120
121  if (![s isValidForURL:_url]) {
122    [self logWithFormat:
123            @"mismatch between subscription-id and subscribed resource"];
124    return nil;
125  }
126
127  if (![s renewSubscription])
128    return _sid;
129
130  return _sid;
131}
132
133- (BOOL)unsubscribeID:(NSString *)_sid onURL:(NSURL *)_url {
134  SoSubscription *s;
135
136  if (_sid == nil) return NO;
137  if (_url == nil) return NO;
138
139  if ((s = [self->idToSubscription objectForKey:_sid]) == nil) {
140    [self debugWithFormat:@"no subscription with id '%@'", _sid];
141    return NO;
142  }
143  if (![s isValidForURL:_url]) {
144    [self logWithFormat:
145            @"mismatch between subscription-id and subscribed resource"];
146    return NO;
147  }
148
149  [self->idToSubscription removeObjectForKey:_sid];
150  [self debugWithFormat:@"canceled subscription '%@'", _sid];
151  return YES;
152}
153
154- (id)pollSubscriptions:(NSArray *)_sids onURL:(NSURL *)_url {
155  NSEnumerator   *e;
156  NSMutableArray *pending  = nil;
157  NSMutableArray *inactive = nil;
158  NSString       *sid;
159
160  e = [_sids objectEnumerator];
161  while ((sid = [e nextObject])) {
162    SoSubscription *s;
163
164    if ((s = [self->idToSubscription objectForKey:sid]) == nil) {
165      [self debugWithFormat:@"no subscription with id '%@'", sid];
166      continue;
167    }
168    if (![s isValidForURL:_url]) {
169      [self debugWithFormat:@"subscription '%@' is not valid for given URL",
170              sid];
171      continue;
172    }
173
174    [s renewSubscription]; /* renew subscription on access ... */
175
176    if ([s hasEventsPending]) {
177      [self debugWithFormat:@"events pending on sid '%@' ...", sid];
178      [s resetEvents];
179      if (pending == nil) pending = [[NSMutableArray alloc] init];
180      [pending addObject:sid];
181    }
182    else {
183      [self debugWithFormat:@"no events pending on sid '%@' ...", sid];
184      if (inactive == nil) inactive = [[NSMutableArray alloc] init];
185      [inactive addObject:sid];
186    }
187  }
188
189  if (pending  == nil) pending  = [NSArray array];
190  if (inactive == nil) inactive = [NSArray array];
191  return [NSDictionary dictionaryWithObjectsAndKeys:
192                         pending,  @"pending",
193                         inactive, @"inactive",
194                         nil];
195}
196
197/* tracking changes */
198
199- (void)trackChangedURL:(NSURL *)_url {
200  [self debugWithFormat:@"url was changed: %@", _url];
201}
202- (void)trackChangedPath:(NSString *)_path {
203  [self debugWithFormat:@"path was changed: %@", _path];
204}
205- (void)trackChangedObject:(id)_object {
206  id url = nil;
207
208  if ((url = [_object baseURLInContext:nil]))
209    url = [NSURL URLWithString:url];
210
211  if (url) {
212    [self debugWithFormat:@"object with URL was changed: %@", _object];
213    [self trackChangedURL:url];
214  }
215  else
216    [self debugWithFormat:@"object without URL was changed: %@", _object];
217}
218
219- (void)trackChange:(NSNotification *)_notification {
220  id object;
221
222  if ((object = [_notification object]) == nil) {
223    [self logWithFormat:@"missing changed object in change notification ..."];
224    return;
225  }
226
227  if ([object isKindOfClass:[NSURL class]])
228    [self trackChangedURL:object];
229  else if ([object isKindOfClass:[NSString class]])
230    [self trackChangedPath:object];
231  else
232    [self trackChangedObject:object];
233}
234
235/* expiration check */
236
237- (void)performExpirationCheck:(NSTimer *)_timer {
238  NSAutoreleasePool *pool;
239  NSArray        *subs;
240  NSEnumerator   *e;
241  SoSubscription *s;
242
243  pool = [[NSAutoreleasePool alloc] init];
244
245  /* scan for expired subscriptions */
246
247  subs = [[self->idToSubscription allValues] shallowCopy];
248  if (debugOn) {
249    [self debugWithFormat:@"perform expiration check (%i subscriptions) ...",
250            [subs count]];
251  }
252
253  e = [subs objectEnumerator];
254  while ((s = [e nextObject])) {
255    if ([s isExpired]) {
256      if (debugOn)
257        [self debugWithFormat:@"  expired: %@", [s subscriptionID]];
258      [self->idToSubscription removeObjectForKey:[s subscriptionID]];
259    }
260  }
261
262  /* remove timer if we have no subscriptions ... */
263
264  if ([self->idToSubscription count] == 0) {
265    [self debugWithFormat:@"  no subscriptions left, removing timer .."];
266    [self->timer invalidate];
267    [self->timer release]; self->timer = nil;
268  }
269
270  [pool release];
271}
272
273/* debugging */
274
275- (BOOL)isDebuggingEnabled {
276  return debugOn;
277}
278
279@end /* SoSubscriptionManager */
280