Hlavní navigace

Proč mám rád velblouda

18. 1. 2022 0:00 Jiří Raška

Používám Apache Camel v různých projektech již více jak deset let a jsem jeho velkým příznivcem. Samotní tvůrci jej označují jako univerzální integrační platformu, ale z vlastní zkušenosti můžu říct, že pole jeho využití je širší.

Jeho funkcionalita zahrnující stovky komponent a datových formátů je velice široká. Vždy záleží na vašich potřebách, co z toho reálně použijete.

Popisovat jednotlivé vlastnosti a důvody, proč se mi líbí, by bylo hodně zdlouhavé a asi i nudné. Kromě toho byly o Apache Camel napsány obsáhlé knihy, na což ambici nemám.

Tak jsem si řekl, že bych mohl ukázat zajímavé případy, kdy jsem Apache Camel reálně použil. Snad v někom z vás vzbudím zájem se na něj také podívat. Tohle je jeden z takových případů.

Všechny zdroje k příkladu najdete na GitHub projektu: jraska1/jv-camel-examples/example-01.

Kapitola nultá – prolog

Dostal jsem za úkol propojit dva systémy pro výměnu zdravotnické dokumentace tak, aby si zdravotnická zařízení v nich zapojená mohla vyměňovat DASTA zprávy. Abych mohl odeslat zprávu z jednoho systému do druhého, musím vědět, jaká zdravotnická zařízení jsou v tom druhém systému zapojena. A to bude cílem této ukázky.

Systém, na který se napojuji, poskytuje informace o jim podporovaných zdravotnických zařízeních ve formě XML dokumentu dostupného prostřednictvím webové služby.

Potřebuji tento dokument načíst, vybrat z něj pro mne podstatné údaje, a zapsat si je do vlastní správy ve formě Java Bean.

Takto vypadá zkrácená verze XML dokumentu se zdrojovými daty:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<Participants>
  <Participant>
    <Clickbox>
      <organisationname>Poliklinika Ústí nad Orlicí - TEST</organisationname>
      <zarizeni>Poliklinika Ústí nad Orlicí - TEST</zarizeni>
      <ico>60112301</ico>
      <pracoviste>Poliklinika Ústí nad Orlicí - pracoviště</pracoviste>
      <street>Čs. armády</street>
      <housenumber>1181</housenumber>
      <postalcode>56201</postalcode>
      <city>Ústí nad Orlicí</city>
      <mnet>CZ9TEST5</mnet>
      <phoneoffice>777166528</phoneoffice>
      <phonemobile>777166528</phonemobile>
      <email>petr.hartman.cb@seznam.cz</email>
      <icz>65123000</icz>
      <icp>65123001</icp>
      <icp>65123002</icp>
      <odb>001</odb>
      <odb>207</odb>
      <odb>603</odb>
      <countrycode>CZ</countrycode>
      <cgmnumber>01300103792</cgmnumber>
      <doctor>
      <firstname>Karel</firstname>
      <lastname>Svoboda</lastname>
      <idclk>1111111111</idclk>
      <phone>111111111</phone>
    </Clickbox>
    <MedicalNet>
      <Alias>CZ9TEST5</Alias>
      <MailAddress>cz9test5@cz.top.medicalnetworks.com</MailAddress>
      <DisplayName>CGM CZ - test - LSM Petr Hartman</DisplayName>
      <SupportedMessageCompressions>
        <SupportedMessageCompression>None</SupportedMessageCompression>
        <SupportedMessageCompression>GZip</SupportedMessageCompression>
      </SupportedMessageCompressions>
      <SupportedMessageExtensions>
        <SupportedMessageExtension>DispositionNotification</SupportedMessageExtension>
      </SupportedMessageExtensions>
      <SupportedSignatureHashAlgorithms>
        <Oid>1.3.14.3.2.26</Oid>
      </SupportedSignatureHashAlgorithms>
      <SupportsDetachedSignatures>0</SupportsDetachedSignatures>
      <SupportedEncryptionAlgorithms>
        <Oid>1.2.840.113549.3.7</Oid>
      </SupportedEncryptionAlgorithms>
      <LastName>CGM CZ - test - LSM</LastName>
      <FirstName>Petr Hartman</FirstName>
      <Prefix/>
      <Head>To</Head>
      <Street>Čs. Armády 1181</Street>
      <ZipCode>562 15</ZipCode>
      <City>Ústí nad Orlicí</City>
      <TelephoneNumber>+420777166528</TelephoneNumber>
      <FaxNumber/>
      <Speciality>001</Speciality>
    </MedicalNet>
  </Participant>
  <Participant>
    <Clickbox>
      <organisationname>UROLOGIE ČERNOŠICE S.R.O., TÁBORSKÁ</organisationname>
      <zarizeni>UROLOGIE ČERNOŠICE S.R.O.</zarizeni>
      <ico>03298345</ico>
      <pracoviste>TÁBORSKÁ</pracoviste>
      <street>Táborská</street>
      <housenumber>2025</housenumber>
      <postalcode>252 28</postalcode>
      <city>ČERNOŠICE  </city>
      <mnet>null</mnet>
      <phoneoffice>724773054</phoneoffice>
      <phonemobile>724773054</phonemobile>
      <email>mhujo@centrum.cz</email>
      <icz>29725000</icz>
