]> err.no Git - sope/blob - sope-appserver/NGObjWeb/Templates/WOWrapperTemplateBuilder.m
enhanced input support in libxmlSAXdriver for HTML,
[sope] / sope-appserver / NGObjWeb / Templates / WOWrapperTemplateBuilder.m
1 /*
2   Copyright (C) 2000-2004 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 #include "WOWrapperTemplateBuilder.h"
23 #include "WODParser.h"
24 #include "WOHTMLParser.h"
25 #include "WOCompoundElement.h"
26 #include "WOChildComponentReference.h"
27 #include <NGObjWeb/WOAssociation.h>
28 #include "common.h"
29
30 /*
31   .wo components need to know at parsing time whether we are dealing
32   with a component or a dynamic element, the .wod or the .html does
33   not contain this information.
34   
35   What to do ... ?? Always checking the class isn't very nice either ..
36 */
37
38 @interface _WODFileEntry : NSObject
39 {
40 @public
41   NSString     *componentName;
42   NSDictionary *associations;
43   Class        componentClass;
44   signed char  isDynamicElement;
45 }
46
47 - (BOOL)isDynamicElement;
48 - (Class)componentClass;
49
50 @end
51
52 @interface WODynamicElement(UsedPrivates)
53 - (id)initWithElementName:(NSString *)_element
54   attributes:(NSDictionary *)_attributes
55   contentElements:(NSArray *)_subElements
56   componentDefinition:(id)_cdef;
57 + (BOOL)isDynamicElement;
58 @end
59
60 static Class AssocClass  = Nil;
61 static Class StrClass    = Nil;
62
63 @implementation WOWrapperTemplateBuilder
64
65 static BOOL logExtraAssociations = NO;
66 static BOOL logScriptAdditions   = NO;
67 static NSStringEncoding parserEncoding;
68
69 + (int)version {
70   return [super version] + 0 /* v2 */;
71 }
72
73 + (void)initialize {
74   NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
75   NSAssert2([super version] == 2,
76             @"invalid superclass (%@) version %i !",
77             NSStringFromClass([self superclass]), [super version]);
78   
79   AssocClass = [WOAssociation class];
80   StrClass   = [NSString      class];
81   
82   if ([ud boolForKey:@"WOParsersUseUTF8"]) {
83     parserEncoding = NSUTF8StringEncoding;
84     NSLog(@"Note: using UTF-8 as wrapper template parser encoding.");
85   }
86   else
87     parserEncoding = [NSString defaultCStringEncoding];
88 }
89
90 - (void)dealloc {
91   [self->lastException  release];
92   [self->definitions    release];
93   [self->componentNames release];
94   [self->iTemplate      release];
95   [super dealloc];
96 }
97
98 /* parsing */
99
100 - (BOOL)_parseDeclarationsFile:(NSData *)_decl {
101   NSDictionary *defs;
102   WODParser *parser;
103     
104   parser = [[[WODParser alloc] initWithHandler:(id)self] autorelease];
105   defs = [parser parseDeclarationData:_decl];
106   return defs ? YES : NO;
107 }
108
109 - (WOElement *)parseWithHTMLData:(NSData *)_html
110   declarationData:(NSData *)_decl
111 {
112   WOHTMLParser *parser;
113   NSArray      *topLevel    = nil;
114   NSException  *exception   = nil;
115   WOElement    *rootElement;
116   
117   /* parse declarations file */
118   if (![self _parseDeclarationsFile:_decl])
119     return nil;
120   
121   /* parse HTML file */
122   parser = [[[WOHTMLParser alloc] initWithHandler:(id)self] autorelease];
123   if ((topLevel = [parser parseHTMLData:_html]) == nil)
124     exception = [parser parsingException];
125   
126   /* setup root element */
127   
128   if ([topLevel count] == 1) {
129     rootElement = [[topLevel objectAtIndex:0] retain];
130   }
131   else if ([topLevel count] > 1) {
132     static Class CompoundElemClass = Nil;
133     if (CompoundElemClass == Nil)
134       CompoundElemClass = NSClassFromString(@"WOCompoundElement");
135     
136     rootElement =
137       [[CompoundElemClass allocForCount:[topLevel count] zone:[self zone]]
138                           initWithChildren:topLevel];
139   }
140   else /* no topLevel element */
141     rootElement = nil;
142   
143   if (exception) [exception raise];
144   return rootElement;
145 }
146
147 - (void)reset {
148   [self->definitions    removeAllObjects];
149   [self->componentNames removeAllObjects];
150   [self->iTemplate release]; self->iTemplate = nil;
151 }
152
153 - (NSData *)rewriteData:(NSData *)_data 
154   fromEncoding:(NSStringEncoding)_from
155   toEncoding:(NSStringEncoding)_to
156 {
157   NSString *s;
158   NSData   *d;
159   
160   if ((s = [[NSString alloc] initWithData:_data encoding:_from]) == nil) {
161     [self logWithFormat:@"ERROR: template file has incorrect encoding!"];
162     return _data;
163   }
164   if ((d = [s dataUsingEncoding:_to]) == nil) {
165     [self logWithFormat:
166             @"ERROR: could not represent template file in parser encoding!"];
167     return _data;
168   }
169   return d;
170 }
171
172 - (NSException *)_handleBuildException:(NSException *)_exc atURL:(NSURL *)_url{
173   NSException *newException;
174   NSDictionary *userInfo;
175   NSMutableDictionary *newUserInfo;
176
177   [self reset];
178
179   if ((userInfo = [_exc userInfo]) != nil) {
180     newUserInfo = [[NSMutableDictionary alloc] initWithCapacity:
181                                                  [userInfo count] + 1];
182     [newUserInfo addEntriesFromDictionary:userInfo];
183     [newUserInfo setObject:_url forKey:@"templateURL"];
184   }
185   else {
186     newUserInfo = (NSMutableDictionary *)
187       [[NSDictionary alloc] initWithObjectsAndKeys:_url, @"templateURL", nil];
188   }
189   newException = [NSException exceptionWithName:[_exc name]
190                               reason:[_exc reason]
191                               userInfo:newUserInfo];
192   return newException;
193 }
194
195 - (WOTemplate *)buildTemplateAtURL:(NSURL *)_url {
196   static NSData *emptyData = nil;
197   NSFileManager *fm;
198   WOTemplate    *template;
199   WOElement     *rootElement;
200   BOOL          withLanguage;
201   NSData        *wodFile     = nil;
202   NSData        *htmlFile    = nil;
203   NSDictionary  *wooFile     = nil;
204   NSString      *tmpPath;
205   NSString      *path, *name;
206   NSStringEncoding encoding;
207   WOComponentScript *script;
208   id tmp;
209   
210   NSAssert(self->iTemplate == nil, @"parsing in progress !!!");
211   [self reset];
212   [self->lastException release]; self->lastException = nil;
213   
214   if (_url == nil)
215     return nil;
216   
217   if (![_url isFileURL]) {
218     [self logWithFormat:@"can only process wrappers at file-URLs: %@", _url];
219     return nil;
220   }
221
222   if (emptyData == nil)
223     emptyData = [[NSData alloc] init];
224   
225   fm       = [NSFileManager defaultManager];
226   path     = [_url path];
227   
228   if (self->definitions == nil)
229     self->definitions = [[NSMutableDictionary alloc] initWithCapacity:64];
230   
231   tmpPath      = [path lastPathComponent];
232   withLanguage = [[tmpPath pathExtension] isEqualToString:@"lproj"];
233   
234   /*
235     TODO: can this code handle ".wo" templates without a wrapper? Eg if I place
236           Main.html and Main.wod directly into the bundle resources directory?
237     TODO: can this code handle static names for contained files? (eg 
238           template.wod instead of Main.wod) This way we could avoid renaming
239           the individual files if the wrapper name changes.
240   */
241   if (withLanguage) {
242     /* eg /a/b/c/a.wo/English.lproj/a.html */
243     tmpPath = [path stringByDeletingLastPathComponent];
244     name = [[tmpPath lastPathComponent] stringByDeletingPathExtension];
245   }
246   else {
247     /* eg /a/b/c/a.wo/a.html */
248     name = [[path lastPathComponent] stringByDeletingPathExtension];
249   }
250   
251   tmpPath = [name stringByAppendingPathExtension:@"wod"];
252   tmpPath = [path stringByAppendingPathComponent:tmpPath];
253   wodFile = [NSData dataWithContentsOfFile:tmpPath];
254   
255   tmpPath = [name stringByAppendingPathExtension:@"html"];
256   tmpPath = [path stringByAppendingPathComponent:tmpPath];
257   htmlFile = [NSData dataWithContentsOfFile:tmpPath];
258
259   tmpPath = [name stringByAppendingPathExtension:@"woo"];
260   tmpPath = [path stringByAppendingPathComponent:tmpPath];
261   if ([fm fileExistsAtPath:tmpPath])
262     wooFile = [NSDictionary dictionaryWithContentsOfFile:tmpPath];
263   
264   /* process language specific pathes */
265   
266   script = nil;
267   if (wodFile == nil) { /* no .wod, no script (cannot be bound ...) */
268     if (withLanguage) {
269       tmpPath = [name stringByAppendingPathExtension:@"wod"];
270       tmpPath = [[path stringByDeletingLastPathComponent]
271                        stringByAppendingPathComponent:tmpPath];
272       
273       if ((wodFile = [NSData dataWithContentsOfFile:tmpPath]) == nil) {
274         wodFile = emptyData;
275         [self logWithFormat:
276                 @"%s:%i:\n"
277                 @"  could not load wod file of component '%@'\n"
278                 @"  URL:      '%@'\n"
279                 @"  tmp-path: '%@'\n"
280                 @"  path:     '%@'",
281                 __PRETTY_FUNCTION__, __LINE__, 
282                 name, [_url absoluteString], tmpPath, path];
283       }
284     }
285     else {
286       wodFile = emptyData;
287       [self logWithFormat:
288               @"%s:%i:\n  could not load wod file of %@\n"
289               @"  path=%@, not with lang.",
290               __PRETTY_FUNCTION__, __LINE__, name, path];
291     }
292   }
293   else {
294     /* check for script */
295     NSFileManager *fm = [NSFileManager defaultManager];
296     
297     tmpPath = [name stringByAppendingPathExtension:@"js"];
298     tmpPath = [path stringByAppendingPathComponent:tmpPath];
299     
300     if ([fm fileExistsAtPath:tmpPath])
301       script = [[WOComponentScript alloc] initWithContentsOfFile:tmpPath];
302   }
303   if (htmlFile == nil) {
304     [self logWithFormat:@"%s:\n  could not load html file of component %@.",
305             __PRETTY_FUNCTION__, name];
306     return nil;
307   }
308   
309   /* process string encoding */
310   
311   encoding = parserEncoding;
312   if ((tmp = [wooFile objectForKey:@"encoding"])) {
313     // TODO: move to an NSString category, isn't there a method for this in
314     //       Foundation?!
315
316     encoding = 0;
317     
318     if ([tmp isEqualToString:@"NSASCIIStringEncoding"])
319       encoding = NSASCIIStringEncoding;
320     else if ([tmp isEqualToString:@"NSNEXTSTEPStringEncoding"])
321       encoding = NSNEXTSTEPStringEncoding;
322     else if ([tmp isEqualToString:@"NSUTF8StringEncoding"])
323       encoding = NSUTF8StringEncoding;
324     else if ([tmp isEqualToString:@"NSISOLatin1StringEncoding"])
325       encoding = NSISOLatin1StringEncoding;
326     else if ([tmp isEqualToString:@"NSISOLatin2StringEncoding"])
327       encoding = NSISOLatin2StringEncoding;
328     else if ([tmp isEqualToString:@"NSUnicodeStringEncoding"])
329       encoding = NSUnicodeStringEncoding;
330     else if ([tmp length] == 0)
331       ; // keep platform encoding
332 #if LIB_FOUNDATION_LIBRARY
333     else if ([tmp isEqualToString:@"NSISOLatin9StringEncoding"])
334       encoding = NSISOLatin9StringEncoding;
335     else if ([tmp isEqualToString:@"NSWinLatin1StringEncoding"])
336       encoding = NSWinLatin1StringEncoding;
337 #endif
338 #if NeXT_Foundation_LIBRARY || COCOA_Foundation_LIBRARY
339     else
340       encoding = [NSString stringEncodingForEncodingNamed:tmp];
341 #endif
342     
343     if (encoding == 0) {
344       [self logWithFormat:
345               @"ERROR(%s): cannot deal with template encoding: '%@'",
346               __PRETTY_FUNCTION__, tmp];
347       encoding = parserEncoding;
348     }
349     
350     if (encoding != parserEncoding) {
351       // TODO: HACK and slow, the parsers should be able to deal with Unicode
352       // TODO: in case this works, remove the log
353       [self logWithFormat:
354               @"Note: rewriting template NSData for parser encoding (%@=>%@).",
355               [NSString localizedNameOfStringEncoding:encoding],
356               [NSString localizedNameOfStringEncoding:parserEncoding]];
357       
358       htmlFile = [self rewriteData:htmlFile 
359                        fromEncoding:encoding toEncoding:parserEncoding];
360       wodFile  = [self rewriteData:wodFile
361                        fromEncoding:encoding toEncoding:parserEncoding];
362     }
363   }
364   
365   /* instantiate template */
366   
367   self->iTemplate = [[WOTemplate alloc] initWithURL:_url rootElement:nil];
368   
369   NS_DURING
370     rootElement = [self parseWithHTMLData:htmlFile declarationData:wodFile];
371   NS_HANDLER
372     [[self _handleBuildException:localException atURL:_url] raise];
373   NS_ENDHANDLER;
374   
375   [self->iTemplate setRootElement:rootElement];
376   template = self->iTemplate;
377   self->iTemplate = nil;
378   
379   [self reset];
380   
381   if ((tmp = [wooFile objectForKey:@"variables"]))
382     [template setKeyValueArchivedTemplateVariables:tmp];
383   
384   if (script) {
385     if (logScriptAdditions) {
386       [self logWithFormat:@"adding script %@ to template: '%@'", 
387               script, template];
388     }
389     [template setComponentScript:script];
390     [script release];
391   }
392   
393   return [template autorelease];
394 }
395
396 /* HTML parser callbacks */
397
398 - (NSString *)_uniqueComponentNameForDefinitionWithName:(NSString *)_element {
399   NSString *cname;
400   int      i = 0;
401   
402   if (self->componentNames == nil)
403     self->componentNames = [[NSMutableSet alloc] init];
404   
405   cname = _element;
406   while ([self->componentNames containsObject:cname]) {
407     cname = [NSString stringWithFormat:@"%@%i", _element, i];
408     i++;
409     if (i > 200) break;
410   }
411   
412   NSAssert3(i < 200,
413             @"more than 200 components for definition named %@ "
414             @"(last name %@) (names=%@) ??",
415             _element, cname, self->componentNames);
416   
417   [self->componentNames addObject:cname];
418   return cname;
419 }
420
421 - (WOElement *)componentWithName:(NSString *)_element
422   attributes:(NSDictionary *)_attributes // not the associations !
423   contentElements:(NSArray *)_subElements
424 {
425   /* setup a new child component reference */
426   static Class ChildRefClass = Nil;
427   _WODFileEntry      *def;
428   WOChildComponentReference *element = nil;
429   NSString *cname = nil;
430   
431   if ((def = [self->definitions objectForKey:_element]) == nil)
432     return nil;
433   
434   if (ChildRefClass == Nil)
435     ChildRefClass = NSClassFromString(@"WOChildComponentReference");
436   
437   cname = [self _uniqueComponentNameForDefinitionWithName:_element];
438   
439   /* add subcomponent info */
440   [self->iTemplate
441        addSubcomponentWithKey:cname
442        name:def->componentName
443        bindings:def->associations];
444   
445   /* add subcomponent reference */
446   element = [[ChildRefClass alloc]
447                             initWithName:cname
448                             associations:nil
449                             contentElements:_subElements];
450   if (element == nil) {
451     [self logWithFormat:
452             @"ERROR: could not instantiate child component reference."];
453   }
454   
455   return element;
456 }
457
458 - (WOElement *)dynamicElementWithName:(NSString *)_element
459   attributes:(NSDictionary *)_attributes // not the associations !
460   contentElements:(NSArray *)_subElements
461 {
462   _WODFileEntry *def;
463   Class               elementClass;
464   NSMutableDictionary *assoc = nil;
465   WODynamicElement    *element;
466
467   if ((def = [self->definitions objectForKey:_element]) == nil) {
468     [self logWithFormat:
469             @"ERROR: did not find definition of dynamic element '%@'",
470             _element];
471     return [[NSClassFromString(@"WONoContentElement") alloc]
472                                 initWithElementName:_element
473                                 attributes:_attributes
474                                 contentElements:_subElements
475                                 componentDefinition:nil];
476   }
477   
478   if (![def isDynamicElement]) {
479     /* definition describes a component */
480     return [self componentWithName:_element
481                  attributes:_attributes
482                  contentElements:_subElements];
483   }
484   
485   elementClass = [def componentClass];
486   NSAssert1(elementClass, @"got no class for element %@", def);
487   
488   assoc = [def->associations mutableCopy];
489
490   element = [[elementClass alloc]
491                            initWithName:_element
492                            associations:assoc
493                            contentElements:_subElements];
494   if (element == nil) {
495     NSLog(@"ERROR: could not instantiate dynamic element of class %@",
496           NSStringFromClass(elementClass));
497   }
498   if ([assoc count] > 0) {
499     if (logExtraAssociations)
500       [self logWithFormat:@"remaining definition attributes: %@", assoc];
501     [element setExtraAttributes:assoc];
502   }
503   [assoc release]; assoc = nil;
504
505   return element;
506 }
507
508 /* WOTemplate(HTMLParser) */
509
510 - (BOOL)parser:(id)_parser willParseHTMLData:(NSData *)_data {
511   return YES;
512 }
513
514 - (void)parser:(id)_parser finishedParsingHTMLData:(NSData *)_data
515   elements:(NSArray *)_elements
516 {
517 }
518
519 - (void)parser:(id)_parser failedParsingHTMLData:(NSData *)_data
520   exception:(NSException *)_exception
521 {
522 }
523
524 /* WOTemplate(WODParser) */
525
526 - (BOOL)parser:(id)_parser willParseDeclarationData:(NSData *)_data {
527   return YES;
528 }
529 - (void)parser:(id)_parser finishedParsingDeclarationData:(NSData *)_data
530   declarations:(NSDictionary *)_decls
531 {
532 }
533 - (void)parser:(id)_parser failedParsingDeclarationData:(NSData *)_data
534   exception:(NSException *)_exception
535 {
536   [_exception raise];
537 }
538
539 - (id)parser:(id)_parser makeAssociationWithValue:(id)_value {
540   return [AssocClass associationWithValue:_value];
541 }
542 - (id)parser:(id)_parser makeAssociationWithKeyPath:(NSString *)_keyPath {
543   NSCAssert([_keyPath isKindOfClass:StrClass],
544             @"invalid keypath property (expected string)");
545   return [AssocClass associationWithKeyPath:_keyPath];
546 }
547 - (id)parser:(id)_parser makeDefinitionForComponentNamed:(NSString *)_cname
548   associations:(id)_entry
549   elementName:(NSString *)_elemName
550 {
551   _WODFileEntry *def;
552   
553   def = [[[_WODFileEntry alloc] init] autorelease];
554   def->componentName = [_cname copy];
555   def->associations  = [_entry retain];
556   
557   [self->definitions setObject:def forKey:_elemName];
558   
559   return def;
560 }
561
562 @end /* WOWrapperTemplateBuilder */
563
564 @implementation _WODFileEntry
565
566 - (id)init {
567   self->isDynamicElement = -1;
568   return self;
569 }
570
571 - (void)dealloc {
572   [self->componentName release];
573   [self->associations  release];
574   [super dealloc];
575 }
576
577 /* accessors */
578
579 - (NSString *)componentName {
580   return self->componentName;
581 }
582 - (NSDictionary *)bindings {
583   return self->associations;
584 }
585
586 - (Class)componentClass {
587   if (self->componentClass == nil)
588     self->componentClass = NSClassFromString(self->componentName);
589   
590   return self->componentClass;
591 }
592
593 - (BOOL)isDynamicElement {
594   if (self->isDynamicElement == -1) {
595     self->isDynamicElement = 
596       [[self componentClass] isDynamicElement] ? 1 : 0;
597   }
598   return (self->isDynamicElement == 0) ? NO : YES;
599 }
600
601 @end /* _WODFileEntry */