Zend_XmlRpc_Server.xml 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <!-- EN-Revision: 17133 -->
  3. <!-- Reviewed: no -->
  4. <sect1 id="zend.xmlrpc.server">
  5. <title>Zend_XmlRpc_Server</title>
  6. <sect2 id="zend.xmlrpc.server.introduction">
  7. <title>Einführung</title>
  8. <para>
  9. <classname>Zend_XmlRpc_Server</classname> ist als vollständiger XML-RPC Server geplant,
  10. der den <ulink url="http://www.xmlrpc.com/spec">Spezifikationen auf
  11. www.xmlrpc.com</ulink> folgt. Des Weiteren implementiert er die
  12. Methode system.multicall(), welche dem Entwickler erlaubt, mehrere
  13. Anfragen aufzureihen.
  14. </para>
  15. </sect2>
  16. <sect2 id="zend.xmlrpc.server.usage">
  17. <title>Grundlegende Benutzung</title>
  18. <para>
  19. Ein Beispiel der grundlegendsten Benutzung:
  20. </para>
  21. <programlisting language="php"><![CDATA[
  22. $server = new Zend_XmlRpc_Server();
  23. $server->setClass('My_Service_Class');
  24. echo $server->handle();
  25. ]]></programlisting>
  26. </sect2>
  27. <sect2 id="zend.xmlrpc.server.structure">
  28. <title>Server-Struktur</title>
  29. <para>
  30. <classname>Zend_XmlRpc_Server</classname> ist aus einer Vielfalt von Komponenten
  31. zusammengesetzt, die vom Server selbst über Anfrage-, Antwort- und bis hin zu
  32. Fehler-Objekten reicht.
  33. </para>
  34. <para>
  35. Um den <classname>Zend_XmlRpc_Server</classname> zu erstellen, muss der Entwickler dem
  36. Server eine oder mehrere Klassen oder Funktionen durch die Methoden
  37. <methodname>setClass()</methodname> und <methodname>addFunction()</methodname>
  38. hinzufügen.
  39. </para>
  40. <para>
  41. Wenn dieses erstmal erledigt wurde, kann man entweder der Methode
  42. <classname>Zend_XmlRpc_Server::handle()</classname> ein
  43. <classname>Zend_XmlRpc_Request</classname>-Objekt übergeben oder es wird ein
  44. <classname>Zend_XmlRpc_Request_Http</classname> instanziert, falls keines angegeben
  45. wurde - die Anfrage wird also aus <code>php://input</code> geladen.
  46. </para>
  47. <para>
  48. <classname>Zend_XmlRpc_Server::handle()</classname> versucht daraufhin, den
  49. zuständigen Handler, der durch die angeforderte Methode bestimmt wird,
  50. auszuführen. Es wird entweder ein <classname>Zend_XmlRpc_Response</classname>-
  51. oder ein <classname>Zend_XmlRpc_Server_Fault</classname>-Objekt zurückgegeben.
  52. Beide Objekte besitzen eine Methode <methodname>__toString()</methodname>, die eine
  53. valide XML-RPC Antwort im XML-Format zurückgibt, die direkt ausgegeben
  54. werden kann.
  55. </para>
  56. </sect2>
  57. <sect2 id="zend.xmlrpc.server.conventions">
  58. <title>Konventionen</title>
  59. <para>
  60. <classname>Zend_XmlRpc_Server</classname> ermöglicht es dem Entwickler, Funktionen und
  61. Methodenaufrufe als ausführbare XML-RPC Methoden anzufügen. Durch
  62. <classname>Zend_Server_Reflection</classname> wird die Überwachung aller angefügten
  63. Methoden - durch Nutzung der DocBlocks der Methoden und Funktionen
  64. werden deren Hilfstexte und Signaturen ermittelt - ermöglicht.
  65. </para>
  66. <para>
  67. XML-RPC Typen werden nicht zwingend 1:1 zu PHP-Typen konvertiert.
  68. Dennoch wird versucht, einen passenden Typ, anhand der in
  69. @param- und @return-Zeilen enthaltenen Werte, zu ermitteln. Einige
  70. XML-RPC-Typen besitzen jedoch kein direktes Äquivalent und sollten
  71. deshalb mittels PHPdoc auf einen XML-RPC-Typen hinweisen. Diese
  72. beinhalten:
  73. </para>
  74. <itemizedlist>
  75. <listitem><para>dateTime.iso8601, ein String, der das Format
  76. YYYYMMDDTHH:mm:ss besitzt</para></listitem>
  77. <listitem><para>base64, base64-kodierte Daten</para></listitem>
  78. <listitem><para>struct, jegliches assoziatives Array</para></listitem>
  79. </itemizedlist>
  80. <para>
  81. 'Anbei ein Beispiel für einen solchen Hinweis:
  82. </para>
  83. <programlisting language="php"><![CDATA[
  84. /**
  85. * Dies ist eine Beispielfunktion.
  86. *
  87. * @param base64 $val1 Base64-kodierte Daten
  88. * @param dateTime.iso8601 $val2 Ein ISO-Datum
  89. * @param struct $val3 ein assoziatives Array
  90. * @return struct
  91. */
  92. function myFunc($val1, $val2, $val3)
  93. {
  94. }
  95. ]]></programlisting>
  96. <para>
  97. PhpDocumentor validiert keine Typen, die in Parameter- oder
  98. Rückgabewerten angegeben sind, weshalb dies keinen Einfluss auf
  99. die API-Dokumentation hat. Das Angeben der Hinweise ist notwendig,
  100. da der Server die, dem Methodenaufruf zugewiesenen, Parameter
  101. validiert.
  102. </para>
  103. <para>
  104. Es ist genauso gut möglich, mehrere Werte als Parameter oder für
  105. die Rückgabe anzugeben; die XML-RPC Spezifikation schlägt sogar
  106. vor, dass system.methodeSignatur ein Array, das alle möglichen
  107. Methodensignaturen (d.h. jegliche Kombination aus Parametern und
  108. Rückgabewerten) enthält, zurückgibt. Um dies zu erreichen, kann
  109. man, wie man es normalerweise auch beim PhpDocumentor auch tun würde,
  110. einfach den '|'-Operator nutzen.
  111. </para>
  112. <programlisting language="php"><![CDATA[
  113. /**
  114. * Dies ist eine Beispiel-Funktion.
  115. *
  116. * @param string|base64 $val1 String oder base64-kodierte Daten
  117. * @param string|dateTime.iso8601 $val2 String oder ein ISO-Datum
  118. * @param array|struct $val3 Normal indiziertes oder assoziatives Array
  119. * @return boolean|struct
  120. */
  121. function myFunc($val1, $val2, $val3)
  122. {
  123. }
  124. ]]></programlisting>
  125. <para>
  126. Dennoch eine Anmerkung: Das Erlaubung von vielen Signaturen kann
  127. zu Verwirrung für Entwickler führen, die diese Services nutzen;
  128. man sollte einer XML-RPC Methode deshalb nur eine Signatur zuweisen.
  129. </para>
  130. </sect2>
  131. <sect2 id="zend.xmlrpc.server.namespaces">
  132. <title>Nutzen von Namensräumen</title>
  133. <para>
  134. XML-RPC besitzt ein Konzept für Namensräume; Grundlegend erlaubt es
  135. das Gruppieren von XML-RPC-Methoden durch Punkt-separierte Namensräume.
  136. Dies hilft, Namenkollisionen zwischen Methoden, die durch verschiedene
  137. Klassen offeriert werden, zu verhindern. Beispielsweise kann der
  138. XML-RPC-Server mehrere Methoden im 'system'-Namensraum nutzen:
  139. </para>
  140. <itemizedlist>
  141. <listitem><para>system.listMethods</para></listitem>
  142. <listitem><para>system.methodHelp</para></listitem>
  143. <listitem><para>system.methodSignature</para></listitem>
  144. </itemizedlist>
  145. <para>
  146. Intern werden die Methoden zu Methoden desselben Namens in der
  147. Klasse <classname>Zend_XmlRpc_Server</classname> umgeleitet.
  148. </para>
  149. <para>
  150. Um angebotenen Methoden Namensräume hinzuzufügen, muss man lediglich beim
  151. Hinzufügen der gewünschten Klasse oder Funktion einen Namensraum angeben:
  152. </para>
  153. <programlisting language="php"><![CDATA[
  154. // Alle öffentlichten Methoden in My_Service_Class sind als
  155. // myservice.METHODNAME verfügbar
  156. $server->setClass('My_Service_Class', 'myservice');
  157. // Funktion 'somefunc' ist als funcs.somefunc ansprechbar.
  158. $server->addFunction('somefunc', 'funcs');
  159. ]]></programlisting>
  160. </sect2>
  161. <sect2 id="zend.xmlrpc.server.request">
  162. <title>Eigene Request-Objekte</title>
  163. <para>
  164. Die meiste Zeit wird man einfach den Standard-Anfragetyp
  165. <classname>Zend_XmlRpc_Request_Http</classname>, welcher im
  166. <classname>Zend_XmlRpc_Server</classname> enthalten ist, nutzen. Jedoch gibt es
  167. gelegentlich Fälle, in denen XML-RPC über die Kommandozeile (CLI), ein grafisches
  168. Benutzerinterface (GUI), eine andere Umgebung oder beim Protokollieren von ankommenden
  169. Anfragen erreichbar sein muss. Um dies zu bewerkstelligen, muss man ein eigenes
  170. Anfrage-Objekt kreieren, das <classname>Zend_XmlRpc_Request</classname> erweitert.
  171. Die wichtigste Sache, die man sich merken muss, ist sicherzustellen, dass die Methoden
  172. getMethod() und getParams() implementiert sind, so dass der XML-RPC-Server Informationen
  173. erhält, die er für das Abfertigen einer Anfrage benötigt.
  174. </para>
  175. </sect2>
  176. <sect2 id="zend.xmlrpc.server.response">
  177. <title>Eigene Antwort-Objekte</title>
  178. <para>
  179. Ähnlich wie bei den Anfrage-Objekten, kann der <classname>Zend_XmlRpc_Server</classname>
  180. auch eigene Antwortobjekte ausliefern; standardmäßig ist dies ein
  181. <classname>Zend_XmlRpc_Response_Http-Objekt</classname>, das einen passenden
  182. Content-Type HTTP-Header sendet, der für XML-RPC genutzt wird. Mögliche Nutzungen eines
  183. eigenen Objekts sind z.B. das Protokollieren von Antworten oder das Senden der
  184. Antworten zu STDOUT.
  185. </para>
  186. <para>
  187. Um eine eigene Antwortklasse zu nutzen, muss
  188. <classname>Zend_XmlRpc_Server::setResponseClass()</classname> vor dem Aufruf von
  189. <methodname>handle()</methodname> aufgerufen werden.
  190. </para>
  191. </sect2>
  192. <sect2 id="zend.xmlrpc.server.fault">
  193. <title>Verarbeiten von Exceptions durch Fehler</title>
  194. <para>
  195. <classname>Zend_XmlRpc_Server</classname> fängt die, durch eine ausgeführte Methode
  196. erzeugten, Exceptions and generiert daraus einen XML-RPC-Fehler als Antwort, wenn
  197. eine Exception gefangen wurde. Normalerweise werden die Exceptionnachrichten
  198. und -codes nicht in der Fehler-Antwort genutzt. Dies ist eine gewollte
  199. Entscheidung um den Code zu schützen; viele Exceptions entblößen mehr
  200. Informationen über den Code oder die Umgebung als der Entwickler
  201. wünscht (ein Paradebeispiel beinhaltet Datenbankabstraktion- oder
  202. die Zugriffsschichten-Exceptions).
  203. </para>
  204. <para>
  205. Exception-Klassen können jedoch anhand einer Weißliste (Whitelist) als
  206. Fehler-Antworten zurückgegeben werden. Dazu muss man lediglich die gewünschte
  207. Exception mittels
  208. <classname>Zend_XmlRpc_Server_Fault::attachFaultException()</classname> zur
  209. Weißliste hinzufügen:
  210. </para>
  211. <programlisting language="php"><![CDATA[
  212. Zend_XmlRpc_Server_Fault::attachFaultException('My_Project_Exception');
  213. ]]></programlisting>
  214. <para>
  215. Abgeleitete Exceptions lassen sich als ganze Familie von Exceptions
  216. hinzufügen, indem man deren Basisklasse angibt.
  217. <classname>Zend_XmlRpc_Server_Exception</classname>'s sind immer auf der Weißliste zu
  218. finden, da sie spezielle Serverfehler berichten (undefinierte Methoden, etc.).
  219. </para>
  220. <para>
  221. Jede Exception, die nicht auf der Weißliste zu finden ist, generiert
  222. eine Antwort mit dem '404' Code und der Nachricht 'Unknown error'.
  223. </para>
  224. </sect2>
  225. <sect2 id="zend.xmlrpc.server.caching">
  226. <title>Zwischenspeichern von Serverdefinitionen zwischen den Anfragen</title>
  227. <para>
  228. Das Hinzufügen einer Vielzahl von Klassen zu einer XML-RPC-Server Instanz kann zu einem
  229. großen Ressourcenverbrauch führen; jede Klasse muss via Reflection
  230. (<classname>Zend_Server_Reflection</classname>) inspiziert werden, welche eine Liste
  231. von allen möglichen Signaturen, die der Server verwenden kann, zurückgibt.
  232. </para>
  233. <para>
  234. Um die Einbußen zu reduzieren, kann <classname>Zend_XmlRpc_Server_Cache</classname>
  235. genutzt werden, welche die Serverdefinitionen zwischen den Anfragen zwischenspeichert.
  236. Wenn dies mit __autoload() kombiniert wird, kann es zu einem großen
  237. Geschwindigkeitsschub kommen.
  238. </para>
  239. <para>
  240. Ein Beispiel folgt:
  241. </para>
  242. <programlisting language="php"><![CDATA[
  243. function __autoload($class)
  244. {
  245. Zend_Loader::loadClass($class);
  246. }
  247. $cacheFile = dirname(__FILE__) . '/xmlrpc.cache';
  248. $server = new Zend_XmlRpc_Server();
  249. if (!Zend_XmlRpc_Server_Cache::get($cacheFile, $server)) {
  250. require_once 'My/Services/Glue.php';
  251. require_once 'My/Services/Paste.php';
  252. require_once 'My/Services/Tape.php';
  253. $server->setClass('My_Services_Glue', 'glue'); // glue. Namensraum
  254. $server->setClass('My_Services_Paste', 'paste'); // paste. Namensraum
  255. $server->setClass('My_Services_Tape', 'tape'); // tape. Namensraum
  256. Zend_XmlRpc_Server_Cache::save($cacheFile, $server);
  257. }
  258. echo $server->handle();
  259. ]]></programlisting>
  260. <para>
  261. Obiges Beispiel zeigt, wie der Server versucht, eine Definition
  262. aus der Datei xmlrpc.cache, welches sich im selben Ordner wie das
  263. Skript befindet, zu laden. Wenn dies nicht erfolgreich ist,
  264. lädt es die Server-Klassen, die es benötigt, und fügt sie zum
  265. Server hinzu. Danach wird versucht, die Cache-Datei mit der
  266. Serverdefinition zu erstellen.
  267. </para>
  268. </sect2>
  269. <sect2 id="zend.xmlrpc.server.use">
  270. <title>Nutzungsbeispiele</title>
  271. <para>
  272. Unten finden sich etliche Beispiele für eine Nutzung, die das
  273. gesamte Spektrum der verfügbaren Optionen für den Entwickler darstellen.
  274. These Beispiele bauen immer auf den vorangegangenen Beispielen auf.
  275. </para>
  276. <sect3 id="zend.xmlrpc.server.use.case1">
  277. <title>Grundlegende Benutzung</title>
  278. <para>
  279. Folgendes Beispiel fügt eine Funktion als ausführbare XML-RPC-Methode
  280. hinzu und verarbeitet eingehende Aufrufe.
  281. </para>
  282. <programlisting language="php"><![CDATA[
  283. /**
  284. * Gibt die MD5-Summe eines Strings zurück.
  285. *
  286. * @param string $value Wert aus dem die MD5-Summe errechnet wird
  287. * @return string MD5-Summe des Werts
  288. */
  289. function md5Value($value)
  290. {
  291. return md5($value);
  292. }
  293. $server = new Zend_XmlRpc_Server();
  294. $server->addFunction('md5Value');
  295. echo $server->handle();
  296. ]]></programlisting>
  297. </sect3>
  298. <sect3 id="zend.xmlrpc.server.use.case2">
  299. <title>Hinzufügen einer Klasse</title>
  300. <para>
  301. Das nächste Beispiel illustriert, wie man die öffentlichen Methoden
  302. eienr Klasse als ausführbare XML-RPC-Methoden hinzufügt.
  303. </para>
  304. <programlisting language="php"><![CDATA[
  305. $server = new Zend_XmlRpc_Server();
  306. $server->setClass('Services_Comb');
  307. echo $server->handle();
  308. ]]></programlisting>
  309. </sect3>
  310. <sect3 id="zend.xmlrpc.server.use.case3">
  311. <title>Mehrere Klassen unter der Nutzung von Namensräumen hinzufügen</title>
  312. <para>
  313. Das nächste Beispiel zeigt, wie man mehrer Klassen mit ihren eigenen
  314. Namensräumen hinzufügt.
  315. </para>
  316. <programlisting language="php"><![CDATA[
  317. require_once 'Services/Comb.php';
  318. require_once 'Services/Brush.php';
  319. require_once 'Services/Pick.php';
  320. $server = new Zend_XmlRpc_Server();
  321. // Methoden werden als comb.* aufgerufen
  322. $server->setClass('Services_Comb', 'comb');
  323. // Methoden werden als brush.* aufgerufen
  324. $server->setClass('Services_Brush', 'brush');
  325. // Methoden werden als pick.* aufgerufen
  326. $server->setClass('Services_Pick', 'pick');
  327. echo $server->handle();
  328. ]]></programlisting>
  329. </sect3>
  330. <sect3 id="zend.xmlrpc.server.use.case4">
  331. <title>Bestimmen von Exceptions als valide Fehler-Antwort</title>
  332. <para>
  333. Im nächsten Beispiel wird gezeigt, wie man jede Exception, die von
  334. Services_Exception abgeleitet wurde, als Fehler-Antwort nutzen kann,
  335. dessen Nachricht und Code erhalten bleibt.
  336. </para>
  337. <programlisting language="php"><![CDATA[
  338. require_once 'Services/Exception.php';
  339. require_once 'Services/Comb.php';
  340. require_once 'Services/Brush.php';
  341. require_once 'Services/Pick.php';
  342. // Services_Exceptions dürfen als Fehler-Antwort genutzt werden
  343. Zend_XmlRpc_Server_Fault::attachFaultException('Services_Exception');
  344. $server = new Zend_XmlRpc_Server();
  345. // Methoden werden als comb.* aufgerufen
  346. $server->setClass('Services_Comb', 'comb');
  347. // Methoden werden als brush.* aufgerufen
  348. $server->setClass('Services_Brush', 'brush');
  349. // Methoden werden als pick.* aufgerufen
  350. $server->setClass('Services_Pick', 'pick');
  351. echo $server->handle();
  352. ]]></programlisting>
  353. </sect3>
  354. <sect3 id="zend.xmlrpc.server.use.case5">
  355. <title>Nutzen eines eigenen Anfrage-Objekts</title>
  356. <para>
  357. Im folgenden Beispiel wird ein eigenes Anfrage-Objekt instanziert
  358. und durch den Server verarbeitet.
  359. </para>
  360. <programlisting language="php"><![CDATA[
  361. require_once 'Services/Request.php';
  362. require_once 'Services/Exception.php';
  363. require_once 'Services/Comb.php';
  364. require_once 'Services/Brush.php';
  365. require_once 'Services/Pick.php';
  366. // Services_Exceptions dürfen als Fehler-Antwort genutzt werden
  367. Zend_XmlRpc_Server_Fault::attachFaultException('Services_Exception');
  368. $server = new Zend_XmlRpc_Server();
  369. // Methoden werden als comb.* aufgerufen
  370. $server->setClass('Services_Comb', 'comb');
  371. // Methoden werden als brush.* aufgerufen
  372. $server->setClass('Services_Brush', 'brush');
  373. // Methoden werden als pick.* aufgerufen
  374. $server->setClass('Services_Pick', 'pick');
  375. // Ein neues Anfrage-Objekt wird erstellt
  376. $request = new Services_Request();
  377. echo $server->handle($request);
  378. ]]></programlisting>
  379. </sect3>
  380. <sect3 id="zend.xmlrpc.server.use.case6">
  381. <title>Nutzen eigener Antwort-Objekte</title>
  382. <para>
  383. Das nachstehende Beispiel zeigt, wie man eine eigene Antwort-Klasse
  384. als zurückgegebene Antwort für den Server setzt.
  385. </para>
  386. <programlisting language="php"><![CDATA[
  387. require_once 'Services/Request.php';
  388. require_once 'Services/Response.php';
  389. require_once 'Services/Exception.php';
  390. require_once 'Services/Comb.php';
  391. require_once 'Services/Brush.php';
  392. require_once 'Services/Pick.php';
  393. // Services_Exceptions dürfen als Fehler-Antwort genutzt werden
  394. Zend_XmlRpc_Server_Fault::attachFaultException('Services_Exception');
  395. $server = new Zend_XmlRpc_Server();
  396. // Methoden werden als comb.* aufgerufen
  397. $server->setClass('Services_Comb', 'comb');
  398. // Methoden werden als brush.* aufgerufen
  399. $server->setClass('Services_Brush', 'brush');
  400. // Methoden werden als pick.* aufgerufen
  401. $server->setClass('Services_Pick', 'pick');
  402. // Ein neues Anfrage-Objekt wird erstellt
  403. $request = new Services_Request();
  404. // Nutzen eigener Antwort-Klasse
  405. $server->setResponseClass('Services_Response');
  406. echo $server->handle($request);
  407. ]]></programlisting>
  408. </sect3>
  409. <sect3 id="zend.xmlrpc.server.use.case7">
  410. <title>Zwischenspeichern von Serverdefinition zwischen den Anfragen</title>
  411. <para>
  412. Dieses Beispiel zeigt, wie man Serverdefinitionen zwischen verschiedenen
  413. Anfragen zwischenspeichern kann.
  414. </para>
  415. <programlisting language="php"><![CDATA[
  416. // Definieren einer Cache-Datei
  417. $cacheFile = dirname(__FILE__) . '/xmlrpc.cache';
  418. // Services_Exceptions dürfen als Fehler-Antwort genutzt werden
  419. Zend_XmlRpc_Server_Fault::attachFaultException('Services_Exception');
  420. $server = new Zend_XmlRpc_Server();
  421. // Versucht die Serverdefinition aus dem Cache zu laden
  422. if (!Zend_XmlRpc_Server_Cache::get($cacheFile, $server)) {
  423. // Methoden werden als comb.* aufgerufen
  424. $server->setClass('Services_Comb', 'comb');
  425. // Methoden werden als brush.* aufgerufen
  426. $server->setClass('Services_Brush', 'brush');
  427. // Methoden werden als pick.* aufgerufen
  428. $server->setClass('Services_Pick', 'pick');
  429. // Speichern des Caches
  430. Zend_XmlRpc_Server_Cache::save($cacheFile, $server);
  431. }
  432. // Ein neues Anfrage-Objekt wird erstellt
  433. $request = new Services_Request();
  434. // Nutzen eigener Antwort-Klasse
  435. $server->setResponseClass('Services_Response');
  436. echo $server->handle($request);
  437. ]]></programlisting>
  438. </sect3>
  439. </sect2>
  440. </sect1>
  441. <!--
  442. vim:se ts=4 sw=4 et:
  443. -->