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