...

  </Participant>
</Participants>

A ta data potřebuji dostat do vlastního správce konfigurací zdravotnických zařízení, který vypadá zhruba takto:

Map<URI, HealthCareProvider> configStorage

kde klíčem je unikátní identifikátor ve formě URI, a hodnota pak instance třídy HealtCareProvider:

public class HealthCareProvider {

    private URI oid;
    private String name;
    private String dn;
    private String ico;
    private String addr;
    private String icz;
    private String phone;
    private String email;
    private List<String> icp;
    private List<Contact> contacts;

    public URI getOid() { return oid; }
    public void setOid(URI oid) { this.oid = oid; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public String getDn() { return dn; }
    public void setDn(String dn) { this.dn = dn; }
    public String getIco() { return ico; }
    public void setIco(String ico) { this.ico = ico; }
    public String getAddr() { return addr; }
    public void setAddr(String addr) { this.addr = addr; }
    public String getIcz() { return icz; }
    public void setIcz(String icz) { this.icz = icz; }
    public String getPhone() { return phone; }
    public void setPhone(String phone) { this.phone = phone; }
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
    public List<String> getIcp() { return icp; }
    public void setIcp(List<String> icp) { this.icp = icp; }
    public List<Contact> getContacts() { return contacts; }
    public void setContacts(List<Contact> contacts) { this.contacts = contacts; }
}

A to je vše, co potřebujete vědět pro pochopení tohoto článku.

Vlastní řešení bude zahrnovat především Camel cestu definovanou ve třídě route/CamelRoutes.

Takto vypadá její skelet:

@Component
public class CamelRoutes extends RouteBuilder {

    private static final Logger logger = LoggerFactory.getLogger(CamelRoutes.class);

    @Autowired
    private Map<URI, HealthCareProvider> configStorage;

