Zend_XmlRpc_Server.xml 20 KB

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