d118bae7b03124f7c185f0de1141e00381ec8dca
[core.git] / inc / classes / main / template / menu / class_MenuTemplateEngine.php
1 <?php
2 /**
3  * A Menu template engine class
4  *
5  * @author              Roland Haeder <webmaster@shipsimu.org>
6  * @version             0.0.0
7  * @copyright   Copyright (c) 2007, 2008 Roland Haeder, 2009 - 2015 Core Developer Team
8  * @license             GNU GPL 3.0 or any newer version
9  * @link                http://www.shipsimu.org
10  *
11  * This program is free software: you can redistribute it and/or modify
12  * it under the terms of the GNU General Public License as published by
13  * the Free Software Foundation, either version 3 of the License, or
14  * (at your option) any later version.
15  *
16  * This program is distributed in the hope that it will be useful,
17  * but WITHOUT ANY WARRANTY; without even the implied warranty of
18  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19  * GNU General Public License for more details.
20  *
21  * You should have received a copy of the GNU General Public License
22  * along with this program. If not, see <http://www.gnu.org/licenses/>.
23  */
24 class MenuTemplateEngine extends BaseTemplateEngine implements CompileableTemplate {
25         /**
26          * Main nodes in the XML tree ('menu' is ignored)
27          */
28         private $mainNodes = array(
29                 'block-list',
30         );
31
32         /**
33          * Sub nodes in the XML tree
34          */
35         private $subNodes = array(
36                 'entry-list',
37                 'entry',
38                 'entry-id',
39                 'entries-content',
40                 'block-header',
41                 'block-footer',
42                 'footer-id',
43                 'footer-class',
44                 'footer-text',
45                 'block',
46                 'title',
47                 'title-id',
48                 'title-class',
49                 'title-text',
50                 'design',
51                 'text',
52                 'advert',
53                 'anchor',
54                 'anchor-id',
55                 'anchor-text',
56                 'anchor-title',
57                 'anchor-href',
58         );
59
60         /**
61          * Variables for a menu entry
62          */
63         private $menuEntryVariables = array(
64                 // List entry
65                 'entry_id',
66                 // Anchor
67                 'anchor-id',
68                 'anchor-text',
69                 'anchor-title',
70                 'anchor-href',
71         );
72
73         /**
74          * Variables for a menu block
75          */
76         private $menuBlockVariables = array(
77                 // Title
78                 'title_id',
79                 'title_class',
80                 'title_text',
81                 // Content is taken from menuEntries
82                 // Footer
83                 'footer_id',
84                 'footer_class',
85                 'footer_text',
86         );
87
88         /**
89          * Rendered menu entries
90          */
91         private $menuEntries = array();
92
93         /**
94          * Rendered menu blocks
95          */
96         private $menuBlocks = array();
97
98         /**
99          * Menu instance
100          */
101         private $menuInstance = NULL;
102
103         /**
104          * Current main node
105          */
106         private $curr = array();
107
108         /**
109          * Content from dependency
110          */
111         private $dependencyContent = array();
112
113         /**
114          * Protected constructor
115          *
116          * @return      void
117          */
118         protected function __construct () {
119                 // Call parent constructor
120                 parent::__construct(__CLASS__);
121         }
122
123         /**
124          * Creates an instance of the class TemplateEngine and prepares it for usage
125          *
126          * @param       $menuInstance                   A RenderableMenu instance
127          * @return      $templateInstance               An instance of TemplateEngine
128          * @throws      BasePathIsEmptyException                If the provided $templateBasePath is empty
129          * @throws      InvalidBasePathStringException  If $templateBasePath is no string
130          * @throws      BasePathIsNoDirectoryException  If $templateBasePath is no
131          *                                                                                      directory or not found
132          * @throws      BasePathReadProtectedException  If $templateBasePath is
133          *                                                                                      read-protected
134          */
135         public static final function createMenuTemplateEngine (RenderableMenu $menuInstance) {
136                 // Get a new instance
137                 $templateInstance = new MenuTemplateEngine();
138
139                 // Get the application instance from registry
140                 $applicationInstance = Registry::getRegistry()->getInstance('app');
141
142                 // Determine base path
143                 $templateBasePath = $templateInstance->getConfigInstance()->getConfigEntry('application_base_path') . $applicationInstance->getRequestInstance()->getRequestElement('app') . '/';
144
145                 // Is the base path valid?
146                 if (empty($templateBasePath)) {
147                         // Base path is empty
148                         throw new BasePathIsEmptyException($templateInstance, self::EXCEPTION_UNEXPECTED_EMPTY_STRING);
149                 } elseif (!is_string($templateBasePath)) {
150                         // Is not a string
151                         throw new InvalidBasePathStringException(array($templateInstance, $templateBasePath), self::EXCEPTION_INVALID_STRING);
152                 } elseif (!is_dir($templateBasePath)) {
153                         // Is not a path
154                         throw new BasePathIsNoDirectoryException(array($templateInstance, $templateBasePath), self::EXCEPTION_INVALID_PATH_NAME);
155                 } elseif (!is_readable($templateBasePath)) {
156                         // Is not readable
157                         throw new BasePathReadProtectedException(array($templateInstance, $templateBasePath), self::EXCEPTION_READ_PROTECED_PATH);
158                 }
159
160                 // Set the base path
161                 $templateInstance->setTemplateBasePath($templateBasePath);
162
163                 // Set template extensions
164                 $templateInstance->setRawTemplateExtension($templateInstance->getConfigInstance()->getConfigEntry('raw_template_extension'));
165                 $templateInstance->setCodeTemplateExtension($templateInstance->getConfigInstance()->getConfigEntry('menu_template_extension'));
166
167                 // Absolute output path for compiled templates
168                 $templateInstance->setCompileOutputPath($templateInstance->getConfigInstance()->getConfigEntry('base_path') . $templateInstance->getConfigInstance()->getConfigEntry('compile_output_path'));
169
170                 // Set the menu instance
171                 $templateInstance->setMenuInstance($menuInstance);
172
173                 // Init a variable stacker
174                 $stackInstance = ObjectFactory::createObjectByConfiguredName('menu_stacker_class');
175
176                 // Set it
177                 $templateInstance->setStackInstance($stackInstance);
178
179                 // Return the prepared instance
180                 return $templateInstance;
181         }
182
183         /**
184          * Load a specified menu template into the engine
185          *
186          * @param       $template       The menu template we shall load which is
187          *                                              located in 'menu' by default
188          * @return      void
189          */
190         public function loadMenuTemplate ($template) {
191                 // Set template type
192                 $this->setTemplateType($this->getConfigInstance()->getConfigEntry('menu_template_type'));
193
194                 // Load the special template
195                 $this->loadTemplate($template);
196         }
197
198         /**
199          * Getter for current main node
200          *
201          * @return      $currMainNode   Current main node
202          */
203         public final function getCurrMainNode () {
204                 return $this->curr['main_node'];
205         }
206
207         /**
208          * Setter for current main node
209          *
210          * @param       $element                Element name to set as current main node
211          * @return      $currMainNode   Current main node
212          */
213         private final function setCurrMainNode ($element) {
214                 $this->curr['main_node'] = (string) $element;
215         }
216
217         /**
218          * Getter for main node array
219          *
220          * @return      $mainNodes      Array with valid main node names
221          */
222         public final function getMainNodes () {
223                 return $this->mainNodes;
224         }
225
226         /**
227          * Getter for sub node array
228          *
229          * @return      $subNodes       Array with valid sub node names
230          */
231         public final function getSubNodes () {
232                 return $this->subNodes;
233         }
234
235         /**
236          * Handles the start element of an XML resource
237          *
238          * @param       $resource               XML parser resource (currently ignored)
239          * @param       $element                The element we shall handle
240          * @param       $attributes             All attributes
241          * @return      void
242          * @throws      InvalidXmlNodeException         If an unknown/invalid XML node name was found
243          */
244         public function startElement ($resource, $element, array $attributes) {
245                 // Initial method name which will never be called...
246                 $methodName = 'initMenu';
247
248                 // Make the element name lower-case
249                 $element = strtolower($element);
250
251                 // Is the element a main node?
252                 //* DEBUG: */ echo "START: &gt;".$element."&lt;<br />\n";
253                 if (in_array($element, $this->getMainNodes())) {
254                         // Okay, main node found!
255                         $methodName = 'start' . $this->convertToClassName($element);
256
257                         // Set it
258                         $this->setCurrMainNode($element);
259                 } elseif (in_array($element, $this->getSubNodes())) {
260                         // Sub node found
261                         $methodName = 'start' . $this->convertToClassName($element);
262                 } elseif ($element != 'menu') {
263                         // Invalid node name found
264                         throw new InvalidXmlNodeException(array($this, $element, $attributes), XmlParser::EXCEPTION_XML_NODE_UNKNOWN);
265                 }
266
267                 // Call method
268                 //* DEBUG: */ echo "call: ".$methodName."<br />\n";
269                 call_user_func_array(array($this, $methodName), $attributes);
270         }
271
272         /**
273          * Ends the main or sub node by sending out the gathered data
274          *
275          * @param       $resource       An XML resource pointer (currently ignored)
276          * @param       $nodeName       Name of the node we want to finish
277          * @return      void
278          * @throws      XmlNodeMismatchException        If current main node mismatches the closing one
279          */
280         public function finishElement ($resource, $nodeName) {
281                 // Make all lower-case
282                 $nodeName = strtolower($nodeName);
283
284                 // Does this match with current main node?
285                 //* DEBUG: */ echo "END: &gt;".$nodeName."&lt;<br />\n";
286                 if (($nodeName != $this->getCurrMainNode()) && (in_array($nodeName, $this->getMainNodes()))) {
287                         // Did not match!
288                         throw new XmlNodeMismatchException (array($this, $nodeName, $this->getCurrMainNode()), XmlParser::EXCEPTION_XML_NODE_MISMATCH);
289                 } // END - if
290
291                 // Construct method name
292                 $methodName = 'finish' . $this->convertToClassName($nodeName);
293
294                 // Call the corresponding method
295                 //* DEBUG: */ echo "call: ".$methodName."<br />\n";
296                 call_user_func_array(array($this, $methodName), array());
297         }
298
299         /**
300          * Currently not used
301          *
302          * @param       $resource               XML parser resource (currently ignored)
303          * @param       $characters             Characters to handle
304          * @return      void
305          * @todo        Find something useful with this!
306          */
307         public function characterHandler ($resource, $characters) {
308                 // Trim all spaces away
309                 $characters = trim($characters);
310
311                 // Is this string empty?
312                 if (empty($characters)) {
313                         // Then skip it silently
314                         return;
315                 } // END - if
316
317                 // Assign the found characters to variable and use the last entry from
318                 // stack as the name
319                 parent::assignVariable($this->getStackInstance()->getNamed('current_node'), $characters);
320         }
321
322         /**
323          * Handles the template dependency for given node
324          *
325          * @param       $node                                   The node we should load a dependency template
326          * @param       $templateDependency             A template to load to satisfy dependencies
327          * @return      void
328          */
329         private function handleTemplateDependency ($node, $templateDependency) {
330                 // Is the template dependency set?
331                 if ((!empty($templateDependency)) && (!isset($this->dependencyContent[$node]))) {
332                         // Get a temporay menu template instance
333                         $templateInstance = ObjectFactory::createObjectByConfiguredName('menu_template_class', array($this->getMenuInstance()));
334
335                         // Then load it
336                         $templateInstance->loadMenuTemplate($templateDependency);
337
338                         // Parse the XML content
339                         $templateInstance->renderXmlContent();
340
341                         // Save the parsed raw content in our dependency array
342                         $this->dependencyContent[$node] = $templateInstance->getRawTemplateData();
343                 } // END - if
344         }
345
346         /**
347          * Intializes the menu
348          *
349          * @param       $templateDependency             A template to load to satisfy dependencies
350          * @return      void
351          * @todo        Add cache creation here
352          */
353         private function initMenu ($templateDependency = '') {
354                 // Get web template engine
355                 $this->setTemplateInstance(ObjectFactory::createObjectByConfiguredName('html_template_class', array($this->getApplicationInstance())));
356
357                 // Handle the dependency template
358                 $this->handleTemplateDependency('menu', $templateDependency);
359
360                 // Push the node name on the stacker
361                 $this->getStackInstance()->pushNamed('current_node', 'menu');
362         }
363
364         /**
365          * Starts the menu entries
366          *
367          * @param       $templateDependency             A template to load to satisfy dependencies
368          * @return      void
369          */
370         private function startEntryList () {
371                 // Push the node name on the stacker
372                 $this->getStackInstance()->pushNamed('current_node', 'entry-list');
373         }
374
375         /**
376          * Starts the menu block header
377          *
378          * @return      void
379          */
380         private function startBlockHeader () {
381                 // Push the node name on the stacker
382                 $this->getStackInstance()->pushNamed('current_node', 'block-header');
383         }
384
385         /**
386          * Starts the menu block footer
387          *
388          * @return      void
389          */
390         private function startBlockFooter () {
391                 // Push the node name on the stacker
392                 $this->getStackInstance()->pushNamed('current_node', 'block-footer');
393         }
394
395         /**
396          * Starts the menu property 'block-list'
397          *
398          * @return      void
399          */
400         private function startBlockList () {
401                 // Push the node name on the stacker
402                 $this->getStackInstance()->pushNamed('current_node', 'block-list');
403         }
404
405         /**
406          * Starts the menu property 'block'
407          *
408          * @return      void
409          */
410         private function startBlock () {
411                 // Push the node name on the stacker
412                 $this->getStackInstance()->pushNamed('current_node', 'block');
413         }
414
415         /**
416          * Starts the menu property 'title'
417          *
418          * @return      void
419          */
420         private function startTitle () {
421                 // Push the node name on the stacker
422                 $this->getStackInstance()->pushNamed('current_node', 'title');
423         }
424
425         /**
426          * Starts the menu property 'title-id'
427          *
428          * @return      void
429          */
430         private function startTitleId () {
431                 // Push the node name on the stacker
432                 $this->getStackInstance()->pushNamed('current_node', 'title-id');
433         }
434
435         /**
436          * Starts the menu property 'title-class'
437          *
438          * @return      void
439          */
440         private function startTitleClass () {
441                 // Push the node name on the stacker
442                 $this->getStackInstance()->pushNamed('current_node', 'title-class');
443         }
444
445         /**
446          * Starts the menu property 'title-text'
447          *
448          * @return      void
449          */
450         private function startTitleText () {
451                 // Push the node name on the stacker
452                 $this->getStackInstance()->pushNamed('current_node', 'title-text');
453         }
454
455         /**
456          * Starts the menu property 'entry'
457          *
458          * @return      void
459          */
460         private function startEntry () {
461                 // Push the node name on the stacker
462                 $this->getStackInstance()->pushNamed('current_node', 'entry');
463         }
464
465         /**
466          * Starts the menu property 'entry-id'
467          *
468          * @return      void
469          */
470         private function startEntryId () {
471                 // Push the node name on the stacker
472                 $this->getStackInstance()->pushNamed('current_node', 'entry-id');
473         }
474
475         /**
476          * Starts the menu property 'anchor'
477          *
478          * @return      void
479          */
480         private function startAnchor () {
481                 // Push the node name on the stacker
482                 $this->getStackInstance()->pushNamed('current_node', 'anchor');
483         }
484
485         /**
486          * Starts the menu property 'anchor-id'
487          *
488          * @return      void
489          */
490         private function startAnchorId () {
491                 // Push the node name on the stacker
492                 $this->getStackInstance()->pushNamed('current_node', 'anchor-id');
493         }
494
495         /**
496          * Starts the menu property 'anchor-text'
497          *
498          * @return      void
499          */
500         private function startAnchorText () {
501                 // Push the node name on the stacker
502                 $this->getStackInstance()->pushNamed('current_node', 'anchor-text');
503         }
504
505         /**
506          * Starts the menu property 'anchor-title'
507          *
508          * @return      void
509          */
510         private function startAnchorTitle () {
511                 // Push the node name on the stacker
512                 $this->getStackInstance()->pushNamed('current_node', 'anchor-title');
513         }
514
515         /**
516          * Starts the menu property 'anchor-href'
517          *
518          * @return      void
519          */
520         private function startAnchorHref () {
521                 // Push the node name on the stacker
522                 $this->getStackInstance()->pushNamed('current_node', 'anchor-href');
523         }
524
525         /**
526          * Starts the menu property 'footer-id'
527          *
528          * @return      void
529          */
530         private function startFooterId () {
531                 // Push the node name on the stacker
532                 $this->getStackInstance()->pushNamed('current_node', 'footer-id');
533         }
534
535         /**
536          * Starts the menu property 'footer-class'
537          *
538          * @return      void
539          */
540         private function startFooterClass () {
541                 // Push the node name on the stacker
542                 $this->getStackInstance()->pushNamed('current_node', 'footer-class');
543         }
544
545         /**
546          * Starts the menu property 'footer-text'
547          *
548          * @return      void
549          */
550         private function startFooterText () {
551                 // Push the node name on the stacker
552                 $this->getStackInstance()->pushNamed('current_node', 'footer-text');
553         }
554
555         /**
556          * Finishes the title node by added another template to the menu
557          *
558          * @return      void
559          */
560         private function finishTitle () {
561                 // Pop the last entry
562                 $this->getStackInstance()->popNamed('current_node');
563         }
564
565         /**
566          * Finishes the title-id node by
567          *
568          * @return      void
569          */
570         private function finishTitleId () {
571                 // Pop the last entry
572                 $this->getStackInstance()->popNamed('current_node');
573         }
574
575         /**
576          * Finishes the title-class node
577          *
578          * @return      void
579          */
580         private function finishTitleClass () {
581                 // Pop the last entry
582                 $this->getStackInstance()->popNamed('current_node');
583         }
584
585         /**
586          * Finishes the title-class node
587          *
588          * @return      void
589          */
590         private function finishTitleText () {
591                 // Pop the last entry
592                 $this->getStackInstance()->popNamed('current_node');
593         }
594
595         /**
596          * Finishes the footer-text node
597          *
598          * @return      void
599          */
600         private function finishFooterText () {
601                 // Pop the last entry
602                 $this->getStackInstance()->popNamed('current_node');
603         }
604
605         /**
606          * Finishes the footer-class node
607          *
608          * @return      void
609          */
610         private function finishFooterClass () {
611                 // Pop the last entry
612                 $this->getStackInstance()->popNamed('current_node');
613         }
614
615         /**
616          * Finishes the footer-id node
617          *
618          * @return      void
619          */
620         private function finishFooterId () {
621                 // Pop the last entry
622                 $this->getStackInstance()->popNamed('current_node');
623         }
624
625         /**
626          * Finishes the anchor-href node
627          *
628          * @return      void
629          */
630         private function finishAnchorHref () {
631                 // Pop the last entry
632                 $this->getStackInstance()->popNamed('current_node');
633         }
634
635         /**
636          * Finishes the anchor-title node
637          *
638          * @return      void
639          */
640         private function finishAnchorTitle () {
641                 // Pop the last entry
642                 $this->getStackInstance()->popNamed('current_node');
643         }
644
645         /**
646          * Finishes the anchor-text node
647          *
648          * @return      void
649          */
650         private function finishAnchorText () {
651                 // Pop the last entry
652                 $this->getStackInstance()->popNamed('current_node');
653         }
654
655         /**
656          * Finishes the anchor-id node
657          *
658          * @return      void
659          */
660         private function finishAnchorId () {
661                 // Pop the last entry
662                 $this->getStackInstance()->popNamed('current_node');
663         }
664
665         /**
666          * Finishes the anchor node
667          *
668          * @return      void
669          */
670         private function finishAnchor () {
671                 // Pop the last entry
672                 $this->getStackInstance()->popNamed('current_node');
673         }
674
675         /**
676          * Finishes the entry-id node
677          *
678          * @return      void
679          */
680         private function finishEntryId () {
681                 // Pop the last entry
682                 $this->getStackInstance()->popNamed('current_node');
683         }
684
685         /**
686          * Finishes the entry node
687          *
688          * @return      void
689          */
690         private function finishEntry () {
691                 // Pop the last entry
692                 $this->getStackInstance()->popNamed('current_node');
693
694                 // Render this menu entry
695                 $this->renderMenuEntry();
696         }
697
698         /**
699          * Finishes the block node
700          *
701          * @return      void
702          */
703         private function finishBlock () {
704                 // Pop the last entry
705                 $this->getStackInstance()->popNamed('current_node');
706
707                 // Render this menu block
708                 $this->renderMenuBlock();
709         }
710
711         /**
712          * Finishes the block-list node
713          *
714          * @return      void
715          */
716         private function finishBlockList () {
717                 // Pop the last entry
718                 $this->getStackInstance()->popNamed('current_node');
719         }
720
721         /**
722          * Finishes the menu entries
723          *
724          * @return      void
725          */
726         private function finishEntryList () {
727                 // Pop the last entry
728                 $this->getStackInstance()->popNamed('current_node');
729         }
730
731         /**
732          * Finishes the menu block header
733          *
734          * @return      void
735          */
736         private function finishBlockHeader () {
737                 // Pop the last entry
738                 $this->getStackInstance()->popNamed('current_node');
739         }
740
741         /**
742          * Finishes the menu block footer
743          *
744          * @return      void
745          */
746         private function finishBlockFooter () {
747                 // Pop the last entry
748                 $this->getStackInstance()->popNamed('current_node');
749         }
750
751         /**
752          * Finishes the menu
753          *
754          * @return      void
755          */
756         private function finishMenu () {
757                 // Pop the last entry
758                 $this->getStackInstance()->popNamed('current_node');
759         }
760
761         /**
762          * Renders this menu entry, as every block all variables got overwritten
763          * with data from next entry.
764          *
765          * @todo        'anchor_href' may needs expanding with full URL
766          * @return      void
767          */
768         private function renderMenuEntry () {
769                 // Prepare template engine
770                 $templateInstance = $this->prepareTemplateInstance();
771
772                 // Load menu entry template
773                 $templateInstance->loadCodeTemplate('menu_entry');
774
775                 // Copy all variables over to it
776                 foreach ($this->menuEntryVariables as $variableName) {
777                         // Copy variable
778                         $variableValue = $this->readVariable($variableName);
779
780                         // ... into the instance
781                         $templateInstance->assignVariable($variableName, $variableValue);
782                 } // END - foreach
783
784                 // Compile template + variables
785                 $templateInstance->compileTemplate();
786                 $templateInstance->compileVariables();
787
788                 // Remember it here
789                 $this->menuEntries[$this->readVariable('entry_id')] = $templateInstance->getRawTemplateData();
790         }
791
792         /**
793          * Renders this menu block, as next block all data is overwritten with
794          * next block.
795          *
796          * @return      void
797          */
798         private function renderMenuBlock () {
799                 // Init block content
800                 $blockContent = implode('', $this->menuEntries);
801
802                 // Prepare template engine
803                 $templateInstance = $this->prepareTemplateInstance();
804
805                 // Load menu entry template
806                 $templateInstance->loadCodeTemplate('menu_block');
807
808                 // Copy all variables over to it
809                 foreach ($this->menuBlockVariables as $variableName) {
810                         // Copy variable
811                         $variableValue = $this->readVariable($variableName);
812
813                         // ... into the instance
814                         $templateInstance->assignVariable($variableName, $variableValue);
815                 } // END - foreach
816
817                 // Assign block content
818                 $templateInstance->assignVariable('block_content', $blockContent);
819
820                 // Compile template + variables
821                 $templateInstance->compileTemplate();
822                 $templateInstance->compileVariables();
823
824                 // Remember it here
825                 array_push($this->menuBlocks, $templateInstance->getRawTemplateData());
826
827                 // Reset rendered menu entries array
828                 $this->menuEntries = array();
829         }
830
831         /**
832          * "Getter" for menu content
833          *
834          * @return      $menuContent    Returned menu content
835          */
836         public function getMenuContent () {
837                 // Implode menuBlocks
838                 $menuContent = implode('', $this->menuBlocks);
839
840                 // Clean variable
841                 $this->menuBlocks = array();
842
843                 // And return it
844                 return $menuContent;
845         }
846
847         /**
848          * Getter for menu cache file (FQFN)
849          *
850          * @return      $fqfn   Full-qualified file name of the menu cache
851          */
852         public function getMenuCacheFqfn () {
853                 // Get the FQFN ready
854                 $fqfn = sprintf('%s%s%s/%s.%s',
855                         $this->getConfigInstance()->getConfigEntry('base_path'),
856                         $this->getGenericBasePath(),
857                         'menus/_cache',
858                         md5(
859                                 $this->getMenuInstance()->getMenuName() . ':' .
860                                 $this->__toString() . ':' .
861                                 $this->getMenuInstance()->__toString()
862                         ),
863                         $this->getMenuInstance()->getMenuType()
864                 );
865
866                 // Return it
867                 return $fqfn;
868         }
869 }
870
871 // [EOF]
872 ?>