    @Override
    public void configure() {

    }
}

Cesty jsou dále definovány v metodě configure().

Veškeré konfigurační parametry najdete v souboru application.yaml.

Kapitola první – krbové hodiny

Informace zahrnuté ve formě XML seznamu obsahují vždy aktuální přehled připojených zdravotnických zařízení. Nemám jak zjistit, kdy se tento seznam změní a co bude v jeho obsahu změněno. Takže mně nezbývá nic jiného, než opakovaně načítat aktuální stav a promítat jej do interní správy.

Budu potřebovat nějaký časovač, který mi bude opakovaně spouštět akci načtení seznamu a jeho promítnutí do configStorage.

from("scheduler://main?delay={{scheduler.delay}}&repeatCount={{scheduler.repeatCount}}").routeId("main-route")
                .log("Tic tac ...");

A takto vypadá výstup po spuštění:

2021-12-24
09:57:49.421  INFO 3486 --- [main] c.d.c.example01.Application : *** Hello World, greetings from Dwarf ***
2021-12-24 09:57:50.452 INFO 3486 --- [cheduler://main] main-route : Tic tac ...
2021-12-24 09:58:00.459 INFO 3486 --- [cheduler://main] main-route : Tic tac ...
2021-12-24 09:58:10.461 INFO 3486 --- [cheduler://main] main-route : Tic tac ...

Parametry jsou:

  • scheduler.delay – pro zpoždění mezi jednotlivými běhy (v milisekundách)

  • scheduler.repeatCount – pro počet opakování (0 je opakování donekonečna)

Kapitola druhá – katalog

Ono to s tou webovou službou není až tak jednoduché. Vlastní XML soubor nemůžu načíst přímo. Musím si jej nejdříve vyhledat v katalogu souborů, kde najdu i URL pro jeho stažení.

Dotaz na katalog je opět ve formě webové služby. V dotazu musí být specifikováno, o jaký soubor mám zájem, a to ve formě XML. Výsledkem jsou pak informace o dotazovaném souboru ve formě jak jinak než XML.

Dotaz na katalog mám schovaný v templates/catalog_request.xml a vypadá takto:

<?xml version="1.0" encoding="utf-8"?>
<catalogRequest>
    <catalogs>
        <catalog>
            <catalogType>CLICKBOX</catalogType>
            <fileType>ParticipantDirectory</fileType>
            <history>true</history>
            <!--<localVersion>20210101120000</localVersion>-->
        </catalog>
    </catalogs>
</catalogRequest>

Po úpravě a doplnění cesta vypadá takto:

@Value("classpath:templates/catalog_request.xml")
private Resource template;
from("scheduler://main?delay={{scheduler.delay}}&repeatCount={{scheduler.repeatCount}}").routeId("main-route") .process(exchange -> { exchange.getMessage().setBody(template.getInputStream()); exchange.getIn().setHeader(Exchange.HTTP_METHOD, HttpEndpointBuilderFactory.HttpMethods.POST); exchange.getIn().setHeader("Content-Type", "application/xml;charset=UTF-8"); exchange.getIn().setHeader("Accept", "application/xml"); exchange.getIn().setHeader("Accept-Charset", "UTF-8"); }) .to("{{catalog.url}}") .convertBodyTo(String.class) .to("log:cz.dsw.camel_examples.route.CamelRoutes?multiline=true&showHeaders=true");

Obsah dotazu načtu do těla zprávy z template.

Dále si nastavím typ HTTP metody a hlavičky pro webový dotaz na katalog. Jak obsah dotazu tak očekávaná odpověď musí mít typ application/xml.

URL pro dotaz jsem si opět dotáhl z konfigurace pod názvem catalog.url.

Vyvolání dotazu a předání výsledku zajistí direktiva to().

Po spuštění:

2021-12-24 10:58:31.595 INFO 4106 --- [cheduler://main] cz.dsw.camel_examples.route.CamelRoutes : Exchange[
ExchangePattern: InOnly
Headers: {Accept=application/xml, Accept-Charset=UTF-8, Cache-Control=no-cache, no-store, max-age=0, must-revalidate, CamelHttpMethod=POST, CamelHttpResponseCode=200, CamelHttpResponseText=, CamelMessageTimestamp=1640339911004, Connection=keep-alive, Content-Type=application/xml;charset=UTF-8, Date=Fri, 24 Dec 2021 09:58:31 GMT, Expires=0, Pragma=no-cache, Server=nginx, Strict-Transport-Security=max-age=63072000; includeSubDomains; preload, Transfer-Encoding=chunked, X-Content-Type-Options=[nosniff, nosniff], X-Frame-Options=[DENY, DENY], X-XSS-Protection=[1; mode=block, 1; mode=block]}
BodyType: String
Body: <catalogInfo><catalogs><catalog><catalogType>CLICKBOX</catalogType><items><item><catalogType>CLICKBOX</catalogType><fileType>ParticipantDirectory</fileType><localVersion>20211217102744</localVersion><url>https://grinder.cgm-medistar.cz:8443/grinder/rest/storageFiles/CLICKBOX.ParticipantDirectory.ParticipantDirectory_20211217102744.zip</url></item></items></catalog></catalogs></catalogInfo>
]

Výsledek jsem si ještě převedl do řetězce pro zobrazení ve výpisu. Vypadá takto:

<?xml version="1.0"?>
<catalogInfo>
  <catalogs>
    <catalog>
      <catalogType>CLICKBOX</catalogType>
      <items>
        <item>
          <catalogType>CLICKBOX</catalogType>
          <fileType>ParticipantDirectory</fileType>
          <localVersion>20211217102744</localVersion>
          <url>https://grinder.cgm-medistar.cz:8443/grinder/rest/storageFiles/CLICKBOX.ParticipantDirectory.ParticipantDirectory_20211217102744.zip</url>
        </item>
      </items>
    </catalog>
  </catalogs>
</catalogInfo>

Mne z celé odpovědi zajímá pouze obsah elementu /catalogInfo/catalogs/cata­log/items/item/url, takže ještě doplním cestu o jeho výběr:

from("scheduler://main?delay={{scheduler.delay}}&repeatCount={{scheduler.repeatCount}}").routeId("main-route")
        .process(exchange -> {
            exchange.getMessage().setBody(template.getInputStream());
            exchange.getIn().setHeader(Exchange.HTTP_METHOD, HttpEndpointBuilderFactory.HttpMethods.POST);
            exchange.getIn().setHeader("Content-Type", "application/xml;charset=UTF-8");
            exchange.getIn().setHeader("Accept", "application/xml");
            exchange.getIn().setHeader("Accept-Charset", "UTF-8");
        })
        .to("{{catalog.url}}")
        .setBody(xpath("/catalogInfo/catalogs/catalog/items/item/url", String.class))
        .to("log:cz.dsw.camel_examples.route.CamelRoutes?multiline=true&showHeaders=false");

A výsledkem by mělo být URL souboru v těle zprávy:

2021-12-24 11:11:42.728 INFO 4427 --- [cheduler://main] cz.dsw.camel_examples.route.CamelRoutes : Exchange[
ExchangePattern: InOnly
BodyType: String
Body: https://grinder.cgm-medistar.cz:8443/grinder/rest/storageFiles/CLICKBOX.ParticipantDirectory.ParticipantDirectory_20211217102744.zip
]

Kapitola třetí – seznam

Neplést prosím s tím Cibulkovým. V tomto případě se jedná o seznam zdravotnických zařízení.

Prozatím dobrý. Teď mám v těle roury URL, na kterém je k dispozici seznam ve formě XML zabaleného do ZIP (to je vidět podle přípony souboru). A ten potřebuji vyzvednout.

S direktivou to() v tomto případě nevystačím, protože URL není známé při sestavení roury, ale až za jejího běhu. Naštěstí Camel obsahuje dynamickou variantu toD().

Doplněná cesta tedy vypadá takto:

from("scheduler://main?delay={{scheduler.delay}}&repeatCount={{scheduler.repeatCount}}").routeId("main-route")
        .process(exchange -> {
            exchange.getMessage().setBody(template.getInputStream());
            exchange.getIn().setHeader(Exchange.HTTP_METHOD, HttpEndpointBuilderFactory.HttpMethods.POST);
            exchange.getIn().setHeader("Content-Type", "application/xml;charset=UTF-8");
            exchange.getIn().setHeader("Accept", "application/xml");
            exchange.getIn().setHeader("Accept-Charset", "UTF-8");
        })
        .to("{{catalog.url}}")
        .setBody(xpath("/catalogInfo/catalogs/catalog/items/item/url", String.class))
        .process(exchange ->
                      exchange.getIn().setHeader(Exchange.HTTP_METHOD, HttpEndpointBuilderFactory.HttpMethods.GET))
        .toD("${body}")
        .unmarshal().zipFile()
        .convertBodyTo(String.class)
        .to("log:cz.dsw.camel_examples.route.CamelRoutes?multiline=true&showHeaders=true");

Nastavil jsem si do hlavičky typ HTTP metody, kterou chci použít při požadavku na soubor.

Dále jsem udělal vlastní dotaz s tím, že URL dotazu je obsahem těla zprávy v trubce.

Výsledkem je ZIP soubor, který musím ještě rozbalit a obsah poslat do roury.

A takto to vypadá po spuštění:

2021-12-24 11:21:27.493 INFO 4670 --- [cheduler://main] cz.dsw.camel_examples.route.CamelRoutes : Exchange[
ExchangePattern: InOnly
Headers: {Accept=application/xml, Accept-Charset=UTF-8, Cache-Control=public, CamelFileName=ParticipantDirectory_20211217102744.xml, CamelHttpMethod=GET, CamelHttpResponseCode=200, CamelHttpResponseText=, CamelMessageTimestamp=1640341284889, Connection=keep-alive, Content-Description=File Transfer, Content-Disposition=attachment; filename="ParticipantDirectory_20211217102744.zip", Content-Length=121938, Content-MD5=f3c57d08281a90b044aba76a3009d459, Content-Transfer-Encoding=binary, Content-Type=application/zip, Date=Fri, 24 Dec 2021 10:21:27 GMT, Expires=0, Pragma=public, Server=nginx, Strict-Transport-Security=max-age=63072000; includeSubDomains; preload, X-Content-Type-Options=[nosniff, nosniff], X-Frame-Options=[DENY, DENY], X-XSS-Protection=[1; mode=block, 1; mode=block]}
BodyType: String
Body: <?xml version="1.0" encoding="UTF-8" standalone="no"?><Participants> <Participant> <Clickbox> <zarizeni>Ordinace JAR</zarizeni> <pracoviste>Pracoviště ordinace JAR</pracoviste> <street>Šumavská</street> <housenumber>519/35</housenumber> <city>Brno</city> <postalcode>60200</postalcode> <phoneoffice>111222333</phoneoffice> <phonemobile>333222111</phonemobile> <email>jakub.rysavy@cgm.com</email> <ico>47902442</ico> <icz>11223344</icz> <icp>44332211</icp> <odb>001</odb> <countrycode>CZ</countrycode> ... [Body clipped after 10000 chars, total length is 2268309]
]

Výpis těla jsem hodně ořezal, ale asi tušíte, že to je ten XML soubor prezentovaný v prologu.

Kapitola čtvrtá – sekačka

Nyní již mám načtený vlastní XML soubor. Když se na něj podíváte, tak se v podstatě jedná o sekvenci elementů:

  • /Participants/Participant/Clickbox – tohle mne zajímá

  • /Participants/Participant/MedicalNet – tyhle mne nezajímají, patří k jinému systému

Navíc budu každý element zpracovávat samostatně a postupně. Což znamená, že nemusím mít všechny elementy k dispozici současně – použiji tedy stream elementů vyhovujících Xpath /Participants/Participant/Clickbox:

from("scheduler://main?delay={{scheduler.delay}}&repeatCount={{scheduler.repeatCount}}").routeId("main-route")
          .process(exchange -> {
              exchange.getMessage().setBody(template.getInputStream());
              exchange.getIn().setHeader(Exchange.HTTP_METHOD, HttpEndpointBuilderFactory.HttpMethods.POST);
              exchange.getIn().setHeader("Content-Type", "application/xml;charset=UTF-8");
              exchange.getIn().setHeader("Accept", "application/xml");
              exchange.getIn().setHeader("Accept-Charset", "UTF-8");
          })
          .to("{{catalog.url}}")
          .setBody(xpath("/catalogInfo/catalogs/catalog/items/item/url", String.class))
          .process(exchange ->
                               exchange.getIn().setHeader(Exchange.HTTP_METHOD, HttpEndpointBuilderFactory.HttpMethods.GET))
          .toD("${body}")
          .unmarshal().zipFile()
          .convertBodyTo(String.class)
          .split(xpath("/Participants/Participant/Clickbox")).streaming()
               .to("log:cz.dsw.camel_examples.route.CamelRoutes?multiline=true&showHeaders=false")
          .end();

A zkrácený výpis pak vypadá nějak takto:

2021-12-24 11:34:40.827 INFO 4871 --- [cheduler://main] cz.dsw.camel_examples.route.CamelRoutes : Exchange[
ExchangePattern: InOnly
BodyType: com.sun.org.apache.xerces.internal.dom.DeferredElementNSImpl
Body: <Clickbox> <zarizeni>Ordinace JAR</zarizeni> ... </Clickbox>
]
2021-12-24 11:34:40.833 INFO 4871 --- [cheduler://main] cz.dsw.camel_examples.route.CamelRoutes : Exchange[
ExchangePattern: InOnly
BodyType: com.sun.org.apache.xerces.internal.dom.DeferredElementNSImpl
Body: <Clickbox> <organisationname>Poliklinika Ústí nad Orlicí - TEST</organisationname> ... </Clickbox>
]
2021-12-24 11:34:40.837 INFO 4871 --- [cheduler://main] cz.dsw.camel_examples.route.CamelRoutes : Exchange[
ExchangePattern: InOnly
BodyType: com.sun.org.apache.xerces.internal.dom.DeferredElementNSImpl
Body: <Clickbox> <organisationname>UROLOGIE ČERNOŠICE S.R.O., TÁBORSKÁ</organisationname> ... </Clickbox>
]

Jedna zpráva je direktivou split() rozsekána na více zpráv, které budu zpracovávat postupně.

Je zde ještě jeden zádrhel. Abych mohl každé zařízení identifikovat, musím vytvořit jeho unikátní identifikátor ve formě URI. To dělám na základě hodnoty elementu /Clickbox/cgmnumber. Vyberu si tedy pouze ty záznamy, které tento element mají.

Konečná varianta pro tuto kapitolu:

from("scheduler://main?delay={{scheduler.delay}}&repeatCount={{scheduler.repeatCount}}").routeId("main-route")
        .process(exchange -> {
            exchange.getMessage().setBody(template.getInputStream());
            exchange.getIn().setHeader(Exchange.HTTP_METHOD, HttpEndpointBuilderFactory.HttpMethods.POST);
            exchange.getIn().setHeader("Content-Type", "application/xml;charset=UTF-8");
            exchange.getIn().setHeader("Accept", "application/xml");
            exchange.getIn().setHeader("Accept-Charset", "UTF-8");
        })
        .to("{{catalog.url}}")
        .setBody(xpath("/catalogInfo/catalogs/catalog/items/item/url", String.class))
        .process(exchange ->
                      exchange.getIn().setHeader(Exchange.HTTP_METHOD, HttpEndpointBuilderFactory.HttpMethods.GET))
        .toD("${body}")
        .unmarshal().zipFile()
        .split(xpath("/Participants/Participant/Clickbox")).streaming()
            .choice()
                .when(xpath("/Clickbox/cgmnumber"))
                    .to("log:cz.dsw.camel_examples.route.CamelRoutes?multiline=true&showHeaders=false")
                .endChoice()
                .otherwise()
                    .setBody().simple("${null}")
                .endChoice()
            .end()
        .end();

S výsledkem po spuštění obdobným jako výše.

Kapitola pátá – přeměna

Dostal jsem se do stavu, kdy zpracovávám XML zprávu odpovídající jednomu záznamu o zdravotnickém zařízení.

Struktura tohoto XML ale neodpovídá tomu, jaké atributy sleduji ve svém Java Bean. Musím tedy nějak obsah XML napasovat na moji podobu.

V tomto okamžiku mne napadá několik variant, jak bych to mohl udělat:

  1. Načíst XML jako DOM a pomocí Xpath si z něho vyzobat údaje pro svůj bean – to asi není špatný nápad.

  2. Převést XML pomocí JAXB do bean, a následně vyzobávat údaje z tohoto objektu pro svůj bean – to je příliš pracné, neboť nemám XML Schema pro vygenerování zdrojů

  3. Převést XML do struktury odpovídající pro můj bean pomocí XSL transformace a následně načíst pomocí JAXB – také zbytečně pracné, musel bych anotovat svůj třídu

  4. Vypadá to trochu krkolomně, ale jde to: převést XML do JSON pomocí XSL transformace a následně načíst do bean.

Vybral jsem si tu poslední variantu, aby to bylo trochu zajímavější.

Pro převod XML → JSON jsem napsal jednoduchý template, template/xml_to_json.xslt:

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
    <xsl:output method="text"/>
    <xsl:template match="/Clickbox">
{
  "oid": "clickbox:<xsl:value-of select="cgmnumber"/>",
  "name": "<xsl:value-of select="translate(organisationname | zarizeni, '&quot;', '')"/>",
  "dn": null,
  "ico": "<xsl:value-of select="ico"/>",
  <xsl:variable name="addr"><xsl:value-of select="pracoviste"/>, <xsl:value-of select="street"/><xsl:text xml:space="preserve"> </xsl:text><xsl:value-of select="housenumber"/>, <xsl:value-of select="postalcode"/><xsl:text xml:space="preserve"> </xsl:text><xsl:value-of select="city"/></xsl:variable>
  "addr": "<xsl:value-of select="translate($addr, '&quot;', '')"/>",
  "icz": "<xsl:value-of select="icz"/>",
  "phone": "<xsl:value-of select="phoneoffice | phonemobile"/>",
  "email": "<xsl:value-of select="email"/>",
  "icp": [
<xsl:for-each select="icp">
"<xsl:value-of select="."/>"<xsl:choose><xsl:when test="position() != last()">,</xsl:when></xsl:choose>
</xsl:for-each>
],
  "contacts": [
<xsl:for-each select="doctor">{
"name": "<xsl:value-of select="firstname"/><xsl:text xml:space="preserve"> </xsl:text><xsl:value-of select="lastname"/>",
"phone": "<xsl:value-of select="phone"/>"
}<xsl:choose><xsl:when test="position() != last()">,</xsl:when></xsl:choose>
</xsl:for-each>
  ]
}
    </xsl:template>
</xsl:stylesheet>

Roura se zabudovanou transformací pak vypadá takto:

from("scheduler://main?delay={{scheduler.delay}}&repeatCount={{scheduler.repeatCount}}").routeId("main-route")
        .process(exchange -> {
            exchange.getMessage().setBody(template.getInputStream());
            exchange.getIn().setHeader(Exchange.HTTP_METHOD, HttpEndpointBuilderFactory.HttpMethods.POST);
            exchange.getIn().setHeader("Content-Type", "application/xml;charset=UTF-8");
            exchange.getIn().setHeader("Accept", "application/xml");
            exchange.getIn().setHeader("Accept-Charset", "UTF-8");
        })
        .to("{{catalog.url}}")
        .setBody(xpath("/catalogInfo/catalogs/catalog/items/item/url", String.class))
        .process(exchange ->
                       exchange.getIn().setHeader(Exchange.HTTP_METHOD, HttpEndpointBuilderFactory.HttpMethods.GET))
        .toD("${body}")
        .unmarshal().zipFile()
        .split(xpath("/Participants/Participant/Clickbox")).streaming()
            .choice()
                .when(xpath("/Clickbox/cgmnumber"))
                    .to("xslt:templates/xml_to_json.xslt")
                    .to("log:cz.dsw.camel_examples.route.CamelRoutes?multiline=true&showHeaders=false")
                .endChoice()
                .otherwise()
                    .setBody().simple("${null}")
                .endChoice()
            .end()
        .end();

A zkrácený výsledek nějak takto:

2021-12-24 11:55:43.939 INFO 5164 --- [cheduler://main] cz.dsw.camel_examples.route.CamelRoutes : Exchange[
ExchangePattern: InOnly
BodyType: String
Body: { "oid": "clickbox:01300094508", "name": "Ordinace JAR", "dn": null, "ico": "47902442", "addr": "Pracoviště ordinace JAR, Šumavská 519/35, 60200 Brno", "icz": "11223344", "phone": "111222333", "email": "jakub.rysavy@cgm.com", "icp": ["44332211"], "contacts": [ ]}
]
2021-12-24 11:55:43.956 INFO 5164 --- [cheduler://main] cz.dsw.camel_examples.route.CamelRoutes : Exchange[
ExchangePattern: InOnly
BodyType: String
Body: { "oid": "clickbox:01300103792", "name": "Poliklinika Ústí nad Orlicí - TEST", "dn": null, "ico": "60112301", "addr": "Poliklinika Ústí nad Orlicí - pracoviště, Čs. armády 1181, 56201 Ústí nad Orlicí", "icz": "65123000", "phone": "777166528", "email": "petr.hartman.cb@seznam.cz", "icp": ["65123001","65123002","65123014","65123103","65123201","65123207","65123603"], "contacts": [{"name": "Karel Svoboda","phone": "111111111"},{"name": "Jana Nováková","phone": "111111111"},{"name": "Jan Fiala","phone": "111111111"} ]}
]
2021-12-24 11:55:43.971 INFO 5164 --- [cheduler://main] cz.dsw.camel_examples.route.CamelRoutes : Exchange[
ExchangePattern: InOnly
BodyType: String
Body: { "oid": "clickbox:01300101562", "name": "UROLOGIE ČERNOŠICE S.R.O., TÁBORSKÁ", "dn": null, "ico": "03298345", "addr": "TÁBORSKÁ, Táborská 2025, 252 28 ČERNOŠICE ", "icz": "29725000", "phone": "724773054", "email": "mhujo@centrum.cz", "icp": ["29725001","29906000"], "contacts": [{"name": "Marcel Hujo","phone": ""} ]}
]

Nyní mně již chybí pouze poslední dva kroky. Prvním je převést JSON na bean, a následně tento bean zapsat do configStorage.

Takže ještě roura rozšířená o tyto kroky:

from("scheduler://main?delay={{scheduler.delay}}&repeatCount={{scheduler.repeatCount}}").routeId("main-route")
        .process(exchange -> {
            exchange.getMessage().setBody(template.getInputStream());
            exchange.getIn().setHeader(Exchange.HTTP_METHOD, HttpEndpointBuilderFactory.HttpMethods.POST);
            exchange.getIn().setHeader("Content-Type", "application/xml;charset=UTF-8");
            exchange.getIn().setHeader("Accept", "application/xml");
            exchange.getIn().setHeader("Accept-Charset", "UTF-8");
        })
        .to("{{catalog.url}}")
        .setBody(xpath("/catalogInfo/catalogs/catalog/items/item/url", String.class))
        .process(exchange ->
                      exchange.getIn().setHeader(Exchange.HTTP_METHOD, HttpEndpointBuilderFactory.HttpMethods.GET))
        .toD("${body}")
        .unmarshal().zipFile()
        .split(xpath("/Participants/Participant/Clickbox")).streaming()
            .choice()
                .when(xpath("/Clickbox/cgmnumber"))
                    .to("xslt:templates/xml_to_json.xslt")
                    .unmarshal(new JacksonDataFormat(HealthCareProvider.class))
                    .process(exchange -> {
                        HealthCareProvider info = exchange.getMessage().getBody(HealthCareProvider.class);
                        configStorage.put(info.getOid(), info);
                    })
                .endChoice()
                .otherwise()
                    .setBody().simple("${null}")
                .endChoice()
            .end()
        .end();

Po spuštění již nejsou žádné výpisy. Pokud chcete, můžete si je do roury přidat.

Nyní bych měl mít data ve svém úložišti.

Kapitola šestá – stranická čistka

Je zde ještě jeden problém, se kterým se musím popasovat.

V každém běhu aktualizuji ty záznamy, které jsem již dříve načetl, případně přidávám nové. Může se stát, že v průběhu času nějaké zdravotnické zařízení ze seznamu vypadne, a takové bych měl ze své evidence také vyhodit. Které to ale jsou?

Půjdu na to takto:

  • při každém běhu si budu zaznamenávat identifikátory těch zařízení, která jsem aktualizoval nebo přidal (jsou v aktuální verzi XML seznamu).

  • Na závěr každého běhu zpracování si:

    • vytáhnu množinu všech identifikátorů ve vlastní evidenci

    • od ní odečtu množinu identifikátorů v aktuální verzi XML

    • všechny, které zbudou, vyřadím z evidence

To znamená, že při zpracování jednoho záznamu vrátím do roury jeho identifikátor.

Všechny tyto identifikátory agreguji do seznamu na úrovni direktivy split().

No a na závěr udělám tu čistku ve vlastní evidenci.

Upravená a také již konečná podoba roury vypadá takto:

from("scheduler://main?delay={{scheduler.delay}}&repeatCount={{scheduler.repeatCount}}").routeId("main-route")
        .process(exchange -> {
            exchange.getMessage().setBody(template.getInputStream());
            exchange.getIn().setHeader(Exchange.HTTP_METHOD, HttpEndpointBuilderFactory.HttpMethods.POST);
            exchange.getIn().setHeader("Content-Type", "application/xml;charset=UTF-8");
            exchange.getIn().setHeader("Accept", "application/xml");
            exchange.getIn().setHeader("Accept-Charset", "UTF-8");
        })
        .to("{{catalog.url}}")
        .setBody(xpath("/catalogInfo/catalogs/catalog/items/item/url", String.class))
        .process(exchange ->
                exchange.getIn().setHeader(Exchange.HTTP_METHOD, HttpEndpointBuilderFactory.HttpMethods.GET))
        .toD("${body}")
        .unmarshal().zipFile()
        .split(xpath("/Participants/Participant/Clickbox"), new AbstractListAggregationStrategy<URI>() {
            @Override
            public URI getValue(Exchange exchange) {
                return exchange.getIn().getBody(URI.class);
            }
        }).streaming()
            .choice()
                .when(xpath("/Clickbox/cgmnumber"))
                    .to("xslt:templates/xml_to_json.xslt")
                    .unmarshal(new JacksonDataFormat(HealthCareProvider.class))
                    .process(exchange -> {
                        HealthCareProvider info = exchange.getMessage().getBody(HealthCareProvider.class);
                        configStorage.put(info.getOid(), info);
                        exchange.getMessage().setBody(info.getOid());
                    })
                .endChoice()
                .otherwise()
                    .setBody().simple("${null}")
                .endChoice()
            .end()
        .end()
        .process(exchange -> {
            Set<URI> removeKeys = new HashSet<>(configStorage.keySet());
            exchange.getMessage().getBody(List.class).forEach(removeKeys::remove);
            removeKeys.forEach(oid -> configStorage.remove(oid));
        });

Po spuštění se bude průběžně aktualizovat moje evidence zdravotnických zařízení.

Kapitola poslední – epilog

Asi by bylo dobré si ověřit, že to skutečně funguje.

Vytvořil jsem si tedy ještě REST službu, která vrátí obsah mé vlastní evidence. A jak jinak, než v Camel (je součástí route/CamelRoutes):

restConfiguration().port("9091").bindingMode(RestBindingMode.json);
rest("/storage").produces("application/json")
        .get()
            .route()
                .process(exchange -> exchange.getMessage().setBody(configStorage));

A nyní se můžu podívat, jaké záznamy mám v evidenci:

[raska@fedora ~]$ curl -s http://localhost:9091/storage | jq keys
[
  "clickbox:01300094419",
  "clickbox:01300094508",
  "clickbox:01300095445",
  "clickbox:01300096291",
  "clickbox:01300097041",
  "clickbox:01300099401",
  "clickbox:01300099402",
  "clickbox:01300099703",
  "clickbox:01300099798",
  "clickbox:01300101144",
  "clickbox:01300101504",
  "clickbox:01300101562",
  "clickbox:01300101649",
  "clickbox:01300101799",
  "clickbox:01300101800",
  "clickbox:01300102395",
  "clickbox:01300102396",
  "clickbox:01300102397",
  "clickbox:01300103197",
  "clickbox:01300103592",
  "clickbox:01300103597",
  "clickbox:01300103606",
  "clickbox:01300103691",
  "clickbox:01300103744",
  "clickbox:01300103792",
  "clickbox:01300104142",
  "clickbox:01300104594",
  "clickbox:01300105043",
  "clickbox:01300105045",
  "clickbox:01300105281",
  "clickbox:01300105282",
  "clickbox:01300105335",
  "clickbox:01300105422",
  "clickbox:01300105428",
  "clickbox:01300105699",
  "clickbox:01300105741"
]

Případně se podívat na jeden konkrétní:

[raska@fedora ~]$ curl -s http://localhost:9091/storage | jq '."clickbox:01300094419"'
{
  "oid": "clickbox:01300094419",
  "name": "Ordinace ALC",
  "dn": null,
  "ico": "47902442",
  "addr": "Pracoviště ordinace ALC, Šumavská 519/35, 60200 Brno",
  "icz": "55667000",
  "phone": "246007851",
  "email": "ales.cerman@cgm.com",
  "icp": [
    "55667788"
  ],
  "contacts": []
}

A to je vše.

Sdílet