View Javadoc

1   /*
2   Wotonomy: OpenStep design patterns for pure Java applications.
3   Copyright (C) 2000 Blacksmith, Inc.
4   
5   This library is free software; you can redistribute it and/or
6   modify it under the terms of the GNU Lesser General Public
7   License as published by the Free Software Foundation; either
8   version 2.1 of the License, or (at your option) any later version.
9   
10  This library is distributed in the hope that it will be useful,
11  but WITHOUT ANY WARRANTY; without even the implied warranty of
12  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  Lesser General Public License for more details.
14  
15  You should have received a copy of the GNU Lesser General Public
16  License along with this library; if not, see http://www.gnu.org
17  */
18  
19  package net.wotonomy.web;
20  
21  import java.io.BufferedReader;
22  import java.io.ByteArrayInputStream;
23  import java.io.IOException;
24  import java.io.InputStream;
25  import java.io.InputStreamReader;
26  import java.io.LineNumberReader;
27  import java.io.PushbackInputStream;
28  import java.io.StringReader;
29  import java.lang.reflect.InvocationTargetException;
30  import java.util.Enumeration;
31  import java.util.HashMap;
32  import java.util.Iterator;
33  import java.util.LinkedList;
34  import java.util.List;
35  import java.util.Map;
36  import java.util.StringTokenizer;
37  
38  import net.wotonomy.control.EOKeyValueCodingSupport;
39  import net.wotonomy.foundation.NSArray;
40  import net.wotonomy.foundation.NSDictionary;
41  import net.wotonomy.foundation.NSMutableDictionary;
42  import net.wotonomy.foundation.NSSelector;
43  
44  /***
45  * Pure java implementation of WOComponent.
46  *
47  * @author michael@mpowers.net
48  * @author $Author: cgruber $
49  * @version $Revision: 905 $
50  */
51  public class WOComponent
52      extends WOElement
53      implements WOActionResults,
54                 net.wotonomy.control.EOKeyValueCodingAdditions,
55                 net.wotonomy.control.EOKeyValueCoding
56  {
57  	WOElement rootElement;
58      
59      private static final String DIRECTORY_SUFFIX = ".wo";
60      private static final String TEMPLATE_SUFFIX = ".html";
61      private static final String DECLARATION_SUFFIX = ".wod";
62  
63      private static final String OPEN_TAG = "webobject";
64      private static final String CLOSE_TAG = "/webobject";
65      private static final String NAME_KEY = "name";
66      
67      protected transient WOContext context; // don't persist
68      protected boolean cachingEnabled;
69      protected WOElement template;
70      protected WOComponent parent;
71  
72  	/***
73  	* Default constructor.  Deprecated in latest spec.
74  	*/
75      public WOComponent ()
76      {
77          parent = null;
78      	cachingEnabled = true;
79          template = null;
80      }
81      
82      /***
83      * Constructor specifying a context.
84      */
85      public WOComponent( WOContext aContext )
86      {
87          this();
88          context = aContext;
89      }
90      
91  	/***
92  	* Returns the name of the component, which is usually just the class name.
93  	*/
94      public String name ()
95      {
96      	return justTheClassName();
97      }
98      
99      /*** 
100     * Returns the system-dependent file path to the current component
101     * directory, including the ".wo" extension.
102     */
103     public String path ()
104     {
105     	throw new RuntimeException( "Not implemented yet." );
106     }
107 
108 	/***
109 	* Returns the URL for this component, relative to the server's
110 	* document root on the server's file system. 
111     * This is not an http url.
112 	*/
113     public String baseURL ()
114     {
115     	throw new RuntimeException( "Not implemented yet." );
116     }
117 
118 	/***
119 	* Returns the name of the framework that contains this component,
120 	* or null if the component does not belong to a framework.
121 	* This currently returns the package path of the class, or
122 	* null if it does not belong to a package.
123 	*/
124     public String frameworkName ()
125     {
126         return justTheResourcePath();
127     }
128     
129     /***
130     * Sets whether templates are cached.  If true, templates will
131     * only be read once per application lifetime.  Otherwise, templates
132     * will be read each time this class is instantiated.  Defaults to false.
133     */
134     public void setCachingEnabled (boolean enabled)
135 	{
136 		cachingEnabled = enabled;
137 	}
138 
139     /***
140     * Returns whether templates are cached.  If true, templates are
141     * read once per application lifetime.  Otherwise, templates are
142     * read each time this class is instantiated.
143     */
144     public boolean isCachingEnabled ()
145     {
146     	return cachingEnabled && WOApplication.application().isCachingEnabled();
147     }
148     
149     /***
150     * Returns the root of the tree of elements produced by parsing
151     * the templates in the component directory for this component.
152     */
153     public WOElement template()
154     {
155         return template;
156     }
157     
158     /***
159     * Returns the root of the tree of elements produced by parsing
160     * the templates in the component directory for the named component.
161     * @deprecated Use template() instead.
162     */
163     public WOElement templateWithName(String aComponentName)
164     {
165         return templateWithName( aComponentName, null );
166     }
167     
168     /***
169     * Returns the root of the tree of elements produced by parsing
170     * the templates in the component directory for the named component.
171     */
172     WOElement templateWithName(String aComponentName, String aFramework)
173     {
174         NSArray languages = null;
175         WOContext context = context();
176         if ( context != null )
177         {
178             languages = context.request().browserLanguages();
179         }
180         WOElement result = templateWithHTMLString( 
181             readTemplateResource( aComponentName, aFramework, TEMPLATE_SUFFIX, languages ), 
182             readTemplateResource( aComponentName, aFramework, DECLARATION_SUFFIX, languages ), 
183             languages );
184         if ( result == null )
185         {
186             System.out.println( "WOComponent.templateWithName: failed for " + aComponentName );
187         }
188         return result;
189     }
190 
191     /***
192     * Returns the root of the tree of elements produced by parsing
193     * the specfified HTML string and bindings declaration string.
194     * Note: language list is currently ignored.
195     */
196     public static WOElement templateWithHTMLString (
197     	String anHTMLString, String aDeclaration, List aLanguageList)
198     {
199         if ( anHTMLString == null ) return null;
200         WOElement result = null;
201         try
202         {
203 			NSDictionary bindings = processDeclaration( aDeclaration );
204             List elements = new LinkedList();
205             int index = processTemplate( elements, anHTMLString, 0, bindings, aLanguageList );
206             if ( index == -1 )
207             {
208                 if ( elements.size() == 1 )
209                 {
210                     result = (WOElement) elements.get(0);
211                 }
212                 else
213                 {
214                     result = new WOParentElement( elements );
215                 }
216             }
217             else // entire template did not process
218             {
219                 throw new RuntimeException( "No closing tag: " + anHTMLString.substring( index ) );
220             }
221         }
222         catch ( Exception exc )
223         {
224             exc.printStackTrace();
225         }
226 		return result;
227     }
228     
229     /***
230     * Called at the beginning of a request-response cycle.
231     * Override to perform any necessary initialization.
232     * This implementation does nothing.
233     */
234     public void awake ()
235     {
236     }
237 
238     /***
239     * Package access only.  Called to initialize the component with 
240     * the proper context before the start of the request-response cycle.
241     * If the context has a current component, that component becomes
242     * this component's parent.
243     */
244     void ensureAwakeInContext (WOContext aContext)
245     {
246 		context = aContext;
247 		parent = aContext.parent();
248         if ( template == null )
249         {
250 	        template = templateWithName( name(), frameworkName() );
251         }
252         if ( template != null )
253         {
254             template.ensureAwakeInContext( aContext );
255         }
256         awake();
257     }
258 
259     public void takeValuesFromRequest (WORequest aRequest, WOContext aContext)
260     {
261         if ( synchronizesVariablesWithBindings() )
262         {
263             pullValuesFromParent();
264             if ( template != null )
265             {
266                 template.takeValuesFromRequest( aRequest, aContext ); 
267             }
268             pushValuesToParent();
269         }
270         else
271         if ( template != null )
272         {
273             template.takeValuesFromRequest( aRequest, aContext ); 
274         }
275     }
276 
277     public WOActionResults invokeAction (WORequest aRequest, WOContext aContext)
278     {
279     	WOActionResults result = null;
280         if ( synchronizesVariablesWithBindings() )
281         {
282             pullValuesFromParent();
283             if ( template != null )
284             {
285                 result = template.invokeAction( aRequest, aContext ); 
286             }
287             pushValuesToParent();
288         }
289         else
290         if ( template != null )
291         {
292             result = template.invokeAction( aRequest, aContext ); 
293         }
294 		return result;
295     }
296 
297     public void appendToResponse (WOResponse aResponse, WOContext aContext)
298     {
299         if ( synchronizesVariablesWithBindings() )
300         {
301             pullValuesFromParent();
302             if ( template != null )
303             {
304                 template.appendToResponse( aResponse, aContext ); 
305             }
306             pushValuesToParent();
307         }
308         else
309         if ( template != null )
310         {
311             template.appendToResponse( aResponse, aContext ); 
312         }
313         context = null;
314     }
315 
316     /***
317     * Called at the end of a request-response cycle.
318     * Override to perform any necessary clean-up.
319     * This implementation does nothing.
320     */
321     public void sleep ()
322     {
323     }
324     
325     /***
326     * Generates a WOResponse and calls appendToResponse() on it.
327     */
328     public WOResponse generateResponse ()
329     {
330     	WOResponse response = new WOResponse();
331         WOContext context = context();
332         appendToResponse( response, context ); // nulls out context
333         context.session().savePage( this ); //?is this the right place for this?
334         return response;
335     }
336 
337 	/***
338 	* Returns this component's parent component, or null if none.
339 	*/    
340     public WOComponent parent()
341     {
342     	return parent;
343     }
344 
345     /***
346     * Invokes the specified action on this component's parent.
347     * Variables will be synchronized when this method returns.
348     */
349     public WOActionResults performParentAction(String anAction)
350     {
351         WOActionResults result = parent().performAction( anAction );
352         if ( synchronizesVariablesWithBindings() )
353         {
354             pullValuesFromParent();
355         }
356         return result;
357     }
358     
359     /***
360     * Invokes the specified action on this component.
361     */
362     WOActionResults performAction( String anAction )
363     {
364 	    try 
365 	    {
366 	    	return (WOActionResults) NSSelector.invoke( anAction, this );
367 		}
368 		catch ( NoSuchMethodException exc )
369 		{
370 			// returns below
371 		}
372 		catch ( InvocationTargetException exc )
373 		{
374 			Throwable t = exc.getTargetException();
375             exc.printStackTrace();
376 			throw new RuntimeException( t.toString() );
377 		}
378 		catch ( Exception exc )
379 		{
380             exc.printStackTrace();
381 			throw new RuntimeException( exc.toString() );
382 		}
383         return null;
384     }
385 
386     /***
387     * Called before each phase of the request-response cycle,
388     * if synchronizesVariablesWithBindings is true and the 
389     * component is not stateless.
390     */
391     public void pullValuesFromParent()
392     {
393         if ( associations == null ) return;
394         String key;
395         Enumeration e = associations.keyEnumerator();
396         while ( e.hasMoreElements() )
397         {
398             key = e.nextElement().toString();
399             takeValueForKey( valueForBinding( key ), key );
400         }
401     }
402     
403     /***
404     * Called after each phase of the request-response cycle,
405     * if synchronizesVariablesWithBindings is true and the 
406     * component is not stateless.
407     */
408     public void pushValuesToParent()
409     {
410         if ( associations == null ) return;
411         String key;
412         Enumeration e = associations.keyEnumerator();
413         while ( e.hasMoreElements() )
414         {
415             key = e.nextElement().toString();
416             setValueForBinding( valueForKey( key ), key );
417         }
418     }
419     
420     /***
421     * Returns whether this component should be considered stateless.
422     * Stateless components are shared between sessions to conserve memory.
423     * This implementation returns false; override to return true.
424     */
425     public boolean isStateless()
426     {
427         return false;
428     }
429     
430     /***
431     * Called only on stateless components to tell themselves to reset
432     * themselves for another invocation using a different context.
433     * This implementation does nothing.
434     */
435     public void reset()    
436     {
437         // does nothing
438     }
439     
440     /***
441     * Returns the application containing this instance of the class. 
442     */ 
443     public WOApplication application ()
444     {
445     	return context.application();
446     }
447     
448     /***
449     * Returns whether a session has been created for this user.
450     */
451     public boolean hasSession ()
452     {
453     	return context.hasSession();
454     }
455 
456     /***
457     * Returns the current session object, creating it if it doesn't exist.
458     */
459     public WOSession session ()
460     {
461     	return context.session();
462     }
463 
464 	/***
465 	* Returns the current context for this component.
466 	*/
467     public WOContext context ()
468     {
469     	return context;
470     }
471 
472 	/***
473 	* Returns a new WOComponent with the specified name.
474 	* If null, returns the component named "Main".
475 	* If the named component doesn't exist, returns null.
476 	*/ 
477     public WOComponent pageWithName (String aName)
478     {
479     	return application().pageWithName( aName, context() );
480     }
481 
482 	/***
483 	* Called when exceptions are raised by assigning values 
484 	* to this object.  This implementation does nothing, but 
485 	* subclasses may override to do something useful.
486 	*/
487     public void validationFailedWithException (
488     	Throwable anException, Object aValue, String aPath)
489     {
490         // does nothing
491     }
492 
493 	/***
494 	* Called on the component that represents the requested page.
495 	* Override to return logging information specific to your 
496 	* component.  This implementation returns the component's name.
497 	*/ 
498     public String descriptionForResponse (
499     	WOResponse aResponse, WOContext aContext)
500     {
501     	return name();
502     }
503 
504 	/***
505 	* Returns true if this component should get and set values
506 	* in its parent.  This implementation returns true.
507 	* Override to create a component that does not automatically
508     * synchronize bindings with its parent, useful if you wish
509     * to handle synchronization manually.
510 	*/
511     public boolean synchronizesVariablesWithBindings ()
512     {
513     	return true;
514     }
515 
516     /***
517     * Returns whether this component has a readable value that maps
518     * to the specified binding.  This implementation calls 
519     * hasBinding(aBinding).
520     */
521     public boolean canGetValueForBinding(String aBinding)
522     {
523         return hasBinding( aBinding );
524     }
525     
526     /***
527     * Returns whether this component has a writable value that maps
528     * to the specified binding.  
529     */
530     public boolean canSetValueForBinding(String aBinding)
531     {
532         WOAssociation assoc = 
533             (WOAssociation)associations.objectForKey(aBinding);
534         if (assoc != null) 
535         {
536             if ( assoc.isValueSettable() ) return true;
537         }
538         return false;
539     }
540 
541 	/*** 
542 	* Returns whether this component has the specified binding.
543 	*/
544     public boolean hasBinding (String aBinding)
545     {
546         if ( associations == null ) return false;
547     	return associations.containsKey( aBinding );
548     }
549 
550 	/***
551 	* Returns the value for the specified binding for this component.
552 	* The parent component is expected to have set the binding for 
553 	* this component. If no such binding exists, the binding is 
554 	* treated as a property is and obtained using valueForKey.
555 	* If the property is not found, this method returns null.
556 	*/
557     public Object valueForBinding (String aBinding)
558     {
559         WOComponent parent = parent();
560         if ( associations != null )
561         {
562             WOAssociation assoc = 
563                 (WOAssociation)associations.objectForKey(aBinding);
564             if (assoc != null && parent != null) 
565             {
566                 return assoc.valueInComponent( parent );
567             }
568         }
569         if ( parent != null )
570         {
571             return parent.valueForKey( aBinding );
572         }
573         return null;
574    }
575 
576 	/***
577 	* Sets the value for the specified binding for this component.
578 	* The parent component is expected to have set the binding
579 	* for this component.  If no such binding exists, the binding
580 	* is treated as a property and is set using takeValueForKey.
581 	* If the property is not found, this method fails silently.
582 	*/
583     public void setValueForBinding (Object aValue, String aBinding)
584     {
585         if ( associations == null ) return;
586         
587         WOComponent parent = parent();
588 
589         if ( associations != null )
590         {
591             WOAssociation assoc = 
592                 (WOAssociation)associations.objectForKey(aBinding);
593             if (assoc != null && parent != null) 
594             {
595                 if ( assoc.isValueSettable() )
596                 {
597                     assoc.setValue( aValue, parent );
598                     return;
599                 }
600             }
601         }
602         if ( parent != null )
603         {
604             parent.takeValueForKey( aValue, aBinding );
605         }
606     }
607  
608     public static void logString (String aString)
609     {
610     	System.out.println( aString );
611     }
612 
613     public static void debugString (String aString)
614     {
615     	System.err.println( aString );
616     }
617 
618     public Object valueForKeyPath (String aPath)
619     {
620         // currently key value coding support also handles keypaths
621     	return valueForKey( aPath );
622     }
623 
624     public void takeValueForKeyPath (Object aValue, String aPath)
625     {
626         // currently key value coding support also handles keypaths
627     	takeValueForKey( aValue, aPath );
628     }
629 
630     public NSDictionary valuesForKeys (List aKeyList)
631     {
632     	throw new RuntimeException( "Not implemented yet." );
633     }
634 
635     public void takeValuesFromDictionary (Map aValueMap)
636     {
637     	throw new RuntimeException( "Not implemented yet." );
638     }
639 
640     public Object valueForKey (String aKey)
641     { // System.out.println( "valueForKey: " + aKey + "->" + this );      
642         // handle "^" property keys
643         if ( aKey.startsWith( "^" ) )
644         {
645             return valueForBinding( aKey.substring(1) );
646         }
647     	return EOKeyValueCodingSupport.valueForKey( this, aKey );
648     }
649 
650     public void takeValueForKey (Object aValue, String aKey)
651     { // System.out.println( "takeValueForKey: " + aKey + " : " + aValue + "->" + this );      
652         // handle "^" property keys
653         if ( aKey.startsWith( "^" ) )
654         {
655             setValueForBinding( aValue, aKey.substring(1) );
656             return;
657         }
658     	EOKeyValueCodingSupport.takeValueForKey( this, aValue, aKey );
659     }
660 
661     public Object storedValueForKey (String aKey)
662     {
663     	return EOKeyValueCodingSupport.storedValueForKey( this, aKey );
664     }
665 
666     public void takeStoredValueForKey (Object aValue, String aKey)
667     {
668     	EOKeyValueCodingSupport.takeStoredValueForKey( this, aValue, aKey );
669     }
670 
671     public Object handleQueryWithUnboundKey (String aKey)
672     {
673     	return EOKeyValueCodingSupport.handleQueryWithUnboundKey( this, aKey );
674     }
675 
676     public void handleTakeValueForUnboundKey (Object aValue, String aKey)
677     {
678     	EOKeyValueCodingSupport.handleTakeValueForUnboundKey( this, aValue, aKey );
679     }
680 
681     public void unableToSetNullForKey (String aKey)
682     {
683     	EOKeyValueCodingSupport.unableToSetNullForKey( this, aKey );
684     }
685 
686     public Object validateTakeValueForKeyPath (Object aValue, String aKey)
687     {
688     	throw new RuntimeException( "Not implemented yet." );
689     }
690 
691 
692 	// Template Processing
693 
694     /***
695     * Takes a template string and a location to begin parsing,
696     * looking only for interesting tags, and calling itself recursively
697     * as necessary.  Returns the index to resume parsing, or -1 if done.
698     */
699     static private int processTemplate(
700         List elements, String template, int index, 
701 		Map bindings, List aLanguageList )
702     	throws java.io.IOException
703     { //System.out.println( "processTemplate: " + index );
704     	if ( template == null ) return -1;
705         
706         int start = index;
707         
708         while ( true )
709         {
710             // search for start of next tag
711             start = template.indexOf( '<', start );
712     
713             if ( start == -1 )
714             {
715                 // if no tags, send output and return
716                 elements.add( new WOStaticElement( template.substring( index ) ) );
717                 return -1;
718             }
719             
720             // search for end of opening tag
721             int end = template.indexOf( ">", start + 1 );
722             if ( end == -1 )
723             {
724                 // if no end to tag
725                 throw new RuntimeException( "No end to tag: "
726                     + template.substring( start ) );
727             }
728     
729             boolean hasBody = true;	
730             if ( template.charAt( end - 1 ) == '/' )
731             {
732                 // tag is standalone - no body
733                 end = end - 1;
734                 hasBody = false;
735             }
736             
737             // search for name of tag
738             int endName = start + 1;
739             while ( endName < end ) 
740             {
741                 if ( Character.isWhitespace( 
742                     template.charAt(endName) ) ) break;
743                 endName++;
744             }
745             
746             String name = template.substring( start + 1, endName );
747     
748             if ( name.toLowerCase().startsWith( OPEN_TAG ) )
749             {
750                 // add the contents before the tag
751                 //System.out.println( index + " : " + start + " : " + hasBody );                
752                 elements.add( new WOStaticElement( template.substring( index, start ) ) );
753     
754                 // interesting tag; parse parameters
755                 Map params = new HashMap( 5 ); // arbitrary init length			
756                 if ( endName < end )
757                 {
758                     // delimit by whitespace
759                     StringTokenizer tokens = new StringTokenizer(
760                         template.substring( endName+1, end ) );
761                     int equals;
762                     String token;
763                     String value;
764                     while ( tokens.hasMoreTokens() )
765                     {
766                         token = tokens.nextToken();
767                         equals = token.indexOf( '=' );
768                         if ( equals != -1 )
769                         {
770                             value = token.substring( equals+1 );
771     
772                             if ( value.startsWith( "\"" ) )
773                             {
774                                 // handle spaces within parameter names
775                                 while ( ! value.endsWith( "\"" ) ) 
776                                 { 
777                                     value = value + " " + tokens.nextToken();   
778                                 }
779     
780                                 // strip quotation marks
781                                 if ( value.endsWith( "\"" ) )
782                                 {
783                                     value = value.substring( 1, value.length()-1 );
784                                 }
785                             }
786     
787                             // register key with specified value
788                             params.put( 
789                                 token.substring( 0, equals ).toLowerCase(), value );
790                                 
791                         }
792                         else
793                         {
794                             // no value found, register the key name
795                             params.put( token.toLowerCase(), "" );
796                         }
797                     }
798                 }
799             
800                 index = end + (hasBody?1:2);
801                 
802                 WOElement body = null;
803                 if ( hasBody )
804                 {
805                     List childElements = new LinkedList();
806                     
807                     index = processTemplate( 
808                         childElements, template, index, 
809                         bindings, aLanguageList );
810                     start = index;
811                         
812                     if ( index == -1 )
813                     {
814                         throw new RuntimeException( 
815                             "No closing tag found: " + template.substring( end ) );
816                     }
817                     
818                     if ( childElements.size() == 1 )
819                     {
820                         body = (WOElement) childElements.get(0);
821                     }
822                     else
823                     {
824                         body = new WOParentElement( childElements );
825                     }
826                 }
827                 
828                 WOElement element = null;
829                 String nameProperty = (String) params.get( NAME_KEY );
830                 NSDictionary original = (NSDictionary) bindings.get( nameProperty );
831                 //System.out.println( nameProperty + " : " + associations );                
832                 if ( original == null ) 
833                 {
834                     original = NSDictionary.EmptyDictionary;
835                     System.err.println( "No associations for: " + nameProperty );
836                     System.err.println( bindings );
837                 }
838                 
839                 NSDictionary associations = new NSMutableDictionary( original );
840                 String elementClass = (String) associations.remove( WOApplication.ELEMENT_CLASS );
841             
842                 WOApplication application = WOApplication.application();
843                 element = application.dynamicElementWithName(
844                     elementClass, associations, body, aLanguageList );
845                 if ( element == null )
846                 {
847                     // unable to create element: show assocs in static element
848                     element = new WOStaticElement( associations.toString() );
849                 }
850 
851                 //System.out.println( element );                
852                 elements.add( element );
853                 
854                 if ( !hasBody )
855                 {
856                     start = end + 2;
857                 }
858             }
859             else
860             if ( name.toLowerCase().startsWith( CLOSE_TAG ) )
861             {
862                 // add any contents before the tag
863                 elements.add( new WOStaticElement( template.substring( index, start ) ) );
864     
865 //                return end + name.length() + 1; // "<" + ">" - 1 = 1
866                 return end + (hasBody?1:2); // "<" + ">" - 1 = 1
867             }
868             else
869             {
870                 // tag not interesting: continue
871                 start = end + (hasBody?1:2);
872             }
873         }
874     }
875 
876 
877     // Utility Methods
878     
879     static private void rewriteTag( String tagName, 
880     	Map properties, String body, StringBuffer context )
881     	throws java.io.IOException
882 	{
883 		context.append( "<"+tagName );
884 		Iterator it = properties.keySet().iterator();
885 		String key;
886 		while ( it.hasNext() )
887 		{
888 			key = (String) it.next();
889 	    	context.append( " " + key + "=\"" + properties.get( key ) + "\"" );
890 		}
891 		
892 		if ( body == null )
893 		{
894 			context.append( "/>" );
895 			return;
896 		}
897 		
898 		context.append( ">" + body + "</" + tagName + ">" );
899 	}
900 	
901     private String justTheClassName()
902     {
903     	String className = getClass().getName();
904 	    int index = className.lastIndexOf( "." );
905 	    if ( index == -1 ) return className;
906 	    return className.substring( index+1 );
907     }
908 
909     private String justTheResourcePath()
910     {
911         int last = -1;
912         char[] src = getClass().getName().toCharArray();
913         char[] dst = new char[ src.length ];
914         for ( int i = 0; i < src.length; i++ )
915         {
916             if ( src[i] == '.' )
917             {
918                 dst[i] = '/';
919                 last = i;
920             }
921             else
922             {
923                 dst[i] = src[i];
924             }
925         }
926         if ( last == -1 ) return null;
927         return new String( dst, 0, last );
928     }
929 
930     private String readTemplateResource( String name, String framework, String suffix, NSArray languages )
931     {
932         if ( name == null ) return null;
933         name = name + DIRECTORY_SUFFIX + '/' + name + suffix;
934         InputStream is = null;
935         if ( isCachingEnabled() )
936         {
937             byte[] data = WOApplication.application().resourceManager().bytesForResourceNamed( 
938                 name, framework, languages );
939             if ( data != null )
940             {
941                 is = new ByteArrayInputStream( data );
942             }
943         }
944         else
945         {
946             is = WOApplication.application().resourceManager().inputStreamForResourceNamed( 
947                 name, framework, languages );
948         }
949         if ( is == null ) 
950         {
951             System.err.println( "No resources found for: " + name );            
952             return null;
953         }
954         
955         // try to autodetect encoding
956         String encoding = "ISO8859_1";
957         try
958         {
959             byte[] header = new byte[4];
960             is = new PushbackInputStream( is, 4 );
961             is.read( header );
962             if ( header[0] < 33 || header[0] > 126 ) 
963             {
964                 // if any funny characters, presume UTF-16
965                 encoding = "UTF-16";
966                 if (!( header[1] < 33 || header[1] > 126 ))
967                 {
968                     // if second character is valid, presume UTF-8
969                     encoding = "UTF-8";
970                 }
971             }
972             // check byte-order-mark
973             if (header[0] == 0xef && header[1] == 0xbb && header[2] == 0xbf) // utf-8
974             {
975                 encoding = "UTF-8";
976             }
977             else
978             if (header[0] == 0xfe && header[1] == 0xff) // utf-16
979             {
980                 encoding = "UTF-16";
981             }
982             else
983             if (header[0] == 0 && header[1] == 0 && header[2] == 0xfe && header[3] == 0xff) // ucs-4
984             {
985                 encoding = "UCS-4"; //??
986             }                
987             else
988             if (header[0] == 0xff && header[1] == 0xfe) // ucs-2le, ucs-4le, and 
989             {
990                 encoding = "UCS-16le"; //??
991             }
992             // put back the header
993             ((PushbackInputStream)is).unread( header );
994         }
995         catch ( Throwable t )
996         {
997             t.printStackTrace();
998             System.err.println( 
999                 "Error while autodetecting encoding: should never happen" );
1000         }
1001 
1002         try
1003         {
1004             String line;
1005             StringBuffer buf = new StringBuffer();
1006             BufferedReader r = new BufferedReader( new InputStreamReader( is, encoding ) );
1007             while ( ( line = r.readLine() ) != null )
1008             {
1009                 buf.append( line );
1010                 buf.append( '\n' );
1011             }
1012             is.close(); // release the resource
1013             return buf.toString();
1014         }
1015         catch ( IOException exc )
1016         {
1017             System.err.println( "Error while reading: " + name );            
1018             exc.printStackTrace();
1019             return null;
1020         }
1021     }
1022     
1023     // Declaration Parsing
1024 
1025     /***
1026     * Parses the declarations in the specified content and returns a map of element names 
1027     * to maps of attribute names to WOAssociations.
1028     */
1029     private static NSDictionary processDeclaration( String content )
1030     {
1031 	    int index;
1032     	NSMutableDictionary result = new NSMutableDictionary();
1033 	    
1034 	    // strip out comments
1035 	    StringBuffer stripped = new StringBuffer();
1036 	    try
1037 	    {
1038             LineNumberReader reader = 
1039 	    		new LineNumberReader( new StringReader( content ) );
1040             String line;
1041             while ( ( line = reader.readLine() ) != null )
1042             {
1043                 index = line.indexOf("//");
1044                 while (index > -1) {
1045                     //(chochos) This used to truncate lines with quoted URLs
1046                     //in them. We have to check that the "//" is not inside quotes.
1047                     boolean quoted = false;
1048                     if (index > 0) {
1049                         for (int _position = 0; _position < index; _position++)
1050                             if (line.charAt(_position) == '"')
1051                                 quoted = !quoted;
1052                     }
1053                     if (!quoted) {
1054                         line = line.substring( 0, index );
1055                         index = -1;
1056                     } else {
1057                         //if we didn't truncate the line it's because the //
1058                         //were quoted. let's look for more, and check if they're not quoted...
1059                         index = line.indexOf("\"", index);
1060                         if (index > 0) {
1061                             index = line.indexOf("//", index);
1062                         }
1063                     }
1064                 }
1065                 stripped.append( line );
1066             }
1067 		}
1068 		catch ( IOException exc )
1069 		{
1070 			throw new RuntimeException( 
1071 				"Error while stripping comments from declaration: " + stripped );
1072 		}
1073         while ( (index = stripped.toString().indexOf( "/*" )) != -1 )
1074         {
1075             int j = stripped.toString().indexOf( "*/", index+1 );
1076             if ( j == -1 ) break;
1077             stripped.delete( index, j+2 );
1078         }
1079 	    
1080 	    String token;
1081 	    StringTokenizer tokens = new StringTokenizer( stripped.toString(), "{}", true );
1082 	    while ( tokens.hasMoreTokens() )
1083 	    {
1084 	    	token = tokens.nextToken();
1085 	    
1086 	    	// next token is the name and class
1087 		    String name, cl;
1088 			index = token.indexOf( ":" );
1089 			if ( index > -1 )
1090 			{
1091 			    name = token.substring( 0, index ).trim();
1092 				cl = token.substring( index+1 ).trim();
1093 			}
1094 			else
1095 			{
1096                 System.err.println( "Could not parse declaration:" );                    
1097                 System.err.println( content );                    
1098 				throw new RuntimeException( 
1099 					"Could not parse declaration: " + token );
1100 			}    
1101 
1102 			// next token is the declaration for the name and class
1103 			if ( ! tokens.hasMoreTokens() )
1104 			{
1105                 System.err.println( "Could not find associations for declaration:" );                    
1106                 System.err.println( content );                    
1107 				throw new RuntimeException( 
1108 					"Could not find associations for declaration: " + name );
1109 			}
1110 			
1111 	    	token = tokens.nextToken();
1112             if ( token.equals( "{" ) ) 
1113             {
1114                 if ( !tokens.hasMoreTokens() ) throw new RuntimeException(
1115                     "Error parsing declaration: expected { but found: '" + token + "'" );
1116                 token = tokens.nextToken();
1117             }
1118             
1119             NSMutableDictionary associations = new NSMutableDictionary();
1120             
1121             if ( !token.equals( "}" ) ) 
1122             {
1123                 String line, key, value; 
1124                 StringTokenizer lines = 
1125                     new StringTokenizer( token, ";" );
1126                 while ( lines.hasMoreElements() )
1127                 {
1128                     line = lines.nextToken(); 
1129                     index = line.indexOf( "=" );
1130                     if ( index > -1 )
1131                     {
1132                         if ( line.length() == index+ 1 ) line += " ";
1133                         key = line.substring( 0, index ).trim();
1134                         value = line.substring( index+1 ).trim();
1135                     }
1136                     else
1137                     {
1138                         // not a valid key: skip
1139                         key = null;
1140                         value = null;
1141                     }
1142     
1143                     if ( key != null )
1144                     {
1145                         // if in quotation marks				
1146                         if ( ( value.startsWith( "\"" ) ) && ( value.endsWith( "\"" ) ) )
1147                         {
1148                             // it's a constant value association
1149                             value = value.substring( 1, value.length()-1 );
1150                             associations.put( key, 
1151                                 WOAssociation.associationWithValue( value ) );
1152                         }
1153                         else
1154                         if ( value.equalsIgnoreCase( "true" ) || value.equalsIgnoreCase( "false" ) )
1155                         {
1156                             //HACK: needed to be compatible with woextensions
1157                             // apparently true and false are allowed without quotes
1158                             associations.put( key, 
1159                                 WOAssociation.associationWithValue( value ) );
1160                         }
1161                         else
1162                         {
1163                             //HACK: needed to be compatible with woextensions:
1164                             // apparently a standalone integer is allowed without quotes.
1165                             try 
1166                             {
1167                                 Integer.parseInt( value ); // does it parse?
1168                                 associations.put( key, 
1169                                     WOAssociation.associationWithValue( value ) );
1170                             }
1171                             catch ( NumberFormatException nfe )
1172                             {
1173                                 // did not parse:
1174                                 // it's a key path association
1175                                 associations.put( key, 
1176                                     WOAssociation.associationWithKeyPath( value ) );
1177                             }
1178                         }
1179                     }
1180                 }
1181                 if ( tokens.hasMoreTokens() ) 
1182                 {
1183                     token = tokens.nextToken();
1184                     if ( !token.equals( "}" ) ) throw new RuntimeException( 
1185                         "Error parsing declaration: expected } but found: '" + token + "'" );
1186                 }
1187             }
1188             associations.put( WOApplication.ELEMENT_CLASS, cl ); // store classname
1189             result.put( name, associations );
1190             
1191 	    }
1192         //System.out.println( "processDeclaration: " + result );
1193 	    return result;
1194     }
1195 
1196 }
1197 
1198 /*
1199  * $Log$
1200  * Revision 1.2  2006/02/19 01:44:02  cgruber
1201  * Add xmlrpc files
1202  * Remove jclark and replace with dom4j and javax.xml.sax stuff
1203  * Re-work dependencies and imports so it all compiles.
1204  *
1205  * Revision 1.1  2006/02/16 13:22:22  cgruber
1206  * Check in all sources in eclipse-friendly maven-enabled packages.
1207  *
1208  * Revision 1.32  2003/08/07 00:15:14  chochos
1209  * general cleanup (mostly removing unused imports)
1210  *
1211  * Revision 1.31  2003/07/24 00:23:21  chochos
1212  * fixed problem with parsing wod files that have //-type comments. Quotes URL's would be truncated.
1213  *
1214  * Revision 1.30  2003/03/28 15:33:11  mpowers
1215  * Now using a PushBackInputStream for auto detection of content encoding.
1216  * No longer relying on markSupported() since jar input streams don't have it.
1217  *
1218  * Revision 1.29  2003/03/03 16:41:52  mpowers
1219  * Bad characters in cvs log.
1220  *
1221  * Revision 1.28  2003/03/03 16:37:35  mpowers
1222  * Better handling for string encodings.
1223  * Now trying to autodetect unicode-formatted templates and declarations.
1224  * Now handlings block-style comments in declarations.
1225  *
1226  * Revision 1.27  2003/01/28 19:33:51  mpowers
1227  * Implemented the rest of WOResourceManager.
1228  * Implemented support for java-style i18n.
1229  * Components now use the resource manager to load templates.
1230  *
1231  * Revision 1.26  2003/01/24 20:13:22  mpowers
1232  * Now accepting immutable NSDictionary in constructor, not Map.
1233  *
1234  * Revision 1.25  2003/01/21 17:53:45  mpowers
1235  * Now correctly reporting error for missing bindings.
1236  *
1237  * Revision 1.24  2003/01/20 17:50:11  mpowers
1238  * Caught a loop condition when same declaration was used twice.
1239  *
1240  * Revision 1.23  2003/01/19 22:33:25  mpowers
1241  * Fixed problems with classpath and dynamic class loading.
1242  * Dynamic elements now pass on ensureAwakeInContext.
1243  * Parser how handles <standalone/> tags.
1244  *
1245  * Revision 1.22  2003/01/17 22:55:09  mpowers
1246  * Straighted out the parent binding issue (I think).
1247  * Fixes for woextensions compatibility.
1248  *
1249  * Revision 1.21  2003/01/17 20:34:57  mpowers
1250  * Better handling for components and parents in the context's element stack.
1251  *
1252  * Revision 1.19  2003/01/17 15:32:22  mpowers
1253  * Changes to better support generic elements and containers.
1254  * Now preserving newlines in templates.
1255  *
1256  * Revision 1.17  2003/01/16 22:47:30  mpowers
1257  * Compatibility changes to support compiling woextensions source.
1258  * (34 out of 56 classes compile!)
1259  *
1260  * Revision 1.15  2003/01/16 15:50:43  mpowers
1261  * More robust declaration parsing.
1262  * Subcomponents are now supported.
1263  * dynamicElementWithName can now return subcomponents.
1264  *
1265  * Revision 1.14  2003/01/15 19:50:49  mpowers
1266  * Fixed issues with WOSession and Serializable.
1267  * Can now persist sessions between classloaders (hot swap of class impls).
1268  *
1269  * Revision 1.13  2003/01/15 14:33:48  mpowers
1270  * Refactoring: element id handling is now confined to WOParentElement.
1271  * Other elements/components should not have to do element id incrementing.
1272  *
1273  * Revision 1.12  2003/01/14 16:05:12  mpowers
1274  * Removed extraneous printlns.
1275  *
1276  * Revision 1.11  2003/01/13 22:24:25  mpowers
1277  * Request-response cycle is working with session and page persistence.
1278  *
1279  * Revision 1.10  2003/01/10 19:33:28  mpowers
1280  * Added contextID for the component url generation.
1281  *
1282  * Revision 1.9  2003/01/10 19:16:40  mpowers
1283  * Implemented support for page caching.
1284  *
1285  * Revision 1.8  2003/01/09 21:16:48  mpowers
1286  * Bringing request-response cycle more into conformance.
1287  *
1288  * Revision 1.7  2003/01/09 16:13:55  mpowers
1289  * Implemented WOComponentRequestHandler:
1290  * Bringing the request-response cycle more into conformance.
1291  *
1292  * Revision 1.6  2002/12/20 22:56:33  mpowers
1293  * Reimplemented the template parsing again.
1294  * Nested components are now correctly parsed.
1295  * ElementID numbering is now working.
1296  *
1297  * Revision 1.3  2002/12/18 14:12:38  mpowers
1298  * Support for differentiated request handlers.
1299  * Support url generation for WOContext and WORequest.
1300  *
1301  * Revision 1.2  2002/11/07 18:52:33  mpowers
1302  * New components courtesy of ezamudio@nasoft.com.  Many thanks!
1303  *
1304  * Revision 1.1.1.1  2000/12/21 15:53:01  mpowers
1305  * Contributing wotonomy.
1306  *
1307  * Revision 1.2  2000/12/20 16:25:49  michael
1308  * Added log to all files.
1309  *
1310  *
1311  */
1312