<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[https://pydyniak.pl RSS feed]]></title><description><![CDATA[https://pydyniak.pl RSS feed]]></description><link>https://pydyniak.pl</link><generator>GatsbyJS</generator><lastBuildDate>Tue, 23 Sep 2025 13:47:00 GMT</lastBuildDate><item><title><![CDATA[Tworzenie klastra w Liferayu - przewodnik]]></title><description><![CDATA[Witaj! W tym artykule chciałbym zaprezentować możliwości stworzenia klastra w CMSie Liferay. W moim przykładowym środowisku użyję Dockera, aczkolwiek kroki będą analogiczne jeżeli po prostu instalujesz w sposób "tradycyjny" dwie instancje (lub więcej) na jednym serwerze lub kilku. Postaram się pokazać cały proces tworzenia środowiska krok po prostu, tak abyś mógł jak najlepiej zrozumieć "jak to działa". Niemniej jednak gdybyś chciał od razu zobaczyć stworzone środowisko - jest ono dostępne na moim githubie]]></description><link>https://pydyniak.pl/tworzenie-klastra-w-liferayu-przewodnik/</link><guid isPermaLink="false">https://pydyniak.pl/tworzenie-klastra-w-liferayu-przewodnik/</guid><pubDate>Wed, 26 May 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;Uwaga&lt;/strong&gt; &lt;em&gt;Poniższy artykuł jest tłumaczeniem oryginału z angielskiej wersji strony. Jeżeli interesują Cię tego typu tematy to polecam czytać wersje angielską z uwagi na to, że artykuły zawsze w pierwszej kolejności tworzone są tam, a strona polska czasami nawet bardzo długo jest &quot;do tyłu&quot;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Witaj! W tym artykule chciałbym zaprezentować możliwości stworzenia klastra w CMSie Liferay. W moim przykładowym środowisku użyję Dockera, aczkolwiek kroki będą analogiczne jeżeli po prostu instalujesz w sposób &quot;tradycyjny&quot; dwie instancje (lub więcej) na jednym serwerze lub kilku.&lt;/p&gt;
&lt;p&gt;Postaram się pokazać cały proces tworzenia środowiska krok po prostu, tak abyś mógł jak najlepiej zrozumieć &quot;jak to działa&quot;. Niemniej jednak gdybyś chciał od razu zobaczyć stworzone środowisko - jest ono dostępne na &lt;a href=&quot;https://github.com/RafalPydyniak/liferay-docker-cluster&quot; target=&quot;_self&quot; rel=&quot;nofollow&quot;&gt;moim githubie&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Ważna sprawa - skupię się na konfiguracji Liferaya i prostocie całego środowiska. W związku z tym nie będę tworzył klastra bazy danych/Elasticsearcha/Load Balancera. Jeżeli byś tego potrzebował to rozszerzenie mojej konfiguracji powinno być dość proste, a w Internecie można znaleźć mnóstwo materiałów na te tematy. W moim przykładowym środowisku będzie jedynie podstawowa konfiguracja tych trzech, a klaster powstanie tylko dla Liferaya.&lt;/p&gt;
&lt;h2&gt;Wymagania wstępne&lt;/h2&gt;
&lt;p&gt;Aby rozpocząć będziesz potrzebował Dockera na swojej maszynie. Nie będę omawiał podstaw Dockera/konteneryzacji/Docker Compose więc dobrze by było abyś miał chociaż podstawowe pojęcie na ich temat, aczkolwiek nie jest to konieczne aby jako tako prześledzić artykuł.&lt;/p&gt;
&lt;p&gt;Jeżeli planujesz &quot;na bieżąco&quot; tworzyć środowisko podczas czytania to możesz rozpocząć od stworzenia nowego katalogu z plikami/katalogami jak na screenie poniżej. Pliki oczywiście na tym etapie mogą być puste.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 194px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 55.154639175257735%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAA7DAAAOwwHHb6hkAAABvUlEQVQoz5WSW3PSUBSF+/8ffPEf+BN0OrVoaUEtlUpoS+5QkJ7cSIEk3AuEz0MCDlUfcM+ss5OZnbXXOisnyEoXXdajGnltmM/npJvd22b3cGSdbI91XCZ13uAIwe33CqXzIh8LF7ie/5v0WOKccNpiObhkvU5J05StvCiKWS6XB4T/oTCNSxC+paWbFIuXlD5d8aDpLBYv2dB2SY5c6b7vVR+qzwhfxg7ToMJiNiGKI+IoIUmSo2z+OZMRKuYX7qvvEPYjmlRpqiqGYeL7Aabshq7xoNTRNI1ms5Xh/q7BbDr7t0LFKnOnXzAdTXFdj14Q4MtAZjLtsBcy6D/zs9XOFmyDEsKRMyGr1erVHW+RW16sGfYT3G6HJznsuS6O49Co1el2XXy5RDyJjDyUy8aTyaHnvy0P4xhPbu/3ehimhW03pXUDW7exrFxZ5fpGXoOF2mhgNR9Js7BkQJs9sp+Dk5U8vMGYjvBR2wGGM8R2I0wxpKZ10EWE1vZRbA/bixHxmnAuvxlvduAVMkLxnNB1A27VNmdlhcK3OqelKqeFG86vVb7+0Hn/ucqHYoUrxcaXWYhkgzNCYt9z/AK9rEE1hVtL5gAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Początkowa struktura plików&apos; title=&apos;Początkowa struktura plików&apos; src=&apos;/static/0697cd0895b8d853ff2cfba514f0837c/2bf95/initial-files-structure.png&apos; srcset=&apos;/static/0697cd0895b8d853ff2cfba514f0837c/2bf95/initial-files-structure.png 194w&apos; sizes=&apos;(max-width: 194px) 100vw, 194px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Początkowa struktura plików&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;W tym samym katalogu stwórz dwa foldery &quot;portal-node-1&quot; i &quot;portal-node-2&quot;:&lt;/p&gt;
&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 136px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 61.76470588235294%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAYAAABiDJ37AAAACXBIWXMAAA7DAAAOwwHHb6hkAAABhUlEQVQoz5WSW0sbURSF86cF/4Y+KV5qvSKmCQWF2sY7CD60JBpMRY19SeMkk8wlM5nLuXyeCbHFMmq6YHMOZ2/WWXvtXXi0bG4fWvT6Hla3h+34JKkgg9aat/Ccz06l1OgsdAzJxY86ZxeX3DV/cf69ge0GExHm1RReK5ok8moLI7kiQMuYNwXp99WNFGoZItsLCHt3XKSQUhKGQ4a+TzCMkMafDL5n/E0SBp7HIAjzW1bCJ21OgzVLo1altLXOl8ohG4srbK6sUms0+VoqUTk+YnFumZ1iiebPO1rtTj6hVgmpXUG65/S7Hdy+Q2qmnMQxsVGTITX3oVEaRTFCiHdaVhHKLiKd0xcJpTJvhSFN/7yJbJ0MR5JEDAbBay17iPspeJzh5uqKz9tb7B8csbn00bS8RvX6nm/lMoenJyzNf+BTscx1vUqtfjP+WP2rMCbt7qHcY6zfLax2myAM8RwH13XNQDSBGYJrotfrm2GFRuFfOybaw0mR7+FoIeU49H9FHukTXoqa9Jn8RTQAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Początkowa struktura katalogów &amp;quot;deploy&amp;quot;&apos; title=&apos;Początkowa struktura katalogów &amp;quot;deploy&amp;quot;&apos; src=&apos;/static/275a99b0a40d6a7fe220c278d7d83f9c/8604e/deploy-directory-initial-files-structure.png&apos; srcset=&apos;/static/275a99b0a40d6a7fe220c278d7d83f9c/8604e/deploy-directory-initial-files-structure.png 136w&apos; sizes=&apos;(max-width: 136px) 100vw, 136px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Początkowa struktura katalogów &amp;quot;deploy&amp;quot;&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;h3&gt;Docker storage - mountowanie, wolumeny (volumes)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Uwaga&lt;/strong&gt; Po opublikowaniu wstępnej wersji tego postu pomyślałem o jednej dodatkowej kwestii - w poniższych instrukcjach użyłem &quot;Docker volumes&quot; do przetrzymywania niektórych danych (Elasticsearch/dokumenty Liferaya), a inne &quot;zamountowałem&quot; (katalogi deploy: portal-node-1/portal-node-2). Dodatkowo niektóre pliki (konfiguracyjne, jary, portal-ext.properties) kopiuję używając &quot;Dockerfile&quot;, a niektórych danych (MySQL) nie zapisuję nigdzie.&lt;/p&gt;
&lt;p&gt;To podejście oczywiście pokazuje różne możliwości, ale z drugiej strony powinienem wspomnieć o tym, że jest to podejście &quot;pokazowe&quot;. Jeżeli planujesz stworzyć środowisko, które będzie realnie wykorzystywane to powinieneś najpierw zapoznać się ze wszystkimi możliwościami przechowywania plików. Prawdopodobnie najlepiej zacząć od &lt;a href=&quot;https://docs.docker.com/storage/&quot; target=&quot;_self&quot; rel=&quot;nofollow&quot;&gt;dokumentacji Dockera&lt;/a&gt;. Jest to istotne gdyż każda metoda przechowywania danych ma swoje plusy i minusy.&lt;/p&gt;
&lt;h2&gt;Proces tworzenia środowiska&lt;/h2&gt;
&lt;h3&gt;Kontenery Liferaya&lt;/h3&gt;
&lt;p&gt;Chcemy stworzyć klaster, a więc potrzebujemy dwóch lub więcej instancji Liferaya. W przypadku tego tutorialu użyję dwóch, ale użycie większej liczby jest analogiczne.&lt;/p&gt;
&lt;p&gt;Tak więc od początku - jak w ogóle zacząć? Skoro chcemy użyć Docker Compose potrzebujemy stworzyć plik docker-compose.yml. Plik ten służy do definicji tego jakie kontenery chcemy mieć stworzone oraz opisuje to jak mają one zostać stworzone (np. gdzie chcemy przechować dane kontenera).
Aby jednak rozpocząć pracę nad docker-compose.yml potrzebujemy obrazów dla naszych kontenerów. W większości użyjemy gotowych, już zbudowanych obrazów, ale w przypadku Liferaya stworzymy własny obraz, który rozszerzy oficjalny.&lt;/p&gt;
&lt;p&gt;Aby tego dokonać tworzymy plik &quot;Dockerfile-liferay&quot; z następującą treścią:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FROM liferay/portal:7.0.6-ga7
MAINTAINER Rafal Pydyniak &amp;#x3C;rafal@pydyniak.pl&gt;

COPY ./configs/portal-ext.properties $LIFERAY_HOME/
COPY ./configs/\*.config $LIFERAY_HOME/osgi/configs/
COPY ./configs/\*.jar $LIFERAY_HOME/osgi/portal/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Tak więc tworzymy nasz własny obraz na bazie oficjalnego w wersji 7.0.6 GA7. Użyłem takiej wersji, ale wszystkie 7+ będą działały podobnie. W naszej wersji obrazu dodajemy kopiowanie portal-ext.properties do katalogu LIFERAY_HOME a także kopiujemy niezbędne konfiguracje/JARy o czym będzie później. Moglibyśmy oczywiście zamiast tego zmapować nasz dysk na dysk kontenera, ale dla przykładu użyjemy tej metody szczególnie, że pliki konfiguracyjne/JARy nie będą się zmieniały w czasie. Prawdopodobnie co do portal-ext.properties lepiej byłoby go zmapować, ale póki co zostawmy to w ten sposób.&lt;/p&gt;
&lt;p&gt;Jeżeli chciałbyś przetestować obraz możesz to zrobić budując go, uruchamiając a następnie łącząc się do kontenera. Następnie możesz zweryfikować czy pliki rzeczywiście zostały skopiowane. Poniższe komendy pomogą Ci w tym zadaniu:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker build -t my-liferay-7-tutorial -f Dockerfile-liferay . # budujemy &quot;Dockerfile-liferay&quot;. Stworzony obraz będzie się nazywał &quot;my-liferay-7-tutorial&quot;. Kropka na końcu służy do wskazania tak zwanego &quot;context directory&quot; czyli katalogu, który ma być &quot;wyjściowym&quot; między innymi przy kopiowaniu w komendach COPY.
docker run -d my-liferay-7-tutorial # Uruchamiamy stworzony obraz
docker exec -it CONTAINER_NAME /bin/bash # Podłączamy się do kontenera, tak aby przetestować czy wszystko działa poprawnie. Odpowiednią nazwę kontenera (do podstawienia pod CONTAINER_NAME) możesz sprawdzić używajć komendy docker container ls.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Komentarze do komend opisują działanie.&lt;/p&gt;
&lt;h3&gt;Stwórzmy docker-compose.yml i wykorzystajmy nasz obraz&lt;/h3&gt;
&lt;p&gt;Kolejnym krokiem będzie stworzenie kontenerów Liferaya czyli naszych instancji. Wykorzystamy do tego wspomniany wcześniej docker-compose.yml. Pierwsza wersja tego pliku poniżej:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;version: &apos;3.3&apos;
services:
  liferay-portal-node-1:
    build:
      context: .
      dockerfile: Dockerfile-liferay
    ports:
      - &apos;6080:8080&apos;
      - &apos;21311:11311&apos;
    hostname: liferay-portal-node-1.local
    volumes:
      - lfr-dl-volume:/opt/liferay/data/document_library
      - ./deploy/portal-node-1:/opt/liferay/deploy
    depends_on:
      - mysql
      - es-node-1
  liferay-portal-node-2:
    build:
      context: .
      dockerfile: Dockerfile-liferay
    ports:
      - &apos;7080:8080&apos;
      - &apos;31311:11311&apos;
    hostname: liferay-portal-node-2.local
    volumes:
      - lfr-dl-volume:/opt/liferay/data/document_library
      - ./deploy/portal-node-2:/opt/liferay/deploy
    depends_on:
      - mysql
      - liferay-portal-node-1
      - es-node-1
volumes:
  lfr-dl-volume:
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Zdefiniowaliśmy więc dwie instancje Liferaya:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;liferay-portal-node-1&lt;/li&gt;
&lt;li&gt;liferay-portal-node-2&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;wykorzystując stworzony wcześniej obraz oraz ustawiając aktualny katalog jako &quot;context&quot; (wykorzystywany do wskazania skąd obraz ma brać pliki). Dodatkowo definiujemy porty, które mają być dostępne poza kontenerem. W ramach przypomnienia: zapis &quot;6080:8080&quot; oznacza, że port 8080 wewnątrz kontenera będzie dostępny na porcie 6080 &quot;na zewnątrz&quot;. Tak więc wszystkie dostępne porty to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;6080 - instancja nr 1 (node1)&lt;/li&gt;
&lt;li&gt;7080 - instancja nr 2 (node2)&lt;/li&gt;
&lt;li&gt;21311 - gogo shell (do obsługi OSGI) instancji nr 1&lt;/li&gt;
&lt;li&gt;31311 - gogo shell (do obsługi OSGI) instancji nr 2&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Ustawiamy również hostname dla naszych instancji, tak aby móc je rozróżnić oraz używamy volumes do dwóch rzeczy:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Przechowywania danych documents &amp;#x26; media jako wolumen dockerowy. Używamy tego samego wolumenu dla obu instancji dzięki czemu pliki są współdzielone między instancjami. Jest to krok wymagany przy klastrowaniu w Liferayu&lt;/li&gt;
&lt;li&gt;Mapujemy nasze lokalne deploy/portal-node-X na katalog &lt;em&gt;deploy&lt;/em&gt; w kontenerze. W ten sposób możemy łatwo deployować nasze moduły bezpośrednio do kontenerów&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Na końcu definiujemy także wszystkie nasze wolumeny - póki co jedynie &quot;lfr-dl-volume&quot;&lt;/p&gt;
&lt;p&gt;Możesz także zwrócić uwagę na fragment &quot;depends_on&quot;. Jest to po prostu deklaracja tego, który kontener &quot;zależy&quot; od którego. W ten sposób możemy zdefiniować, że chociażby nasze instancje Liferaya nie powinny startować przed bazą danych czy Elasticsearchem co powodowałoby błędy. Na ten moment oczywiście nie mamy w ogóle zdefiniowanej bazy/Elasticsearcha, ale niedługo do tego przejdziemy.&lt;/p&gt;
&lt;h4&gt;Documents &amp;#x26; media&lt;/h4&gt;
&lt;p&gt;Warto zwrócić uwagę, że produkcyjnie prawdopodobnie powinieneś użyć &quot;Advanced File System Store&quot; jako repozytorium danych zamiast domyślnego &quot;simple&quot;. W tym wypadku użyłem opcji domyślnej. Zmiana na &quot;Advanced...&quot; jest zalecana, aczkolwiek nie wymagana do klastrowania. Jeżeli chciałbyś dowiedzieć się więcej o repozytoriach danych w Liferayu możesz sprawdzić &lt;a href=&quot;https://help.liferay.com/hc/en-us/articles/360017895932-Document-Repository-Configuration&quot; target=&quot;_self&quot; rel=&quot;nofollow&quot;&gt;ten artykuł&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Konfiguracja bazy danych&lt;/h3&gt;
&lt;p&gt;Do uruchomienia Liferaya niezbędna jest baza danych (nie chcemy używać &quot;domyślnej&quot; i &quot;wbudowanej&quot; H2). W docker-compose dodajemy poniższy blok:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysql:
    image: mysql:5.6
    environment:
      MYSQL_ROOT_PASSWORD: my-secret-pw
      MYSQL_DATABASE: lportal
    ports:
      - &quot;33066:3306&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Jest on raczej dość jasny. Używamy oficjalnego obrazu, udostępniamy porty oraz ustawiamy hasło dla roota i tworzymy bazę danych przez zmienne środowiskowe. W tym wypadku używam MySQL, ale równie dobrze możesz użyć dowolnej innej - w większości wypadków z łatwością znajdziesz odpowiedni obraz na &lt;a href=&quot;https://hub.docker.com/&quot; target=&quot;_self&quot; rel=&quot;nofollow&quot;&gt;DockerHub&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Musimy oczywiście również zdefiniować połączenie do naszej bazy danych w samym Liferayu. Aby tego dokonać używamy pliku konfiguracyjnego portal-ext.properties. Plik ten kopiowany jest do kontenera podczas tworzenia naszego obrazu co zdefiniowaliśmy wcześniej. Konfiguracja bazy będzie następująca:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;jdbc.default.driverClassName=com.mysql.jdbc.Driver
jdbc.default.url=jdbc:mysql://mysql:3306/lportal
jdbc.default.username=root
jdbc.default.password=my-secret-pw
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Możesz oczywiście dodać tutaj dowolne inne wpisy konfiguracyjne tak jak byś to zrobił w kazdym innym przypadku. Wrócimy jeszcze do tego pliku później, podczas konfiguracji klastra.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Uwaga:&lt;/strong&gt; Jak wspominałem wcześniej - w powyższej konfiguracji nigdzie nie przechowujemy danych bazy danych. Usunięcie kontenera spowoduje utrate danych. Aby tego uniknąć możesz stworzyć volume (zobacz konfiguracje Elasticsearch dla przykładu) lub stworzyć dodatkowy katalog i zmapować go:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;volumes:  
- ./data/mysql:/var/lib/mysql
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Konfiguracja Elasticsearch&lt;/h3&gt;
&lt;p&gt;Jednym z kroków tworzenia klastra, opisanym w dokmentacji, jest instalacja osobnej instancji Elasticsearch (ES). W związku z faktem, że tworzymy klaster nie możemy użyć &quot;wbudowanego&quot;, gdyż wszystkie nasze instancje muszą komunikować się z tym samym serwerem ES (lub serwerami w klastrze ES). W naszym wypadku potrzebujemy wersji 2.4.6, ale jeżeli używasz innej wersji Liferaya możesz potrzebować innej. Sprawdź &lt;a href=&quot;https://help.liferay.com/hc/en-us/articles/360017895852-Installing-Elasticsearch-&quot; target=&quot;_self&quot; rel=&quot;nofollow&quot;&gt;artykuł opisujący instalacje instancji ES&lt;/a&gt; gdzie opisany jest proces sprawdzania wersji.&lt;/p&gt;
&lt;p&gt;Gdy już znajdziesz odpowiednią wersje musisz dodać tworzenie serwera ES do pliku docker-compose.yml. W moim przypadku w wersji 2.4.6 wygląda to jak poniżej:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;es-node-1:
    image: elasticsearch:2.4.6-alpine
    environment:
      - cluster.name=elasticsearch
      - bootstrap.memory_lock=true
      - &quot;ES_JAVA_OPTS=-Xms512m -Xmx512m&quot;
    ulimits:
      memlock:
        soft: -1
        hard: -1
    volumes:
      - esdata1:/usr/share/elasticsearch/data
    ports:
      - 9200:9200
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Czyli po raz kolejny. Na początku definiujemy obraz, którego chcemy użyć. Następnie mamy dodatkowe opcje konfiguracyjne, które są raczej dość jasne np. ustawienia pamięci oraz kilka ustawień, które mogą być mniej jasne, ale łatwo odnaleźć ich znaczenie w google (szczególnie &lt;em&gt;ulimits&lt;/em&gt;). Istotna część to &quot;&lt;em&gt;cluster.name&lt;/em&gt;&quot; - musi być ona unikalna dla twojego środowiska, ale jednocześnie może to być dowolny String. W naszym wypadku oczywiście dowolna nazwa będzie unikalna, ale jeżeli przykładowo na jednym serwere masz dwa środowiska (np. DEV i TEST) to musisz zwrócić uwagę, aby instancje ES miały inne cluster.name, tak aby można było je rozróżnić.&lt;/p&gt;
&lt;p&gt;Możesz też zauważyć, że używamy wolumenu dockerowego o nazwie &lt;em&gt;esdata1&lt;/em&gt;. Pamiętajmy, że musimy dodać jego definicje również na końcu naszego docker-compose. Nasza definicja wolumenów będzie teraz wyglądała następująco:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;volumes:
  lfr-dl-volume:
  esdata1:
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Użycie zewnętrznego Elasticsearcha w Liferayu&lt;/h3&gt;
&lt;p&gt;Skonfigurowaliśmy naszą instancje ES, a teraz musimy jej jeszcze użyć w Liferayu. Aby tego dokonać użyjemy pliku konfiguracyjnego OSGI. Stwórzmy plik o nazwie &quot;&lt;em&gt;com.liferay.portal.search.elasticsearch.configuration.ElasticsearchConfiguration.config&lt;/em&gt;&quot; w katalogu &quot;configs&quot; o następującej zawartości:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;operationMode=&quot;REMOTE&quot;
transportAddresses=&quot;es-node-1:9300&quot;
clusterName=&quot;elasticsearch&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Kod ten sprawia, że Liferay będzie używał zewnętrznej instancji ES zamiast wbudowanej. Ustawiamy również wcześniej zdefiniowany clusterName oraz adress naszej instancji ES. Jeżeli zastanawiasz się dlaczego używamy &lt;em&gt;es-node-1:9300&lt;/em&gt; a nie jakiegoś adresu IP typu 127.0.0.1 czy localhost to jest tak dlatego, że wszystkie serwisy zdefiniowane w docker-compose są w tej samej sieci dockerowej, a co za tym idzie są przez siebie osiągalne przez ich nazwę kontenera. Bindowanie adresów jest obsługiwane przez samego Dockera - generalnie dodawane są odpowiednie wpisy w pliku /etc/hosts.&lt;/p&gt;
&lt;p&gt;Nic więcej nie musimy robić, gdyż już wcześniej zdefiniowaliśmy, że wszystkie pliki konfiguracyjne mają być kopiowane do LIFERAY_HOME/osgi/configs - zrobiliśmy to w naszym obrazie Liferaya.&lt;/p&gt;
&lt;h3&gt;Używamy wersji &quot;multiple&quot; niektórych modułów&lt;/h3&gt;
&lt;p&gt;W Liferayu 7.0 CE, którego używamy na potrzeby tego artykułu musimy dokonać dodatkowego kroku opisanego &lt;a href=&quot;https://help.liferay.com/hc/en-us/articles/360028831552-Building-Clustering-for-Liferay-Portal&quot; target=&quot;_self&quot; rel=&quot;nofollow&quot;&gt;tutaj&lt;/a&gt;. Generalnie domyślnie ta wersja Liferaya nie wspiera uruchamiania w trybie klastra. Aby dodać tą funkcje musimy &quot;podmienić&quot; trzy moduły:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;com.liferay.portal.cache.single&lt;/li&gt;
&lt;li&gt;com.liferay.portal.cluster.single&lt;/li&gt;
&lt;li&gt;com.liferay.portal.scheduler.single&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Na odpowiednie wersje z końcówką &quot;multiple&quot;. Te domyślne jednocześnie musimy zdezaktowywać. W artykule wspomnianym wyżej opisane są kroki kompilacji tych modułów więc nie będę wchodził w szczególy. Opiszę jedynie co gdy już je zdobędziemy.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Uwaga:&lt;/strong&gt; Jeżeli również używasz wersji 7.0.6 to możesz te moduły po prostu pobraż z przykładowego repozytorium GITa, które podawałem wcześniej&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Uwaga 2&lt;/strong&gt;: Możliwe, że w ogóle nie potrzebujesz wykonywać tych kroków jeżeli Twoja wersja już wspiera moduły w wersji &quot;multiple&quot;. Jeżeli używasz Liferaya w wersji DXP to z pewnością tak jest. Jeżeli wersji CE to możesz to sprawdzić w gogo shellu - po prostu uruchom komendę &quot;&lt;em&gt;lb portal.scheduler&lt;/em&gt;&quot; i sprawdź czy jest uruchomiony moduł &quot;Liferay Portal Scheduler Multiple&quot;. Jeżeli tak to pomiń ten krok.&lt;/p&gt;
&lt;p&gt;Gdy już masz wersje &quot;multiple&quot; modułów musisz:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;zdeployować je do katalogu &quot;configs&quot; w kontenerze - stworzony przez nasz obraz Liferaya to zrobi. Wystarczy, że wrzucisz moduły do katalogu &quot;configs&quot;&lt;/li&gt;
&lt;li&gt;włączyć ignorowanie wersji &quot;single&quot; - aby tego dokonać stwórz nowy plik &quot;&lt;em&gt;com.liferay.portal.bundle.blacklist.internal.BundleBlacklistConfiguration.config&lt;/em&gt;&quot; w katalogu &quot;configs&quot; z następującą treścią &lt;em&gt;blacklistBundleSymbolicNames=&quot;com.liferay.portal.cache.single,com.liferay.portal.cluster.single,com.liferay.portal.scheduler.single&quot;&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Obraz Dockerfile-Liferay zrobi resztę czyli skopiuje pliki do odpowiednich katalogów.&lt;/p&gt;
&lt;h3&gt;Włączenie klastra&lt;/h3&gt;
&lt;p&gt;Ok, przygotowaliśmy już dość dużo, ale nadal nie dodaliśmy żadnych ustawień odnośnie klastrowania do samego Liferaya. Na szczęście ten krok będzie bardzo prosty - jedyne co musimy zrobić to dodać poniższą linijkę do pliku portal-ext.properties:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cluster.link.enabled=true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Czy to wystarczy? Okazuję się, że tak - przynajmniej w niektórych przypadkach. Generalnie ustawienie te sprawia, że instancje Liferaya zaczynają używać kanału JGroups do komunikacji między sobą na domyślnych IP i portach. Komunikacja odbywa się za pomocą połączenia UDP multicast co jest bardzo istotne gdyż zadziała to &lt;strong&gt;tylko&lt;/strong&gt; jeżeli instancje są w stanie komunikować się za pomocą multicasta. Niestety nie zawsze tak jest. Nawet jeżeli Twoje serwery są w tej samej podsieci to i tak komunikacja przez multicasta może nie działać. Przykładowo Azure nie wspiera multicasta w swoich wirtualnych sieciach i nie ma planów na dodanie tego, przynajmniej gdy to sprawdzałem jakiś czas temu. W takim wypadku najlepiej sprawdzić dokumentacje Liferaya gdzie opisane są inne metody - sam korzystałem z opcji unicasta przez TCP i nie jest to specjalnie trudne - dochodzi jedynie drobna konfiguracja.&lt;/p&gt;
&lt;p&gt;W naszym wypadku jednak takie rozwiązanie jest jak najbardziej wystarczające, gdyż serwisy w ramach jednego docker-compose.yml mogą komunikować się przy użyciu multicasta.&lt;/p&gt;
&lt;p&gt;Ostatnia rzecz, którą możemy dodać do pliku portal-ext.properties to:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;web.server.display.node=true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Konfiguracja ta sprawi, że każda instancja w klastrze będzie wyświetlać informacje o tym jaki to serwer. Jest to przydatne, gdyż normalnie wchodząc na wspólny adres nie wiemy z którym serwerem się łączymy. Informacja taka przydaję się do testów tego czy klaster rzeczywiście działa.&lt;/p&gt;
&lt;p&gt;Jeżeli chcesz poczytać więcej o tym jak działa komunikacja to najlepiej sprawdzić &lt;a href=&quot;https://help.liferay.com/hc/en-us/articles/360018175191-Liferay-DXP-Clustering-#4-enable-cluster-link&quot; target=&quot;_self&quot; rel=&quot;nofollow&quot;&gt;oficjalną dokumentacje&lt;/a&gt;. Jest to dość proste i kilka minut powinno wystarczyć, żeby zrozumieć idee.&lt;/p&gt;
&lt;h3&gt;Load balancing&lt;/h3&gt;
&lt;h4&gt;Wstęp&lt;/h4&gt;
&lt;p&gt;Od razu zaznaczę - krok ten jest w pełni opcjonalny. Nasza konfiguracja da nam dwie instancje na dwóch różnych portach. Jeżeli robisz to np. na serwerze to możesz już mieć jakiegoś load balancera czy serwer HTTP (np. Apache/Nginx) i wtedy konfiguracje powinieneś najlepiej przeprowadzić tam a nie dodawać kolejną warstwę w Docker Compose.
Jeżeli jednak nie masz niczego co mogłoby posłużyć za reverse proxy i load balancera to pokaże jak dodać load balancing przy użyciu HAProxy, który zdefiniujemy jako kolejny serwis w Docker Compose.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Uwaga&lt;/strong&gt; jeżeli pomijasz ten krok i chcesz dokonać konfiguracji sam to należy zwrócić uwagę, że do działania niezbędne jest ustawienie tak zwanego &quot;sticky session&quot;. Odpowiednie instrukcje na pewno znajdziesz w dokumentacji danego narzędzia.&lt;/p&gt;
&lt;h4&gt;Konfiguracja Dockerowa&lt;/h4&gt;
&lt;p&gt;Jeżeli jednak wybierasz load balancing po stronie Docker Compose to poniżej przedstawiam niezbędne kroki. Load balancing jest potrzebny bo oczywiście nie chcemy, aby user sam musiał wybierać odpowiedni serwer, a zamiast tego powinien móc wejść na jeden wspólny adres np. &lt;em&gt;app.pydyniak.com&lt;/em&gt; (nic tam nie ma, nie musisz sprawdzać ;)). Rozdzielanie ruchu powinno nastomiast działać &quot;w tle&quot;. Poniższy diagram pokazuje mniej więcej idee rozwiązania:
&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 541px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 40.86378737541528%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAICAYAAAD5nd/tAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA30lEQVQoz3WSCQqFMAxEe/+rCZ5CcN/qvubzCim18AdKrU4nk4lm33eZ51nWdXU763keiXHft1hrZds2WZbF88H7vm4BU9e1FEUh7F3XSZZlUlWVv6iX27aVcRyl73vHRwyOCinMcRwyTZN3h8B1Xb6quh2GQZqm+XSCsAoq36hyaDs+I0pRfafvcUiBj2BsORYGuFanoSOAIB35lsOqoRhRQKRVsjvP0w0mFIPDUMmcguRrYne64wqhsizdQDjTYsgjRwaZ57l7TtP0vyCZ8Ztwgcqcw7wA33VYFEuSRH6LS3EMQj3ubAAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Diagram architektury&apos; title=&apos;Diagram architektury&apos; src=&apos;/static/9a3573a331c7ce840bc91d42215b9f19/9d576/diagram.png&apos; srcset=&apos;/static/9a3573a331c7ce840bc91d42215b9f19/fb933/diagram.png 301w,
/static/9a3573a331c7ce840bc91d42215b9f19/9d576/diagram.png 541w&apos; sizes=&apos;(max-width: 541px) 100vw, 541px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Diagram architektury&lt;/figcaption&gt;
  &lt;/figure&gt;
Tak jak wspominałem ja użyję HAProxy, ale możesz użyć dowolnego innego narzędzia. Krok ten jest zupełnie niezależny od Liferaya. Jedyne co to należy ustawić sticky session - patrz uwaga wyżej&lt;/p&gt;
&lt;p&gt;A więc tak: tworzymy kolejny obraz - tym razem o nazwie &quot;Dockerfile-haproxy&quot; i kopiujemy poniższą konfiguracje:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FROM haproxy:2.3.4
COPY ./configs/haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nie ma tutaj raczej nic specjalnego. Tworzymy po prostu nasz obraz na bazie oficjalnego obrazu &lt;em&gt;haproxy:2.3.4&lt;/em&gt; i dodajemy kopiowanie pliku konfiguracyjnego. Jest to sposób zalecany zresztą w dokumentacji obrazu na Docker Hubie. Następnie definiujemy tworzenie naszego serwisu HAProxy w pliku docker-compose.yml:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;haproxy:
    build:
      context: .
      dockerfile: Dockerfile-haproxy
    ports:
      - &apos;80:80&apos;
    hostname: lb-haproxy.local
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Wskazujemy więc na nasz obraz (dockerfile) i używamy aktualnego katalogu jako kontekstu. Mapujemy też port wewnątrz kontenera (80) na port naszej maszyny (również 80). Ustawiamy też hostname.&lt;/p&gt;
&lt;p&gt;Teraz czas na wspomniany wcześniej plik haproxy.cfg, którego kopiowanie zdefiniowaliśmy w Dockerfile. Tworzymy więc ten plik i wrzucamy następującą konfiguracje:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;global
   stats timeout 30s
   daemon

defaults
   log global
   mode http
   option httplog
   option dontlognull
   timeout connect 5000
   timeout client 50000
   timeout server 50000

frontend http_front
   bind 0.0.0.0:80
   default_backend http_back

backend http_back
   balance roundrobin
   cookie JSESSIONID prefix
   server liferay-portal-node-1 liferay-portal-node-1:8080 check cookie s1
   server liferay-portal-node-2 liferay-portal-node-2:8080 check cookie s2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Jest to dość długa konfiguracja, ale co najważniejsza robi to czego potrzebujemy. Najważniejsze linijki są na końcu i zaczynają się od &quot;&lt;em&gt;backend http_back&lt;/em&gt;&quot;. W linijkach tych robimy dwie rzeczy:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Ustawiamy sposób rozdzielania ruchu na roundrobin - jest to po prostu jeden ze standardowych sposobów dzielenia ruchu, w którym kolejne żądania są na zmiane przydzielane do serwerów. Dokładny opis można oczywiście znaleźć chociażby na Wikipedii&lt;/li&gt;
&lt;li&gt;Definiujemy nasze serwery oraz używamy wspomnianego wcześniej &quot;sticky session&quot;. Jeżeli nie wiesz czym sticky session jest to już tłumaczę - chodzi o to, że jeżeli user wejdzie na naszą stronę i trafi przykładowo na NODE2 (drugą instancje) to będzie przydzielony do tego samego serwera przez cały czas trwania sesji. Jest to niezbędne, gdyż sesja &lt;strong&gt;nie jest&lt;/strong&gt; dzielona czy też kopiowana między instancjami Liferaya. Dokładniejszy opis jak sticky session działa w HAProxy można znaleźć &lt;a href=&quot;https://www.haproxy.com/blog/load-balancing-affinity-persistence-sticky-sessions-what-you-need-to-know/&quot; target=&quot;_self&quot; rel=&quot;nofollow&quot;&gt;w artykule na blogu HAProxy&lt;/a&gt; - zachęcam do przejrzenia. Idea jest bardzo prosta więc jeżeli nigdy wcześniej nie miałeś z tym do czynienia to warto poczytać.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Uruchamiamy nasz klaster&lt;/h2&gt;
&lt;p&gt;W końcu koniec. Zdefiniowaliśmy wszystko czego potrzebowaliśmy. Mam nadzieję, że opis był zrozumiały i wiesz dlaczego zrobiliśmy to co zrobiliśmy. Teraz potrzebujemy uruchomić nasze środowisko. Możemy tego dokonać komendami poniżej:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker-compose build # budujemy nasze całe środowisko
docker-compose up -d liferay-portal-node-1 # uruchamiamy pierwszy serwer
# Czekamy aż wystartuje. Możesz sprawdzić docker-compose logs, aby zwalidować na jakim etapie jest start serwera
docker-compose up -d liferay-portal-node-2 # uruchamiamy drugi serwer
docker-compose up -d haproxy # uruchamiamy nasze Reverse Proxy
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Podczas uruchamiania możemy sprawdzić logi (&lt;em&gt;docker-compose logs&lt;/em&gt;) i zweryfikować, że rzeczywiście instancje komunikują się między sobą:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;liferay-portal-node-1_1  | 2021-01-24 23:10:59.862 INFO  [Incoming-2,liferay-channel-control,liferay-portal-node-1-3920][JGroupsReceiver:90] Accepted view [liferay-portal-node-1-3920|1] (2) [liferay-portal-node-1-3920, liferay-portal-node-2-36188]
liferay-portal-node-1_1  | 2021-01-24 23:11:00.057 INFO  [Incoming-2,liferay-channel-transport-0,liferay-portal-node-1-47303][JGroupsReceiver:90] Accepted view [liferay-portal-node-1-47303|1] (2) [liferay-portal-node-1-47303, liferay-portal-node-2-30594]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Jeżeli po uruchomieniu wszystkiego wejdziesz na serwer na port :80 zobaczysz, że zostałeś połączony do jednej z dwóch instancji (jeżeli oczywiśćie dodałeś wyświetlanie wiadomości o czym było wcześniej):
&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 398px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 20.26578073089701%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAECAYAAACOXx+WAAAACXBIWXMAABJ0AAASdAHeZh94AAAAl0lEQVQY031OSQ7CMBDr/78HKVsLHCBJm0TQTCnNaRgnIKRKcLBs2Z6lcnHmMCYOlNgLoD19EX5ov+j3sgeoLrcn77uRm/7BJz/xTnTrim7dxAfxwfDQOYfiHyXHXG2IG8nMkFjf3wuVjrwWYEBJAXprKZeVpnykFkaGQxtTMvDqGvNR+1kI0eeXE3ex8D8sO46KZ4c54wV16i5OVtD76QAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Informacja o tym na którym jesteśmy serwerze&apos; title=&apos;Informacja o tym na którym jesteśmy serwerze&apos; src=&apos;/static/0be085a0c742c72940772af18fff305d/692d4/Node-information.png&apos; srcset=&apos;/static/0be085a0c742c72940772af18fff305d/fb933/Node-information.png 301w,
/static/0be085a0c742c72940772af18fff305d/692d4/Node-information.png 398w&apos; sizes=&apos;(max-width: 398px) 100vw, 398px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Informacja o tym na którym jesteśmy serwerze&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;Możesz też spróbować połączyć się do serwera z innej przeglądarki - prawdopodobnie zostaniesz połączony tym razem do drugiej instancji. Jeżeli tak to super - udało się. Niestety to nie koniec testów. Na ten moment wiemy, że:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;W teorii mamy połączenie między serwerami (patrząc na logi)&lt;/li&gt;
&lt;li&gt;HAProxy wydaję się działać bo jesteśmy przekierowywani na odpowiednie instancje. Można ewentualnie też sprawdzić cookiesy, aby to potwierdzić (szczegóły we wspomnianym wcześniej artykule o tym jak działa sticky session w HAProxy)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;W praktyce nadal jednak powinniśmy dokonać kolejnych testów przykładowo:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Czy działa Elasticsearch - czy jeżeli dodamy jakiś content na node1 to czy będzie można go wyszukać na node2?&lt;/li&gt;
&lt;li&gt;Czy połączenie rzeczywiście działa? Czy jeżeli dodamy jakiś content na node2 to czy będzie widoczny na node1?&lt;/li&gt;
&lt;li&gt;Czy pliki Documents &amp;#x26; Media rzeczywiście są współdzielone? Czy plik dodany na jednej z instancji będzie widoczny na drugiej?&lt;/li&gt;
&lt;li&gt;Czy jeżeli zatrzymamy jedną z instancji - czy ruch zostanie odpowiednio przekierowany do drugiej?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Oczywiście możesz też pomyśleć o innych testach szczególnie jeżeli planujesz uruchomić podobną konfiguracje na produkcji.&lt;/p&gt;
&lt;h3&gt;Wydajność&lt;/h3&gt;
&lt;p&gt;Jeżeli uruchomisz środowisko to możesz zauważyć, że serwery nie są zbyt szybkie. Powód tego jest prosty - nie zmienialiśmy żadnych ustawień pamięci, a domyślne są dość niskie. Możesz dodać więcej pamięci poprzez stworzenie pliku configs/setenv.sh. Zawartość tego pliku można skopiować z serwera, a następnie jedynie nadpisać ustawienia dla pamięci. Następnie musisz jeszcze dodać odpowiedni wpis COPY w Dockerfile-liferay - plik ten musi być kopiowany do /opt/liferay/tomcat-8.0.32/bin/ ($LIFERAY_HOME/tomcat-8.0.32/bin/). W przykładowym repozytorium krok ten został dodany więc możesz podejrzeć jak zostało to zrobione.
Dodatkowa rzecz, którą należy wziąć pod uwagę - możliwe, że potrzebne będzie zwiększenie zasobów dostępnych Dockerowi, gdyż domyślnie są one mocno ograniczone.&lt;/p&gt;
&lt;h2&gt;Na koniec&lt;/h2&gt;
&lt;p&gt;Mam nadzieję, że artykuł był zrozumiały. Całość trochę się rozciągnęła i powstało dość dużo tekstu, ale starałem się jak najbardziej wytłumaczyć co zrobiłem i dlaczego w taki a nie inny sposób. Sam projekt Docker Compose jest dość niewielki więc jeżeli tylko rozumiesz podstawy Dockera to myślę, że nie powinno być problemu z jego uruchomieniem. W artykule tym użyłem wersji 7.0.6, ale kroki są analogiczne również dla innych wersji. Raz jeszcze przypominam, że cały kod można znaleźć na &lt;a href=&quot;https://github.com/RafalPydyniak/liferay-docker-cluster&quot; target=&quot;_self&quot; rel=&quot;nofollow&quot;&gt;repozytorum na Githubie&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Jeżeli masz jakiekolwiek pytania jak zawsze zachęcam do &lt;a href=&quot;/kontakt/&quot;&gt;kontaktu&lt;/a&gt;.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Dlaczego migracje projektów Liferayowych na wyższą wersje nie są proste cz. 2]]></title><description><![CDATA[Witam! W pierwszej części Dlaczego migracje projektów Liferayowych na wyższą wersje nie są proste przedstawiłem jak mniej więcej w ogóle wyglądają migracje w projektach Liferayowych oraz pokazałem kilka przykładów potencjalnych problemów, które mogą nas spotkać przy migracji bazy danych. W tej części postaram się pokazać kilka problemów, które mogą się pojawić podczas migrowania samego kodu czyli…]]></description><link>https://pydyniak.pl/dlaczego-migracje-projektow-liferayowych-na-wyzsza-wersje-nie-sa-proste-cz-2/</link><guid isPermaLink="false">https://pydyniak.pl/dlaczego-migracje-projektow-liferayowych-na-wyzsza-wersje-nie-sa-proste-cz-2/</guid><pubDate>Wed, 06 Jan 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Witam! W pierwszej części &lt;a href=&quot;/dlaczego-migracje-projektow-liferayowych-nie-sa-proste/&quot;&gt;Dlaczego migracje projektów Liferayowych na wyższą wersje nie są proste&lt;/a&gt; przedstawiłem jak mniej więcej w ogóle wyglądają migracje w projektach Liferayowych oraz pokazałem kilka przykładów potencjalnych problemów, które mogą nas spotkać przy migracji bazy danych. W tej części postaram się pokazać kilka problemów, które mogą się pojawić podczas migrowania samego kodu czyli naszych customowych modułów m.in. portletów.&lt;/p&gt;
&lt;h2&gt;Jak migrować kod&lt;/h2&gt;
&lt;p&gt;Ale na początku - jak w ogóle zabrać się za migracje kodu? No więc zacznijmy od tego, że jeśli zdecydowałeś się rozpocząć migracje swojego projektu to punktem startowym powinna być &lt;a href=&quot;https://help.liferay.com/hc/en-us/articles/360029316391-Introduction-to-Upgrading-Code-to-Liferay-DXP-7-2&quot; target=&quot;_self&quot; rel=&quot;nofollow&quot;&gt;oficjalna dokumentacja&lt;/a&gt;. Przede wszystkim jest kilka ścieżek i przykładowo inaczej migruje się projekt 6.x (lub wcześniejszy) a inaczej projekt, którego wersja już jest w wersji 7.x i tylko chcemy podnieść wersje do najnowszej. Ogólne zasada oczywiście jest taka, że im wyżej jesteśmy tym łatwiej będzie zmigrować. Jednocześnie im więcej mamy kodu, który wykorzystuje API Liferaya tym więcej problemów może nas spotkać. Niemniej jednak polecam rozpocząć właśnie od dokumentacji bo nakreśli ona schemat działania. Oczywiście niektóre rzeczy można by przedstawić inaczej i prawdopodobnie schemat migracji można by zmienić, aczkolwiek nie o tym ma być ten wpis.&lt;/p&gt;
&lt;h2&gt;Błędy, które mogą nas spotkać.&lt;/h2&gt;
&lt;p&gt;No więc co takiego złego może nas spotkać podczas migracji? Zdecydowanie największym problemem podczas migracji kodu jest to, że niestety nie wszystkie zmiany są udokumentowane (chociażby w tak zwanych &quot;Breaking changes&quot;), a jeśli są to nie wystarczająco dobrze. Co w takim razie możemy zrobić? Niestety zostaje nam przejść przez migracje oraz uczyć się na bieżąco. Oczywiście na wiele z problemów znajdziemy jakieś rozwiązania, sami też możemy zapytać na forum, a jeśli mamy wersje DXP to nawet próbować z oficjalnym wsparciem. Niemniej jednak musimy być na to gotowi. Z dobrych wiadomości mam to, że kilka z takich rzeczy (zarówno drobnostek jak i tych bardziej uciążliwych) postaram się przedstawić poniżej, tak abyście mogli się uczyć na cudzych (moich) błędach.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Uwaga!&lt;/strong&gt; Błędy te znalazłem jakiś czas temu. Może w momencie czytania (a nawet publikacji tego postu) niektóre z tych rzeczy już są w breaing changes - nie sprawdzałem tego. Niemniej jednak pisałem w swoim czasie kilka postów na forum Liferay więc kto wie, może zostało coś dodane.&lt;/p&gt;
&lt;h3&gt;Zmiana API do pobierania zdjęć w Web Contentach&lt;/h3&gt;
&lt;p&gt;Zacznijmy od tego jak wyglądał wpis XML dla zdjęcia w Web Content:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;#x3C;dynamic-content language-id=&quot;en_US&quot; alt=&quot;&quot; name=&quot;XXX.png&quot; title=&quot;XXX.png&quot; type=&quot;document&quot; [url=http://fileEntryId=&quot;12345&quot;&gt;&amp;#x3C;![CDATA[/documents/111/222/XXX.png/QQQQQQ-WWWW-EEEE-CCCC-DDDDDDD?t=1588696601862]]&gt;&amp;#x3C;/dynamic-content&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Natomiast dla porównania 7.2:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;#x3C;dynamic-content [url=http://language-id=&quot;en_US&quot;&gt;&amp;#x3C;![CDATA[{&quot;groupId&quot;:&quot;126435&quot;,&quot;alt&quot;:&quot;&quot;,&quot;name&quot;:&quot;XXX.png&quot;,&quot;title&quot;:&quot;XXX.png&quot;,&quot;type&quot;:&quot;journal&quot;,&quot;uuid&quot;:&quot;QQQQ-WWW-EEE-CCC-ec48068bbf39&quot;,&quot;fileEntryId&quot;:&quot;716022&quot;,&quot;resourcePrimKey&quot;:&quot;716460&quot;}]]&gt;&amp;#x3C;/dynamic-content&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Jaka jest różnica? No więc kluczowy jest brak ścieżki pliku. Zamiast tego mamy JSON&apos;a z kilkoma informacjami o naszym zdjęciu. Jaki w tym problem? No więc taki, że częstym podejściem w 7.0 było pobieranie ścieżki do zdjęcia w następujący sposób:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;final&lt;/code&gt; &lt;code&gt;com.liferay.portal.kernel.xml.Document document = SAXReaderUtil.read(article.getContentByLocale(locale.toString()));&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;final&lt;/code&gt; &lt;code&gt;String imageUrl = document.valueOf(``&quot;//dynamic-element[@name=&apos;Image&apos;]/dynamic-content&quot;``);&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;W wersji 7.2 robiąc w ten sposób kończymy z pobraniem JSON&apos;a, który następnie ląduje w naszym SRC:&lt;/p&gt;
&lt;p&gt;&lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 215px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 23.72093023255814%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAFCAYAAABFA8wzAAAACXBIWXMAABJ0AAASdAHeZh94AAAA9UlEQVQY04WP606EMBSEef83MdFoorsmmoAh62a5yK3QQqEXHkDAJxgPx93f/phM+/Vk5jRYvhdsPxtrXXetrG1bsSwLa9115f8pKKMCl/sz8kOOQXyhjo9o0xiqEcjKAWnRw04jrHMw1sJacmP4fHNmVw/GVkNlEt1ZwWiFSTbQbQVHIVJpyMFg9o5CNZw1HO4p3JkJ3vs/Ng5wzrIHVfiALnmH+HxB/fGMKnyke4SxqyGTEE30xLwmL97uIE6vmPoOKj/xbBMfILOYliiZBX2VYlIt+jLBUKfQouA2bqYt9sGdGd1TSUVs5J/wG/G9+Dbv5xm/MHtiPqLHcLsAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;JSON in image SRC&apos; title=&apos;JSON in image SRC&apos; src=&apos;/static/c3684c07ea1abff5467253bcc969c421/2eb24/JSON-in-image-SRC.png&apos; srcset=&apos;/static/c3684c07ea1abff5467253bcc969c421/2eb24/JSON-in-image-SRC.png 215w&apos; sizes=&apos;(max-width: 215px) 100vw, 215px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos;&gt;
    &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;Aby do tego nie dopuścić musimy zmienić nasz kod na następujący (przykładowo):&lt;/p&gt;
&lt;p&gt;&lt;code&gt;final com.liferay.portal.kernel.xml.Document document = SAXReaderUtil.read(article.getContentByLocale(locale.toString())); final String imageJsonString = document.valueOf(&quot;//dynamic-element[@name=&apos;Image&apos;]/dynamic-content&quot;); JSONObject imageJsonObject = JSONFactoryUtil.createJSONObject(imageJsonString); FileEntry fileEntry = PortletFileRepositoryUtil.getPortletFileEntry(imageJsonObject.getLong(&quot;fileEntryId&quot;)); String imageUrl = PortletFileRepositoryUtil.getDownloadPortletFileEntryURL(serviceContext.getThemeDisplay(), fileEntry, StringPool.BLANK);&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Analogiczna zamiana może być potrzebna np. w naszym templacie napisanym w FTL, aczkolwiek przełożenie powyższego kodu Javowego nie powinno być problemem.&lt;/p&gt;
&lt;p&gt;Warto zobaczyć &lt;a href=&quot;https://liferay.dev/blogs/-/blogs/oh-no-my-urls-disappeared-and-how-to-get-them-back-&quot; target=&quot;_self&quot; rel=&quot;nofollow&quot;&gt;ten wpis na blogu Liferaya&lt;/a&gt;, w którym możemy wyczytać, że zmiana ta została przeprowadzona bo developerzy sądzili, że nikt z tego i tak nie korzysta. Niestety brakuje także lepszej alternatywy w postaci np. API dzięki, któremu programiści w ogóle nie musieliby się przejmować takimi szczegółami implementacyjnymi i wspomniane rozwiązanie wielokrotnie było sugerowane jako najlepsze.&lt;/p&gt;
&lt;h3&gt;Zmiana pól w Search API&lt;/h3&gt;
&lt;p&gt;W Liferayu 7.2 czy też 7.1 nastąpiła zmiana w nazewnictwie pól w Search API. Zmiana polega na tym, że w 7.0 pole w klasie&lt;/p&gt;
&lt;p&gt;&lt;code&gt;com.liferay.portal.kernel.search.Document&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Wyglądało mniej więcej tak:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ddm__keyword__328630__PublicationDate_en_US_String_sortable&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;W wyżej wersji natomiast wygląda tak:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ddm__keyword__328630__PublicationDate_String_sortable&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Różnica? Fragment _en_US. Oczywiście ma to pewien sens - nie tłumaczymy raczej pola PublicationDate i stąd niepotrzebne rozróżnienie. Niemniej jednak może się okazać, że wyszukując w naszym np. portlecie używamy tego fragmentu w jakimś kontekście i nasz kod przestanie działać poprawnie. Słowa kluczowe dla wyszukiwania tego typu problemów to &quot;indexSearchHelper&quot;, &quot;Document&quot; czy &quot;SearchContext&quot; a przykładowy fragment kodu może wyglądać tak:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;final BooleanQuery publicationDateQuery = new BooleanQueryImpl(); publicationDateQuery.addRangeTerm(String.format(PUBLICATION_DATE_TERM_FIELD_PATTERN, newsDescriptionStructureId), sdf.format(from), sdf.format(to)); indexSearcherHelper.search(searchContext, publicationDateQuery).getDocs()&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Gdzie nasz pattern to:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;private static final String PUBLICATION_DATE_TERM_FIELD_PATTERN = &quot;ddm__keyword__%s__PublicationDate_%s&quot;;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Czyli możemy wyszukiwać po nazwie pola (patrz &lt;em&gt;addRangeTerm&lt;/em&gt;) i podawać tam nazwę pola wraz z naszym fragmentem odnośnie języka. Sam kod nadal będzie działać i nie dostaniemy żadnych błędów natomiast rezultat będzie inny gdyż wyszukujemy po nazwie pola, która nie istnieje (bo została zmieniona).&lt;/p&gt;
&lt;p&gt;Błąd ten jest pewnie z tych rzadszych bo Search API nie jest pewnie aż tak popularne niemniej jednak może być ciężki do znalezienia ze względu na brak jakichkolwiek komunikatów o błędzie.&lt;/p&gt;
&lt;h3&gt;Uprawnienia do podglądu Webcontentów&lt;/h3&gt;
&lt;p&gt;Kolejna zmiana, której nie doszukałem się w breaking changes.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://pydyniak.pl/dlaczego-migracje-projektow-liferayowych-na-wyzsza-wersje-nie-sa-proste-cz-2/webcontent-view-permission/&quot; target=&quot;_self&quot; rel=&quot;nofollow&quot;&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1204px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 26.245847176079735%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAFCAYAAABFA8wzAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAiklEQVQY02WQwQ7EIAhE/f+P7Xaj7gEYKaJU2z28DCCZDKZcitb6U2bR1prDzCoiN9EDUCKyGnM+wLaTihn2xnyWoTSFKcmoO7TNgKUy6+jTeX4tYZ2mYWgJLE3XgBjDKNRTDvb3dBwfzTn7OWHoNeC6kPkm944zvyLmdnL128Pssfxivf3v9KRdLw+oiuDB3rhGAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Zdjęcie widoku uprawnień dla Web Contentów&apos; title=&apos;Zdjęcie widoku uprawnień dla Web Contentów&apos; src=&apos;/static/6a0870b3b70203476cae509e9a574e60/35252/Webcontent-view-permission.png&apos; srcset=&apos;/static/6a0870b3b70203476cae509e9a574e60/fb933/Webcontent-view-permission.png 301w,
/static/6a0870b3b70203476cae509e9a574e60/32056/Webcontent-view-permission.png 602w,
/static/6a0870b3b70203476cae509e9a574e60/35252/Webcontent-view-permission.png 1204w,
/static/6a0870b3b70203476cae509e9a574e60/7575b/Webcontent-view-permission.png 1608w&apos; sizes=&apos;(max-width: 1204px) 100vw, 1204px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Zdjęcie widoku uprawnień dla Web Contentów&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Mając webcontent z uprawnieniami jak na screenie powyżej nasz portal może zachować się różnie w zależności od wersji. Ma to związek z jednym z ustawień Liferaya dostępnym pod (w 7.2):&lt;/p&gt;
&lt;p&gt;Control panel → configuration → system settings → web content&lt;/p&gt;
&lt;p&gt;Konfiguracja ta nosi nazwe &quot;Article view permissions check enabled&quot; i za jej pomocą możemy ustawić czy Liferay powinien weryfikować uprawnienie &quot;View&quot; dla Web Contentów czy nie. Czyli:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Mamy przełącznik wyłączony - każdy może obejrzeć nasz WebContent chociaż uprawnienia (patrz screen wyżej) wskazują, że jedynie Owner powinien mieć taki dostęp&lt;/li&gt;
&lt;li&gt;Mamy przełącznik włączony - weryfikacja następuje i rzeczywiście przykładowo Guest nie ma dostępu do WebContentu&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Zmianą między wersjami jest to, że w wersji 7.0 domyślnie to ustawienie jest &lt;strong&gt;wyłączone&lt;/strong&gt; (każdy ma dostęp), natomiast w wersji 7.2 jest domyślnie &lt;strong&gt;włączone.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Możemy więc mieć niespodziankę - wcześniej każdy mógł oglądać nasze WebContenty a teraz nikt nie może. Oczywiście zakładając, że nie definiowaliśmy tych uprawnień wcześniej.&lt;/p&gt;
&lt;h3&gt;Zmiana metody do pobrania aktualnego URLa w FTLu&lt;/h3&gt;
&lt;p&gt;W wersji 7.0.x mogliśmy pobierać aktualny URL używając następującego kodu:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;url = request.attributes[&apos;CURRENT_URL&apos;]&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Po podniesieniu naszego kodu już niestety to nie zadziała. Zamiast tego możemy użyć&lt;/p&gt;
&lt;p&gt;&lt;code&gt;url = portalUtil.getCurrentURL(request)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;To akurat drobnostka, ale również nieudokumentowana.&lt;/p&gt;
&lt;h2&gt;Czy to wszystko?&lt;/h2&gt;
&lt;p&gt;Oczywiście nie. Powyższe przykłady to kilka błędów, które możemy spotkać. Niemniej jednak błędy przede wszystkim z Search API oraz z pobieraniem adresów zdjęć uważam za te idealnie pokazujące niespodziewane problemy, które mogą nas spotkać. Dla kontrastu dodałem także dwa błędy, które są zdecydowanie prostsze do znalezienia i poprawnienia, tak aby pokazać, że nie wszystkie błędy są aż tak strasznie ukryte (szczególnie jak w przypadku zmiany w Search API).&lt;/p&gt;
&lt;p&gt;Mam nadzieję, że powyższy wpis choćiaż trochę pomoże Ci podczas migracji.&lt;/p&gt;
&lt;p&gt;Pozdrawiam!&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Dlaczego migracje projektów Liferayowych na wyższą wersje nie są proste]]></title><description><![CDATA[Cześć! W dzisiejszym wpisie chciałbym poruszyć temat migracji projektów Liferayowych między wersjami, a dokładniej przedstawić bolączki z tym związane w oparciu o własne doświadczenia. Osobiście uczestniczyłem w migracji kilku projektów – zarówno z wersji 6.2 do 7.x jak i z wersji 7.0 do 7.2 (tutaj dodatkowo z wersji CE na wersje DXP). Dodatkowo migracje te… ]]></description><link>https://pydyniak.pl/dlaczego-migracje-projektow-liferayowych-nie-sa-proste/</link><guid isPermaLink="false">https://pydyniak.pl/dlaczego-migracje-projektow-liferayowych-nie-sa-proste/</guid><pubDate>Sun, 18 Oct 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Cześć! W dzisiejszym wpisie chciałbym poruszyć temat migracji projektów Liferayowych między wersjami, a dokładniej przedstawić bolączki z tym związane w oparciu o własne doświadczenia. Osobiście uczestniczyłem w migracji kilku projektów - zarówno z wersji 6.2 do 7.x jak i z wersji 7.0 do 7.2 (tutaj dodatkowo z wersji CE na wersje DXP). Dodatkowo migracje te obejmowały całe spektrum &quot;przypadków&quot; jeśli chodzi o wielkość - były to małe moduły z dosłownie kilkoma portletami, średnie projekty z kilkoma encjami bazodanowymi i kilkunastoma portletami, ale także był jeden projekt, który zawierał dużo ponad 100 portletów i encji bazodanowych.&lt;/p&gt;
&lt;p&gt;Właśnie na bazie tych doświadczeń chciałbym pokazać kilka przykładów tego na jakie problemy możemy się natknąć podczas migracji. Chciałbym przez to uczulić Ciebie na to, aby nie ufać w to, że automatycznie uda Ci się zmigrować projekt bo najprawdopodobniej tak nie będzie. Powiem więcej - sam musiałem w niektórych przypadkach debugować kod Liferaya bo niestety w wielu przypadkach nie pomagała ani dokumentacja, ani istniejące tematy na forum ani nawet zadanie własnego pytania. Oczywiście jeśli masz wersje DXP to jest szansa, że otrzymasz oficjalne wsparcie, aczkolwiek z doświadczenia wiem, że niewielka część portali oparta jest na płatnej wersji co, ze względu na koszty, jest całkiem zrozumiałe.&lt;/p&gt;
&lt;p&gt;Od razu ostrzegam, że nie będzie w tym wpisie zbyt dużo kodu czy rzeczywistych przykładów bo po prostu odtworzenie ich zajęłoby zbyt dużo czasu, a kodu produkcyjnego przy którym moje problemy wystąpiły z oczywistych względów pokazać nie mogę. Niemniej jednak mam nadzieję, że uda Ci się wynieść coś pożytecznego z kilku minut poświęconych na przeczytanie poniższego tekstu.&lt;/p&gt;
&lt;p&gt;Uwaga! W trakcie pisania okazało się, że tekstu wyszło na tyle dużo, że zdecydowałem się podzielić go na dwie części. Druga część pojawi się niebawem - po prostu przy pisaniu tego wpisu, który miał być całością uznałem, że jest już tego za dużo, a nawet nie doszedłem do &quot;głównej&quot; części, o której chciałem napisać czyli migracji modułów :)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Uwaga!&lt;/strong&gt; Dostępna jest już także druga część artykułu o problemach, które mogą nas spotkać podczas migracji kodu. Zachęcam do przeczytania w następnej kolejności: &lt;a href=&quot;/dlaczego-migracje-projektow-liferayowych-na-wyzsza-wersje-nie-sa-proste-cz-2/&quot;&gt;Dlaczego migracje projektów Liferayowych na wyższą wersje nie są proste cz. 2&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Dlaczego w ogóle aktualizować wersje?&lt;/h2&gt;
&lt;p&gt;Warto najpierw zadać sobie pytanie - po co w ogóle migrujemy? Odpowiedzi jest kilka - przede wszystkim bezpieczeństwo. Wystarczy spojrzeć na tzw. &quot;known vulnerabilities&quot;, aby zobaczyć, że stare wersje Liferaya zawierają sporo niedociągnięć.&lt;/p&gt;
&lt;p&gt;Inną sprawą mogą być nowości, które oferują nowsze wersje używanej technologii. W przypadku Liferaya idealnie widać to na przykładzie wersji 6.2 i 7.x (nawet 7.0) - przeskok był ogromny chociażby ze względu na przejście na OSGI.&lt;/p&gt;
&lt;h2&gt;Jak wygląda migracja w Liferayu&lt;/h2&gt;
&lt;p&gt;Tak jak pisałem wcześniej - wpis ten nie będzie instrukcją przeprowadzenia migracji bo od tego jest &lt;a href=&quot;https://help.liferay.com/hc/en-us/articles/360028711612-Introduction-to-Upgrading-to-Liferay-DXP-7-2&quot; target=&quot;_self&quot; rel=&quot;nofollow&quot;&gt;oficjalna dokumentacja&lt;/a&gt;. Nie ukrywam, że prawdopodobnie można by to przedstawić dużo lepiej niż w oficjalnej dokumentacji, ale to raczej byłby temat na cały kurs, a nie wpis na blogu ;)&lt;/p&gt;
&lt;p&gt;Gdybyś jednak na ten moment nie był zainteresowany przedzieraniem się przez całą dokumentacje to możesz póki co przyjąć, że migracja sprowadza się w uproszczeniu do kilku punktów:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Przygotowanie naszego portalu - wszelkie kopie zapasowe, usunięcie duplikatów czy nieużywanych artykułów itp&lt;/li&gt;
&lt;li&gt;Aktualizacja bazy danych - za pomocą skryptu&lt;/li&gt;
&lt;li&gt;Migracja naszych modułów na nowe wersje - w przypadku migracji z 6.2 najprawdopodobniej będzie też potrzebne rozbicie aplikacji na jakiekolwiek moduły&lt;/li&gt;
&lt;li&gt;Testy, testy i jeszcze więcej testów&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I na pierwszych trzech skupię się opisując co może nam się przydarzyć.&lt;/p&gt;
&lt;h2&gt;Przygotowanie portalu&lt;/h2&gt;
&lt;p&gt;Przygotowanie portalu jest krokiem, który jest zalecany w oficjalnej dokumentacji, choć wydawałoby się, że można go pominąć? No niestety okazuję, się, że nie zawsze. Standardowo to przygotowanie portalu ma na celu oszczędzenie miejsca oraz przyspieszenie procesu migracji, aczkolwiek istnieje także drugie dno - Liferay nie zawsze radzi sobie poprawnie z starymi referencjami. Czym jest stara referencja? Przykładowo może być to jakiś stary webcontent, którego już nie używamy i dawno o nim zapomnieliśmy. W pewnym momencie ktoś usunął zdjęcie, którego ten WebContent używał. Wydaję się, że nic złego nie może się wydarzyć bo to standardowe zachowanie - nikt przecież nie analizuje zawartości wszystkich artykułów przed usunięciem zdjęcia i rzeczywiście nic złego się nie dzieje w takim wypadku.&lt;/p&gt;
&lt;p&gt;Przechodzimy jednak do migracji naszego portalu na wyższą wersje, wszystko się nam zmigrowało, portal działa aż pewnego razu pobieramy całego LARa z serwera, żeby lokalnie odtworzyć portal i... kończymy z tak zwanym &quot;infinite loop&quot;. Brzmi nieprawdopodobnie? A jednak, taka właśnie sytuacja mi się przydarzyła. Dodam, że pierwotnie portal był w wersji 7.0.6 CE a zmigrowany został do wersji 7.2.1 DXP. Całą historie można zobaczyć (wraz z poradami, które otrzymałem) na forum Liferaya: &lt;a href=&quot;https://liferay.dev/forums/-/message_boards/message/119669178&quot; target=&quot;_self&quot; rel=&quot;nofollow&quot;&gt;https://liferay.dev/forums/-/message_boards/message/119669178&lt;/a&gt;. Dodam tylko, że oprócz proponowanych tam rozwiązań z logami (które bardzo pomogły!) musiałem debugować kod Liferaya.&lt;/p&gt;
&lt;p&gt;A chodziło tylko o usuniętę zdjęcie... Na obronę przyznam jedynie, że przypadek ten był specyficzny - nie zawsze usuniętę zdjęcie cokolwiek zepsuje (wręcz raczej nigdy), ale z tego co zauważyłem jedynie wtedy kiedy wrzucimy zdjęcie bezpośrednio do treści z edytora, pomijając documents &amp;#x26; media. Może nawet muszą zajść jeszcze jakieś dodatkowe przypadki bo zdjęć miałem w tym migrowanym portalu bardzo dużo (tysiące), a tylko jedno okazało się problematyczne.&lt;/p&gt;
&lt;p&gt;Jak szukać takich zepsutych zdjęć? Generalnie ja nie wiedziałem, że taki problem istnieje i stąd tak jak wspomniałem skończyłem na degowaniu kodu Liferaya wraz z czytaniem logów na poziomie DEBUG (sporo tego) i tak znalazłem mój zepsuty WebContent. Niemniej jednak gdybym takich przypadków miał więcej to najprawdopodobniej spróbowałbym jakiegoś narzędzia do szukania &quot;Broken references&quot; - generalnie chodziłoby o przeszukanie naszej strony w poszukiwaniu miejsc gdzie mamy zepsute zdjęcia (wiecie - taka mała ikonka zdjęcia, które się nie załadowało przez zły src). Niestety nie zadziała to jeśli nasz webcontent nie jest nigdzie wstawiony - wtedy można pokusić się o napisanie jakiegoś małego programu do szukania tego.&lt;/p&gt;
&lt;h2&gt;Aktualizacja bazy danych&lt;/h2&gt;
&lt;p&gt;Jeśli chodzi o aktualizacje samej bazy danych to przyznam, że jest nieźle. Twórcy tak zwanego tak zwanego &quot;upgrade tool&quot; zrobili dobrą robote bo nie spotkałem się z większymi problemami i to mimo migrowania naprawdę dużych baz danych (jedna z około 8 wirtualnymi instancjami a druga nawet z około 20). Napotkałem tak naprawdę jedynie dwa problemy związane stricte z migracją bazy danych, ale nie były one tragiczne w skutkach - biorąc pod uwagę jak wiele mogło się zepsuć np. w WebContentach&lt;/p&gt;
&lt;h3&gt;Wpisy konfiguracyjne JAX-RS&lt;/h3&gt;
&lt;p&gt;W jednej z migrowanych aplikacji (7.0.6 -&gt; 7.2.1 DXP), w której użyty był JAX-RS (o którym w kontekście Liferaya można poczytać &lt;a href=&quot;https://help.liferay.com/hc/en-us/articles/360018161171-JAX-WS-and-JAX-RS&quot; target=&quot;_self&quot; rel=&quot;nofollow&quot;&gt;tutaj&lt;/a&gt;) pojawił się błąd przy starcie aplikacji i wyglądał mniej więcej tak:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://pydyniak.pl/wp-content/uploads/2020/10/JAX-RS-exception.png&quot; target=&quot;_self&quot; rel=&quot;nofollow&quot;&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1204px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 35.880398671096344%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAHCAYAAAAIy204AAAACXBIWXMAAAsTAAALEwEAmpwYAAAA/ElEQVQoz21RB46DQAzk/08MbUtYelhAQvSJ7Uuu5LA0Gq9XGo/toCpLaKWhtUaapkgTRgKVKlhr4ZyDyxzqukZVVcJt26Lve6zrim3bhBk75cEwDFBK4W7vImCMFU5IlJvkLqc/rhtpzP9cY8GrCHzXiTMWSOJEcnZntCF3ubgaxxH7vosbZsZxHCJwnucfBNyJnVjzcqG18PvNbr33IvIZLPDKfjv04o7HZodRFCGOYoRhSIik/nh0mOf5nxvW++EvBE3tEceGoHG7sSCPm5G4IbGM9lVhmnjxwLKc37wsuITskC9ZFIVwnudo6JIlXf992bZpSHS6HPEzniwaFCCdk9yaAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Zdjęcie błędu podczas odpalania portalu z błędnym wpisem w tabeli Configuration_ związanym z JAX-RS&apos; title=&apos;Zdjęcie błędu podczas odpalania portalu z błędnym wpisem w tabeli Configuration_ związanym z JAX-RS&apos; src=&apos;/static/c562e3b39229aa567298f81fafd736dc/35252/JAX-RS-exception.png&apos; srcset=&apos;/static/c562e3b39229aa567298f81fafd736dc/fb933/JAX-RS-exception.png 301w,
/static/c562e3b39229aa567298f81fafd736dc/32056/JAX-RS-exception.png 602w,
/static/c562e3b39229aa567298f81fafd736dc/35252/JAX-RS-exception.png 1204w,
/static/c562e3b39229aa567298f81fafd736dc/32b38/JAX-RS-exception.png 1471w&apos; sizes=&apos;(max-width: 1204px) 100vw, 1204px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Zdjęcie błędu podczas odpalania portalu z błędnym wpisem w tabeli Configuration_ związanym z JAX-RS&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Związany był z niepoprawnymi wpisami w tabeli Configuration_. Wyglądały one mniej więcej tak:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;com.liferay.portal.remote.rest.extender.configuration.RestExtenderConfiguration.e7c762dd-9fed-4956-8bbf-a89ec9e3a1b8&lt;/li&gt;
&lt;li&gt;com.liferay.portal.remote.rest.extender.configuration.RestExtenderConfiguration.factory&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Było ich więcej (jeden dla &quot;Factory&quot; i po jednym dla każdego rejestrowanego endpointu). Numerki też będą inne (w przypadku tego pierwszego) więc najłatwiej ich poszukać zapytaniem:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;select * from Configuration_ WHERE configurationId like &apos;%com.liferay.portal.remote.rest.extender.configuration.RestExtenderConfiguratio%&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Rozwiązanie? Wystarczy usunąć te wpisy i się nimi na etapie migracji bazy danych. Gdy zmigrujemy nasze moduły związane z JAX-RS i je uruchomimy wszystko będzie działać poprawnie&lt;/p&gt;
&lt;h3&gt;Tabela release_&lt;/h3&gt;
&lt;p&gt;Drugim problemem, z którym się spotkałem były niewłaściwe wpisy w tabeli Release_, która krótko mówiąc przechowuję informacje o wersjach aplikacji czy modułów. Problem pojawiał się przy migracji z wersji 6.2 GA2 na wersje 7.3 GA1 - tabela release nie migrowała się poprawnie, aczkolwiek wystarczyło porównać jej zawartość z &quot;świeżą&quot; bazą stworzoną pod 7.3.1 i &quot;podmienić&quot; wpisy i wszystko działało poprawnie.&lt;/p&gt;
&lt;h2&gt;Na koniec&lt;/h2&gt;
&lt;p&gt;Tak jak wspominałem w wstępie jest to pierwszy artykułów z dwóch na temat problemów migracyjnych. W drugim wpisie opiszę temat migracji modułów gdzie problemów jest o wiele więcej - tam na pewno wręcz nie opiszę wszystkich, z którymi się spotkałem bo jest ich sporo, choć wiele z nich wynika raczej z ubogiej dokumentacji (i braków w tak zwanych &quot;Breaking changes&quot;), ale w większości nie są aż tak trudne do naprawienia.&lt;/p&gt;
&lt;p&gt;I na koniec - powodzenia! :)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PS&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Gdybyś w swojej firmie szukał pomocy przy takich migracjach to zawsze zapraszam do kontaktu. Mogę pomóc przy małych migracjach jak i tych dużych, gdzie w zależności od skali możemy porozmawiać o większym zespole który mogę zaproponować.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Continuous integration i sprawdzanie jakości kodu przy użyciu Liferaya 7]]></title><description><![CDATA[Cześć! Dzisiaj chciałbym przedstawić jak można wykorzystać Jenkinsa do procesu “Continuous integration” czyli ciągłej integracji naszego kodu a także do procesu ciągłego sprawdzania jakości kodu z wykorzystaniem narzędzia SonarQube. Postanowiłem napisać ten post gdyż jakiś czas temu spotkałem się z takim właśnie zadaniem i jak się okazało, nie ma zbyt wielu pomocnych materiałów odnośnie tego…]]></description><link>https://pydyniak.pl/continuous-integration-i-sprawdzanie-jakosci-kodu-przy-uzyciu-liferaya-7/</link><guid isPermaLink="false">https://pydyniak.pl/continuous-integration-i-sprawdzanie-jakosci-kodu-przy-uzyciu-liferaya-7/</guid><pubDate>Sat, 22 Aug 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Cześć! Dzisiaj chciałbym przedstawić jak można wykorzystać Jenkinsa do procesu &quot;Continuous integration&quot; czyli ciągłej integracji naszego kodu a także do procesu ciągłego sprawdzania jakości kodu z wykorzystaniem narzędzia SonarQube.&lt;/p&gt;
&lt;p&gt;Postanowiłem napisać ten post gdyż jakiś czas temu spotkałem się z takim właśnie zadaniem i jak się okazało, nie ma zbyt wielu pomocnych materiałów odnośnie tego tematu.&lt;/p&gt;
&lt;h2&gt;Wymagania wstępne&lt;/h2&gt;
&lt;p&gt;Aby rozpocząć potrzebujemy kilku rzeczy:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Projektu Liferaya - na potrzeby tego postu stworzyłem prosty projekt, z którego również możesz skorzystać. Zawiera on zaledwie kilka klas, aczkolwiek powinien być wystarczający do celów &quot;naukowych&quot;. Kod tego projektu możesz znaleźć &lt;a href=&quot;https://github.com/RafalPydyniak/JenkinsSonarDemo&quot; target=&quot;_self&quot; rel=&quot;nofollow&quot;&gt;na moim githubie&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Jenkins&lt;/li&gt;
&lt;li&gt;SonarQube&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Jenkins&lt;/h3&gt;
&lt;p&gt;Zacznijmy od Jenkinsa. Większość z czytających pewnie o tym narzędziu słyszała, ale gdybyś jednak nie spotkał się z nim wcześniej to już tłumaczę -  Jenkins jest to open sourcowe narzędzie służące automatyzacji. Używany jest bardzo często do automatyzacji wszelakich zadań około programistycznych takich jak budowanie aplikacji, uruchamiania testów, a także wrzucaniu aplikacji na serwer, aczkolwiek nie ogranicza się tylko do tego i równie dobrze może zostać użyty np. do cyklicznego uruchamiania jakiegoś skryptu czy aplikacji.&lt;/p&gt;
&lt;p&gt;Instalacja Jenkinsa nie powinna sprawić Ci żadnego problemu, a wszelkie niezbędne informacje można znaleźć na &lt;a href=&quot;https://www.jenkins.io/doc/book/installing/&quot; target=&quot;_self&quot; rel=&quot;nofollow&quot;&gt;oficjalnej stronie&lt;/a&gt;. Jenkinsa można zainstalować zarówno lokalnie (podobnie jak każdą inną aplikacje) lub wykorzystać do tego celu Dockera. Osobiście polecam Dockera gdyż jest to świetne narzędzie, aczkolwiek możesz wybrać dowolną opcje.&lt;/p&gt;
&lt;h3&gt;SonarQube&lt;/h3&gt;
&lt;p&gt;Drugim narzędziem, którego będziemy potrzebowali jest SonarQube czyli open sourcowe narzędzie służące automatycznemu sprawdzaniu jakości naszego kodu. SonarQube może wykryć tak zwane &quot;code smells&quot;, bugi, duplikacje kodu i inne rzeczy tego typu. Instrukcje instalacji, podobnie jak w przypadku Jenkinsa, można znaleźć na &lt;a href=&quot;https://docs.sonarqube.org/latest/setup/install-server/&quot; target=&quot;_self&quot; rel=&quot;nofollow&quot;&gt;oficjalnej stronie&lt;/a&gt;. Po raz kolejny możesz wybrać między standardową instalacją, a Dockerem.&lt;/p&gt;
&lt;h2&gt;Integracja&lt;/h2&gt;
&lt;p&gt;Gdy już mamy wspomniane narzędzia możemy rozpocząć konfiguracje naszej integracji między Jenkinsem, SonarQubem a naszym projektem.&lt;/p&gt;
&lt;p&gt;Generalnie to co chcemy zrobić to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Skonfigurować projekt w SonarQube&lt;/li&gt;
&lt;li&gt;Dodać plugin &quot;SonarQube Scanner&quot; w Jenkinsie, tak aby móć odpalić Sonara z poziomu Jenkinsa&lt;/li&gt;
&lt;li&gt;Skonfigurować wyżej wymienione plugin&lt;/li&gt;
&lt;li&gt;Stworzyć zadanie w Jenkinsa służące budowaniu naszego projektu oraz skanowaniu kodu za pomocą Sonara&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Konfiguracja projektu w SonarQube&lt;/h3&gt;
&lt;p&gt;Zanim rozpoczniemy integracje potrzebujemy stworzyć projekt w SonarQubie. Aby to zrobić musimy odpalić nasz serwer Sonara (najprawdopodobniej dostępny pod adresem localhost:9000), zalogować się (domyślne dane to admin/admin) oraz kliknąć przycisk &quot;plusika&quot; w prawym górnym rogu.&lt;/p&gt;
&lt;p&gt;Następnie generujemy nasz token - po prostu podajemy jakąś nazwe i klikamy &quot;Generate&quot;. Token zostanie dla nas wygenerowany i będziemy mogli go użyć do integracji np. właśnie z Jenkinsem.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 915px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 35.21594684385382%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAHCAYAAAAIy204AAAACXBIWXMAABJ0AAASdAHeZh94AAAA1ElEQVQoz52Q6U7EMAyE8/4PxyP0+IHYNnfTJM3RDk5ZdlnQCoGlTx7ZymgcNgwD+r7HOI7ouh6ccwghIKWEIuQX2lwphWVZnsKUkjeDT5qpPB8/mrW51hree6zrevbvsJfugu5NInoHYy1SSvi1juPpisWUEWKEc+6M3JIISsLnGdM0Yb52TnND82Ls1fP4wWnYRE4bDJ1iKaExGlxqSG1vZz2cRzqEgEA6UpCmW9/3/W64pYJXYeBCW1SkTJSK/xRrfxa2BGEcfNxQa0EtH+Sc/8w7MiUeFIsy8E4AAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Wygenerowany token w SonarQube&apos; title=&apos;Wygenerowany token w SonarQube&apos; src=&apos;/static/0cbb79038d415d8d85f96ab6d90f692a/4255a/Sonar-token.png&apos; srcset=&apos;/static/0cbb79038d415d8d85f96ab6d90f692a/fb933/Sonar-token.png 301w,
/static/0cbb79038d415d8d85f96ab6d90f692a/32056/Sonar-token.png 602w,
/static/0cbb79038d415d8d85f96ab6d90f692a/4255a/Sonar-token.png 915w&apos; sizes=&apos;(max-width: 915px) 100vw, 915px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Wygenerowany token w SonarQube&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;Zapisz sobie wygenerowany token oraz nazwę projektu, przydadzą się one później. Na ekranie wygenerowanego tokenu możesz także kliknąć &quot;Continue&quot;, aby zobaczyć przykłady jak można uruchomić skanowanie projektu w Gradle lub Mavenie, aczkolwiek w naszym przypadku nie będzie to potrzebne.&lt;/p&gt;
&lt;h3&gt;Dodawanie pluginu &quot;SonarQube scanner&quot; do Jenkinsa i jego konfiguracja&lt;/h3&gt;
&lt;p&gt;Kolejnym krokiem niezbędnym do naszej integracji jest zainstalowanie pluginu &quot;SonarQube Scanner&quot; w Jenkinsie. Możemy to zrobić wchodząc w (ze strony głównej):&lt;/p&gt;
&lt;p&gt;Manage Jenkins -&gt; Manage plugins -&gt; Available -&gt; Search for &quot;SonarQube Scanner&quot; -&gt; zaznacz pierwszą dostępną opcje i zaznacz &quot;Install with restart&quot;&lt;/p&gt;
&lt;p&gt;Następnie, po instalacji pluginu, idziemy do (z strony głównej):&lt;/p&gt;
&lt;p&gt;Manage Jenkins -&gt; Global tool Configuration -&gt; W części &quot;SonarQube Scanner&quot; klikamy &quot;Add SonarQube Scanner&quot; i wpisujemy jakąs nazwę. Dla reszty opcji zostawiamy wartości domyślne. Następnie potwierdzamy naszą konfiguracje poprzez kliknięcie &quot;Save&quot;.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1024px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 40.86378737541528%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAICAYAAAD5nd/tAAAACXBIWXMAAA7EAAAOxAGVKw4bAAABCklEQVQoz42RTU7DMBCFc/8LtBwANnCIrmHRbkFtN0Alkjpu4n/HzmPGUAEpQYz0Yk00/ubNuDLGoK5rdF0H7z2GIaFXGkobWOcx4v/BtdWYM1JKBBoQ4wDOucF2t8fr8wtkK6GUgtb6T/VUE7wj4PjTQ6bUuoDTfof+cIC1tjiPMSKEcCn+7wOic+jNJ/As+iBloD0ptEKUNfClTK7n5/wy5MrI34Afbkc4ayAI2DQNpJRw1H1aV0SrKqDHJ7wtllCr1SUwk/hBjHUTI+OvitRU3z9A3N5BbzaopsU8niBXR9HCGFvyORjPc7y+IXcL1MsrmPUaFS/7fImDX1y0DBTltOSU9zgHncY7HNRuCOOu1QAAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Konfiguracja SonarQube Scannera w Jenkinsie&apos; title=&apos;Konfiguracja SonarQube Scannera w Jenkinsie&apos; src=&apos;/static/6ad3677a0671733bcafff23b2e4cc54a/2bef9/SonarQube-Scanner-1024x420-1.png&apos; srcset=&apos;/static/6ad3677a0671733bcafff23b2e4cc54a/fb933/SonarQube-Scanner-1024x420-1.png 301w,
/static/6ad3677a0671733bcafff23b2e4cc54a/32056/SonarQube-Scanner-1024x420-1.png 602w,
/static/6ad3677a0671733bcafff23b2e4cc54a/2bef9/SonarQube-Scanner-1024x420-1.png 1024w&apos; sizes=&apos;(max-width: 1024px) 100vw, 1024px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Konfiguracja SonarQube Scannera w Jenkinsie&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;Wypełniona konfiguracja pluginu SonarQube Scanner&lt;/p&gt;
&lt;p&gt;Kolejny krok to konfiguracja serwera SonarQube. Aby to zrobić przechodzimy do (ze strony głównej Jenkinsa):&lt;/p&gt;
&lt;p&gt;Manage Jenkins -&gt; Configure System -&gt; Szukamy sekcji&quot;SonarQube servers&quot; i klikamy &quot;Add SonarQube&quot;. Nazywamy nasz serwer (nazwa jedynie dla nas) oraz wprowadzamy adres URL.&lt;/p&gt;
&lt;h3&gt;Stworzenie projektu (zadania) w Jenkinsie&lt;/h3&gt;
&lt;p&gt;Jenkins oferuje nam kilka typów &quot;projektów&quot;. Opis tych typów można znaleźć chociażby w dokumentacji lub w innych artykułach. My skupimy się na tak zwanym &quot;Freestyle Project&quot;, który umożliwia nam zdefiniowanie różnych akcji (takich jak budowanie, testy, deployment itp), które wykonają się po kolei. Aby stworzyć taki projekt klikamy na &quot;New item&quot; z strony głównej i wybieramy &quot;Freestyle project&quot;. Nazywamy go wedle uznania, a następnie klikamy &quot;ok&quot;.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 246px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 73.98373983739836%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAPCAYAAADkmO9VAAAACXBIWXMAABJ0AAASdAHeZh94AAACBElEQVQ4y5WRzUsbQRjGJ5Ki5JKieK0gxICH/AOmSYTSc04hgXwQckoCmg+kB6FQkRzFFvoP9NB76VWwFGs0aiv0UFqa1pJsV2u0Nhuzm4/N05lZd5tAstgXfjvvzPvOM8/MEkIIGJaxMdyxWrXcYoGx3pcPY0hdS56vr+DFs8dGweFwwG63DzS7XC7YbDY4nU6zgwgWJ6ew9XQNr55kMT8+wQvpVArZbBbBYBDxeBzRaBSFQgGxWAyRSASJRILnrOZ2u/vdEty7O4lYYhmbj1YxNz2NmdlZLC8tIRQKIRAIcIF0Oo1cLod8Po9wOIwUPTCZTCKTycDv93NBq/ZkmlXfg4cI0kaz97oVPp8PjPvUtnthgY9eOvd6vXzd4/Hwkc112Fr/XNdgkHa7DUan08VA9Ho3g3pT73D0/lEQWZZ5clk7R+ngCIcfPuLN2x0U9w/4vCKIXEiSJNTrdSiKArZnFERuNtFqtXB5UcPuXgk7xRL2mNjhe7zbLeLrtxN+4HWjwTET0wRl7USFig67MnPXZM3MmeFOGQnp0Y063S5DpXQNVFU16O8dBdGdaNGGWfQGeocHYR9FOkd5ewNX5U1I9St8Ov4M4UTAqXCGarkC4XsFYvWXIWrukEarUcOX1ysQj16C3poKVCH++En//G9cnNZwVhFp/uefoJnD21zjf678F3voaoo+ZVMRAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Przycisk do stworzenia nowego projektu w Jenkinsie&apos; title=&apos;Przycisk do stworzenia nowego projektu w Jenkinsie&apos; src=&apos;/static/f64d1ed89904d08b21e004f457871f83/92252/new-item-1.png&apos; srcset=&apos;/static/f64d1ed89904d08b21e004f457871f83/92252/new-item-1.png 246w&apos; sizes=&apos;(max-width: 246px) 100vw, 246px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Przycisk do stworzenia nowego projektu w Jenkinsie&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;Nowe zadanie w Jenkinsie&lt;/p&gt;
&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 661px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 120.93023255813955%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAYCAYAAAD6S912AAAACXBIWXMAABJ0AAASdAHeZh94AAADjUlEQVQ4y4VVy47jVBD1JyC+iC9A4kMQ4jdmiViyHGkWvUFCIFiBoFvqSQd150Encd5xEr9fcezYseNDVcX2ZDLdsDhd915X17116lRFCcMQ+/1eEEWRoFmTjeMY7OP7PoIgeBX8naE4joO721u02210ez10np7wvtVCt9uR9f39PdbrtTi7rvu/UDzPg6qqmIzHmEynmM9msp/PZ3I2GAxgGAZ8egX7vgYOxlYCynPpH06nU4OiKMSWZYnj8Ygsy15FmqYfB1ytVlgsFoLNZvPJ7TU//4Um5SAIEfoeAs+VD1yA3W4nCMOdnHkV/CvrvRBYSQ4p9PAIc5eRQwDdtGE7Hizbhe16SLMcWV4gO14hP53Pr6AcsgLv/7hB+88b6IaJwXCIKRVHHY0wI1sUxCnxyZyW5QduhT/ilvd5njdQEgoYaI+INh0qzA4WVdRxbOi6TtXVsd1uZc12u9001q+4ZWp8oox1y3RRygcUAEoCp58cMuzjBB4F94nDcLc/I9ojqPZBGOFI6R3p9WLz2lLKaZqhv9jiebbCZDLF09MjTMumtEpKKSenSxTNOmW5ZEdJO8s+QOHD3nCM2VKTjlBHQ7EWBXWpi2ybimRbMIkKy7IEDp0xLTatWfSmaTagFx5EuAXd7pGObNsRp7W2FrtZb4RDtnzRmcMzNNLvWtPOa7IMhVW+2yc0CGISsdsI2at0WQ8Ftgfiu+6MGtd75UAcdgZjPKtTDCndXq+PIUnnof1AA6KLf/rPdNZDt9OjikZIkkQmEGNfTSNGfS4vNCwWsU8cWFjM58SXCZ5CwpfjCoeO7QivzKlFfmwNokL8iDu94lg4PJ0KEW892/gmEfTFsLhO86UBISnzH21rYmPYQvqKySWr61uxi8VSbs+lU8qPLvkUJRSO/u6nO9w/jqhKK+FPVcfSdpPJmHgdYUozkvni18vQIHAWvK6nfET88p4CptIBSRKD00+SQ/MTcEl+GFKlPV/arJ5KQaWCkKmqlKGkJIXfW308DNdYLuboU5X51ZcNzyiqAVHjpTOGEsUB7vpjtNQVDIeGgKEhiunm8FxlVr90BqHuBq48V5fX7GNX3wxShwLE+OGvHj7/5i2+ePMzPvv6Ld780kKW2FiutGbS8CTfUJGY52XVIdwZfF53CXeOglOEH/8e4svvfsW3727x1fe/4eZhgPJEoiU+a5lwe9a4lIvsL779CxMNEA3mMyTAAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Opcje nowego projektu w Jenkinsie&apos; title=&apos;Opcje nowego projektu w Jenkinsie&apos; src=&apos;/static/23f59fac0da2eecb951ea25dae2303c2/0012b/New-project.png&apos; srcset=&apos;/static/23f59fac0da2eecb951ea25dae2303c2/fb933/New-project.png 301w,
/static/23f59fac0da2eecb951ea25dae2303c2/32056/New-project.png 602w,
/static/23f59fac0da2eecb951ea25dae2303c2/0012b/New-project.png 661w&apos; sizes=&apos;(max-width: 661px) 100vw, 661px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Opcje nowego projektu w Jenkinsie&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;Ekran tworzenia nowego projektu&lt;/p&gt;
&lt;p&gt;Gdy klikniemy &lt;em&gt;ok&lt;/em&gt; zostaniemy przekierowani do konfiguracji stworzonego zadania. Nie będę opisywał wszystkich dostępnych ustawień, gdyż jest ich całkiem sporo a jednocześnie większość z nich jest albo prosta i jasna albo służy do bardzo specyficznych zastosowań i nie będą Ci nigdy (albo prawie nigdy) potrzebne. Gdybyś jednak chciał się zapoznać z możliwościami to przy każdej z opcji dostępny jest &quot;Znak zapytania&quot;, na który można kliknąć, aby dowiedzieć się więcej o danym polu.&lt;/p&gt;
&lt;p&gt;Mając stworzone zadanie warto zastanowić się co właściwie chcemy tym zadaniem osiągnąc. W naszym przypadku będziemy mieli następujące kroki:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Ściągnięcie kodu z naszego repozytorium - w moim przypadku będzie to GIT, ale równie dobrze możesz użyć np. serwera SVN&lt;/li&gt;
&lt;li&gt;Po ściągnięciu najnowszego kodu chcemy go zbudować używająć gradle. Oczywiście równie dobrze mógłby to być maven, kroki są analogiczne i na pewno nie sprawią Ci większego problemu&lt;/li&gt;
&lt;li&gt;Na koniec chcemy przeskanować nasz kod SonarQubem w celu sprawdzenia jego jakości&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;W naszym przykładzie to będzie wszystko, aczkolwiek w rzeczywistości można by zrobić dużo więcej. Przykładowo po zbudowaniu kodu można uruchomić testy (a nawet należy - bo testy zawsze warto pisać!) albo nawet pójść o krok dalej i automatycznie deployować naszą aplikacje na serwer (Continious deployment).&lt;/p&gt;
&lt;h4&gt;Ściągnięcie kodu z repozytorium&lt;/h4&gt;
&lt;p&gt;W celu automatycznego pullowania kodu z naszego repozytorium przechodzimy do sekcji &quot;&lt;strong&gt;Source code management&lt;/strong&gt;&quot;. Wybieramy rodzaj repozytorium, którego będziemy używać - w moim przypadku GIT. Musimy jeszcze dodać dane autoryzacyjne. Aby to zrobić klikamy &quot;Add&quot; (zakładając, że robimy to po raz pierwszy) i wybieramy &quot;Jenkins&quot;. W panelu, który się nam pokaże wpisujemy login oraz hasło i zatwierdzamy przyciskiem &lt;em&gt;add&lt;/em&gt;. Dane autoryzacyjne zostaną zapisane i następnym razem będziemy mogli ich użyć wybierając odpowiedni wpis z listy przy polu &quot;Credentials&quot;.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1024px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 34.55149501661129%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAHCAYAAAAIy204AAAACXBIWXMAAA7EAAAOxAGVKw4bAAAA6klEQVQoz5WQyW6EMBBE+f8vjBSNQtDgBWOzGGPDpdLlCaMcMoc5PLXVS3W5m9E5KKXQdV2lbb/hJHccB3LOKKW8RePDhHEcsSwL5nmuxG2rgu9QyoFTYuOmKGLrs3Ce529DqQ5fse97JaVHLCUj7hnNIo6stZimqbo0xqDve2iJdE54AvYwEuY2+QVJacOyrvi4y+lCFEERiTFW+NZayx1bDMOAEEId5rJVhrQdYEWQ+es8nAlSN25E3tNDkM2EDX9dXO4YvfewsoRcNb67u4KRRUHqdPyvIIevoVewp1can7cviTyPRxLBH3QXFdnnjOTrAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Zdjęcie pokazujące wypełnione pole z danymi autoryzacyjnymi&apos; title=&apos;Zdjęcie pokazujące wypełnione pole z danymi autoryzacyjnymi&apos; src=&apos;/static/94f7d63950f8bbf0be58d3dcb724b7c4/2bef9/credentials.png&apos; srcset=&apos;/static/94f7d63950f8bbf0be58d3dcb724b7c4/fb933/credentials.png 301w,
/static/94f7d63950f8bbf0be58d3dcb724b7c4/32056/credentials.png 602w,
/static/94f7d63950f8bbf0be58d3dcb724b7c4/2bef9/credentials.png 1024w&apos; sizes=&apos;(max-width: 1024px) 100vw, 1024px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Zdjęcie pokazujące wypełnione pole z danymi autoryzacyjnymi&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;h4&gt;Budowanie aplikacji&lt;/h4&gt;
&lt;p&gt;Teraz przechodzimy do sekcji &quot;Build&quot;. W sekcji tej możemy dodać kroki (jeden lub więcej), które zostaną kolejno wykonane. Takim krokiem może być np. uruchomienie Gradle czy Anta, uruchomienie komendy shellowej (lub CMD w windowsie) itp. W naszym wypadku klikamy &quot;Invoke gradle&quot;, a następnie wybieramy wersje, której chcemy użyć lub zaznaczamy użycie Gradle Wrappera. Ta druga opcja jest rekomendowana, a więc jej użyjemy. Możemy także zaznaczyć &quot;Make gradlew executable&quot; i tak też zrobimy. Zaznaczenie tego pola oznacza, że przed uruchomieniem &lt;em&gt;gradlew&lt;/em&gt; zostanie zrobione coś w stylu &lt;em&gt;chmod +x ./gradlew&lt;/em&gt;. W innym wypadku nasz ./gradlew może nie być wykonywalny i zadanie się nie powiedzie.&lt;/p&gt;
&lt;p&gt;W sekcji &quot;Task&quot; wpisujemy task, który chcemy wykonać:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;clean build&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1024px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 46.51162790697674%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAYAAAAywQxIAAAACXBIWXMAAA7EAAAOxAGVKw4bAAABAElEQVQoz5WSyWrEMBBE9f8/F0hOgcnJjixrs3YvGCrdcsgtjOdQIMvdj+4qCa01pJQ4zxPHcWDfd2zb9rJ26j2oV9RaYK2FMaYrpYRaK0opt8X15v0Ns3YQDBjHEV+PB4ZhQIwROacOvqtC9bP1+Bg9RIgJ38piVgqKZJ3HErgwvgT1xBlt5JVrh/F0apKYZgcbG9Z2c23qb6SUC2afIPiSg2EfQwhXUc7dFw7pmTjEtTW4mPE5LRcwLAv5ljuIx2cwe9mokMH/if9HXlVO0NbBkVU9FE/AEOJvIPlvHT4/E/cZgi00RKUekehSToq80+Dztq1Y1/vqb3C/3iJ//wCRL7K/DnZ21wAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Uruchamianie Gradle w Jenkinsie&apos; title=&apos;Uruchamianie Gradle w Jenkinsie&apos; src=&apos;/static/c8354e3838907f89dbe2d076a455754a/2bef9/Build-gradle-task-1024x477-1.png&apos; srcset=&apos;/static/c8354e3838907f89dbe2d076a455754a/fb933/Build-gradle-task-1024x477-1.png 301w,
/static/c8354e3838907f89dbe2d076a455754a/32056/Build-gradle-task-1024x477-1.png 602w,
/static/c8354e3838907f89dbe2d076a455754a/2bef9/Build-gradle-task-1024x477-1.png 1024w&apos; sizes=&apos;(max-width: 1024px) 100vw, 1024px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Uruchamianie Gradle w Jenkinsie&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;h4&gt;Skanowanie kodu przy użyciu SonarQube&lt;/h4&gt;
&lt;p&gt;Po zbudowaniu projektu, zakładając, że nie zakończy się ono błędem, chcielibyśmy uruchomić skanowanie SonarQubem. Aby to zrobi dodajemy kolejny krok klikająć na &lt;em&gt;&quot;Add Build step&quot;&lt;/em&gt; tuż pod zadaniem uruchomienia gradle. Wybieramy &quot;&lt;em&gt;Execute SonarQube Scanner&quot;&lt;/em&gt;, następnie w części &quot;Analysis properties&quot; musimy wprowadzić informacje niezbędne do przeskanowania naszego projektu. Dostępnych opcji jest cała masa i można je znaleźć w dokumentacji Sonara, ale nam wystarczy zaledwie kilka:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;sonar.login - wygenerowany wcześniej token&lt;/li&gt;
&lt;li&gt;sonar.projectKey - wprowadzony &quot;projectKey&quot; podczas tworzenia projektu&lt;/li&gt;
&lt;li&gt;sonar.java.binaries - definiujemy gdzie są nasze zbudowane klasy&lt;/li&gt;
&lt;li&gt;sonar.java.libraries - lokalizacja dodatkowych bibliotek, których używamy&lt;/li&gt;
&lt;li&gt;sonar.java.source - wersja Javy w naszym projekcie&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Poniżej screen z wprowadzonymi standardowymi wartośćiami - o ile nie masz jakichś szczególnych ścieżek albo wymagań to powinny one zadziałać także u Ciebie (oczywiście poza tokenem i projectKey):&lt;/p&gt;
&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1024px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 48.83720930232558%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAAA7EAAAOxAGVKw4bAAABQ0lEQVQoz42S646CMBSEef9n2+wbGAVFLi29ooCwmT3Ttca4/pBk0kDbOd+ZQ7Hf73E4HJKqqoL3HuM4Isb4mS4XBK3QfH/BxRFFXdfQWsNaC+ccpmlKWpYF67o+dLvd0rd3+tlWnMKCSnkUJLpIFVKFENB3HTrRMAyJlkUo0rDQ9Xp9kQDIqoyDdhEFyYwxYmCSSTag8h7FYtmQAM+aJjEUM+UCiqZpwLa55pZT5edLQp9z5X6OIGtdZpTS7lFLy390w6OtUoaz2+1QliXatkXwAfM8J/Hytm14fVjEOo/AoTxPjG0xPxqxCOkYOk0y1T86eec5J3lHGvZ9n0wof8+JNHnaOfw8uNehkEzrAVVn0BrJUCmVfhtOlIS5vU+0SHaEGIzFSVl0VgyP9Rmnc3NHjonk3STfieeC3CGMEVIvLf8ChHX/ylnMvogAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Konfiguracja SonarQube w Jenkinsie&apos; title=&apos;Konfiguracja SonarQube w Jenkinsie&apos; src=&apos;/static/93bff05518ace06e78347342793dfefa/2bef9/Jenkins-sonar-config-1024x499-1.png&apos; srcset=&apos;/static/93bff05518ace06e78347342793dfefa/fb933/Jenkins-sonar-config-1024x499-1.png 301w,
/static/93bff05518ace06e78347342793dfefa/32056/Jenkins-sonar-config-1024x499-1.png 602w,
/static/93bff05518ace06e78347342793dfefa/2bef9/Jenkins-sonar-config-1024x499-1.png 1024w&apos; sizes=&apos;(max-width: 1024px) 100vw, 1024px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Konfiguracja SonarQube w Jenkinsie&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;h2&gt;Czas na testy!&lt;/h2&gt;
&lt;h3&gt;Uruchomienie stworzonego zadania w Jenkinsie&lt;/h3&gt;
&lt;p&gt;Mamy spełnione wszystkie kroki potrzebne do uruchomienia naszego zadania. Po zapisaniu naszej konfiguracji w Jenkinsie możemy uruchomić stworzone zadanie klikając &quot;Build now&quot; (jeśli jesteśmy na stronie głównej musimy najpierw przejść do naszego zadania klikając na jego nazwe).&lt;/p&gt;
&lt;p&gt;Czekamy chwile, a po wszystkim powinniśmy zobaczyć numer zadania z niebieską kropką, która wskazuje, że wszystko poszło ok:&lt;/p&gt;
&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 417px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 35.21594684385382%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAHCAYAAAAIy204AAAACXBIWXMAABJ0AAASdAHeZh94AAABK0lEQVQoz22RW0/CQBCF9///Hp9USIBoYiSgESIaSUvpDbst3W7pleQ4s9AGiA9f5rI7Z0+n4nA4IMsUkUEpZUjTFFpr8BlHPvuPPNcoitzkWp96QqkUz69zfH59YzgYEENMJhM8PtxjNBrB83zEcYwoiiClNHC+T2KMXxa4G84wnq5guyFiOhPsYifJVaahL15np0mSnJ3kxukl3JNJijBKsE8VNNXcE23bknJkhuu6IeqepjnVVVVd0fWapkZLd/geGyvKEuJ4bMm+NK9cDtyKNufBW/GSRDiyYFUWEGz5fWVhufjodxQEAbbbLe3Pg+M4Jt9sNoYwDPs9dkj6wvBXwgkiCMff4Wm6xNt8ZoQ6McuyDOv1GrZt97XruuaO7/un2M3Qz/txPPwB8rMOX4sjaYQAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Podgląd historii buildów&apos; title=&apos;Podgląd historii buildów&apos; src=&apos;/static/4c7f94ce60181d4fb9d10aa11409d3de/f27fb/jenkins-history.png&apos; srcset=&apos;/static/4c7f94ce60181d4fb9d10aa11409d3de/fb933/jenkins-history.png 301w,
/static/4c7f94ce60181d4fb9d10aa11409d3de/f27fb/jenkins-history.png 417w&apos; sizes=&apos;(max-width: 417px) 100vw, 417px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Podgląd historii buildów&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;W zakładce &quot;Build history&quot; można zobaczyć status wcześniejszych uruchomień zadania (możesz również zauważyć, że moje ma numerek #4 - niestety poprzednie 3 nie wyszły, ale mam nadzieję, że Tobie uda się za pierwszym razem ;) )Możesz także kliknąć na numerek builda, aby przejść do zakładki, w której dostępne są szczegóły zadania takie jak np. logi z konsoli, co jest bardzo przydatne (lub wręcz niezbędne) podczas sprawdzania co poszło nie tak.&lt;/p&gt;
&lt;h3&gt;Sprawdzenie rezultatu w SonarQube&lt;/h3&gt;
&lt;p&gt;Przejdźmy do naszego projektu w aplikacji SonarQube i zobaczmy wynik naszego zadania. Jeśli jesteś na stronie głównej w SonarQubie możesz przejść do &quot;Projects&quot; i wejść do odpowiedniego projektu poprzez kliknięcie na jego nazwe.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Uwaga:&lt;/strong&gt; czasami musisz poczekać chwilę zanim rezultat naszego skanu pojawi się w SonarQubie. Dzieję się tak dlatego, że Jenkins wysyła dane do SonarQube i kończy zadanie, ale SonarQube dopiero wtedy zaczyna swoje procesowanie przesłanych danych. W przypadku mojego testowego projektu rezultat będzie prawdopodobnie od razu, aczkolwiek w większych projektach jest pewne opóźnienie i trzeba mieć to na uwadze.&lt;/p&gt;
&lt;p&gt;Następnie możesz przejśc do swojego projektu, aby zobaczyć podsumowanie wykonanego skanowania kodu:&lt;/p&gt;
&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1024px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 53.156146179401986%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAA7EAAAOxAGVKw4bAAABiUlEQVQoz41TyU0EMRB0QARAAsu+kDgSIgUC4AMiBiQeCC0J8CIBhLSCuTx2+5yZotvLIphdJB718NF1tNvqdf2Gj6bCR12j0x10r2GMARGBnIcVkINzv+G9L/fqqkJVN1zfom5aqOPVCZaPRzi8P8TB3QEuni9g6h690egtoWp7uJh3CLew1jKoiFryUOpaQd0ybhhXCmdPZ0AGyBOmcSzANGEYhr0Y+TznASFwKl9DLR4WOF2d4nx1juXDEpcvl0wAxBSZZ/oXhDQlj5AM1OR4w40bEKu5DOsj9yiwMlsFfhXP1xuIAQsfKqiu77BFq1sYa1gpI8TIqqmobwsloojMCQeO7NmhDhqq7/kBfkCa7GMqZHNC6aes5y6F0CWH1rd/EeZCOi9srS+tGIa8Q0iR0PhmP2HkaDHtRqOQELkV4zDuj+znkTUT8kCPMrB1h3dN35G3D5SmiDVVWDcsXPYmfsTA96Ql065Dww5zCHBMbL5+iLgKYfPqKSd2SmWY5SfJmZiQcxm1TwRGQVVfkF8dAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Podsumowanie błędów znalezionych w Sonarze&apos; title=&apos;Podsumowanie błędów&apos; src=&apos;/static/c9c88e64bd50110ab8a266d103fa16d8/2bef9/Sonar-summary-1024x545-1.png&apos; srcset=&apos;/static/c9c88e64bd50110ab8a266d103fa16d8/fb933/Sonar-summary-1024x545-1.png 301w,
/static/c9c88e64bd50110ab8a266d103fa16d8/32056/Sonar-summary-1024x545-1.png 602w,
/static/c9c88e64bd50110ab8a266d103fa16d8/2bef9/Sonar-summary-1024x545-1.png 1024w&apos; sizes=&apos;(max-width: 1024px) 100vw, 1024px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Podsumowanie błędów&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;W podsumowaniu można zobaczyć dwie zakładki:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;New code - pokazane są statystyki dla nowgo kodu&lt;/li&gt;
&lt;li&gt;Overall code - podsumowanie dla całego kodu&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Główna różnica polega na tym, że jeżeli mamy np. błędy, które mamy &quot;od zawsze&quot; to zostaną one policzone jedynie w &quot;Overall code&quot;, ale nie w &quot;New code&quot;. Dlaczego? Przede wszystkim dlatego, że dla dużych projektów tych błędów mogą być setki i niekoniecznie ktokolwiek będzie ja poprawiał, możemy w takiej sytuacji śledzić jedynie czy sytuacja bieżąca jest wystarczająco dobra, a błędy w starym kodzie poprawiać stopniowo.&lt;/p&gt;
&lt;p&gt;Kolejną przydatną (lub wręcz najważniejszą) zakładką jest &quot;Issues&quot;, która pokazuje konkretne błędy w Twoim kodzie. Możesz w niej podejrzeć każdy znaleziony błąd, a także przefiltrować błędy po krytyczności, typie, miejscu w kodzie itp. Dodatkowo możliwe jest przypisanie poszczególnych błędów do konkretnych osób czy też edycja standardowym opcji dla błędu np. jego krytyczności. Istnieje także możliwość oznaczenia błędu jako &quot;false positive&quot; lub &quot;won&apos;t fix&quot; co może być przydatne gdyż Sonar (jak każde narzędzie) czasami popełnia błędy w swojej ocenie kodu.&lt;/p&gt;
&lt;h3&gt;Problemy z kodem z Liferayowego Service Buildera&lt;/h3&gt;
&lt;p&gt;Jeśli zaczniesz przeglądać znaleziony błędy możesz zauważyć pewien problem - Sonar znajduję &lt;strong&gt;naprawdę dużo błędów&lt;/strong&gt;. Są dwie opcje - albo twój kod jest naprawdę tragiczny albo coś jest nie tak. Jeśli spojrzysz na mój kod testowy to zobaczysz, że praktycznie nie ma tam kodu, a jednak Sonar znalazł sporo błędów. Przyczyna jest dość trywialna - problemem jest automatycznie wygenerowany kod przez Service Buildera. W przypadku mojego testowego projektu mam prawie 100 błędów, mimo że dodałem kod zaledwie w trzech klasach. Dużo, nawet biorąc pod uwagę, że błędy robiłem specjalnie.&lt;/p&gt;
&lt;p&gt;Moglibyśmy oczywiście oznaczyć te błędy jako &quot;False positive&quot;, aczkolwiek w prawdziwym projekcie nie będzie to zbyt rozsąne - liczba błędów byłaby zbyt duża. Tutaj pojawiło się prawie 100 błędów z jednej encji bazodanowej, a w prawdziwym projekcie takich encji mogą być dziesiątki, sam pracowałem przy projekcie z 150 encjami obsługiwanymi przez Service Buildera, a w Twoim projekcie może być ich jeszcze więcej.&lt;/p&gt;
&lt;p&gt;Na szczęście istnieje inny sposób. Możemy powiedzieć Jenkinsowi, aby ignorował niektóre ścieżki przy użyciu właściwośći &lt;em&gt;sonar.exclusions&lt;/em&gt;. Przy użyciu tego &quot;property&quot; możemy wybraż ścieżki do ignorowania. Wydaję się proste, aczkolwiek są z tym dwa problemy:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Jeśli używasz Liferaya 7 to prawdopodobnie masz wiele modułów (a przynajmniej powinieneś bo modularność to dobra rzecz!). To oznacza, że ciężko wskazać jedną ścieżke bo takich modułów, z automatycznie wygenerowanym kodem, może być kilkanaście.&lt;/li&gt;
&lt;li&gt;Moglibyśmy użyć więc jakiegoś &quot;dynamicznego&quot; matchowania nazw, ale to też nie jest do końca proste. W teorii moglibyśmy po prostu ignorować wszystkie ścieżki kończące się na *-api oraz *-service, ale w w praktyce w modułach *-service także jest nasz kod w klasach typu *Impl.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Logiczne wydają się więc dwa rozwiązania:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Jeśli nie masz aż tylu modułów to możesz po prostu ręcznie wypisać ścieżki do ignorowania&lt;/li&gt;
&lt;li&gt;Wymyślić jakąś &quot;ogólną&quot; regułe, która zadziałałaby w Twoim projekcie. Reguła ta musi być wystarczająco dokładna, aby wyeliminować wszystkie miejsca, których nie chcemy skanować, ale jednocześnie nie powinna wykluczać Twojego kodu&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Ogólna reguła wykluczenia&lt;/h4&gt;
&lt;p&gt;Osobiście nie miałem komfortu pracy przy jedynie jednym czy dwóch modułach z serwisami więc wybrałem opcje numer dwa. Poniższej reguły używam w prawdziwym projekcie i mogę powiedzieć, że działa ona dobrze. Konfiguracja to:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;sonar.exclusions=**/build/resources/main/META-INF/resources/**/*.jsp,**/*-service/**/model/impl/**/*.java,**/*-service/**/service/persistence/**/**/*.java,**/*-service/**/service/base/**/*.java,**/*-service/**/service/http/**/*.java,**/*-service/**/service/util/**/*.java,**/*-api/**/dto/**/*.java,**/*-api/**/exception/**/*.java,**/*-api/**/model/**/*.java,**/*-api/**/service/**/*.java&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Mamy więc zdefiniowane ścieżki rozdzielone przecinkami. Używamy także dwóch dostępnych możliwośći w określaniu ścieżek:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;* - zastępuję jeden lub więcej znak w nazwie pliku&lt;/li&gt;
&lt;li&gt;** - zastępuje jeden lub więcej katalogów&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Reguła ta ignoruje automatycznie wygenerowany kod zarówno w modułach *-service jak i *-api. Czy jest to reguła idealna? Niestety nie. Zawiera ona pewne założenia co do struktury twojego projektu, przykładowo ignoruje ona &lt;code&gt;**/*-service/**/service/util/**/*&lt;/code&gt; ale równie dobrze możesz mieć moduł o nazwie przykładowo blogs-service a w środku posiadać katalog service/util z swoim kodem. Co wtedy? Niestety nie ma prostego rozwiązania. Możesz albo doprecyzować regułę do swojego przypadku albo wybrać opcje z ręcznym wskazywaniem katalogów. W większości przypadków powinna ona jednak zadziałać.&lt;/p&gt;
&lt;h3&gt;Kolejny test&lt;/h3&gt;
&lt;p&gt;Po dodaniu wspomnianej wyżej reguły rezultat uruchomienia naszego zadania w Jenkinsie powinien być zdecydowanie lepszy. W moim przypadku z 100 błędów zostało mi zaledwie 10 i rzeczywiście są one rzeczywistami błędami (czy też tak zwanymi &quot;code smell&quot;). Dodatkowo widzę, że posiadam jeden błąd w klasie *LocalServiceImpl co potwierdza, że reguła zadziałała poprawnie i nie ignoruje zbyt wiele.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 362px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 160.46511627906978%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAgCAYAAAASYli2AAAACXBIWXMAABJ0AAASdAHeZh94AAAEXUlEQVRIx51WaXPiSAzl//+ofJmtZBPu+zTGJzZg44P7yFs9gSkqU5NJbVc9+pBaVkvq15QgLdmfkQryLMU6SbDb7bHdbnHY73G8fGJ7vOg4y3PsNhtkhws2ssb2+fmJ51biT5ifEGYHzIwJRuMJgiBAGIaIVkvszsBafqLVClPTROh7mMV7eOlBDVzF4OcTSl+/8H9bYUc9XK/XsB0XvufBms1wvlyxyTOZu/AcB9RN1jFs21EdPwjhicycTmFZMwlTqgZHoxFKx+NR45VI7LIsQxyvcT4d4SxT/DPw8D72kW332G03KqNeKnqqG0WyFmMj+0+nk3x0jRIFaZoil4BvJOCErnGTrGX5bU75brdTUKfQpzPcT8P8WIkL/X5fXLcexoiNbGAoXMfCUI7SarUxHo9VdyWe0aDqyZ6VJKzX6ylKFBTWC6VncJ2GuYlYLpfq0Vc9yiaTye3Ihfu5HuV+xPu8CAPjtL1jc9cr9Bke1u5MEqoe5vLFaLnAMgy0zxLxaBHq+Cuos1zc9AqdVCrgsN9qhahBdxljZM/RnVjomy5mQaQ919gTQ8uX3kPXsHXemzp3uJj6S7hRhpFhohSnGZp2pMKJv4IhMAP5gBNiYM11bksJcc71sbtQcDwLZe7J3A7gZif82zVQihKx7Mdo1Sqo16roddrod9votBqoVyuKfreDdrOBVqOOhuiwH/a7qJbfVe5LJfBml3tTGkwxDhI0RalcqaLZbKLT6WiJ1Ot1LQUGez6fyz0fwzRnCOSem7LW6XRhyG25XoU8rsBHz7gZHM0TuUaGpN2AI1dwMBxiYhjaR1JzP7jI2F8eBjNMxMNWvYJUspslsWYtFqbZb/Nix19tnj/vR47FYMuJ8VZrS6AjCX6A6XwlCfFhhWtE2zOWmyNWm9MfEW1P8PMz3jrGrbDnkvJFfsA82SJId/DXG4TSz6V3Vxk8kbt3PI8fWKXw4hzDiXmrQx7VtS1Y5lT7eLWAPTOFumyFIxRVjLnOuW2J3OX8tmcvbEQqe7BNwttCOpJ7mykxJPockLK4Rp3bPNa7TXCP6smYV9IURi8VjNHtduG6roKCoWSYY5YLS2gwGGjPEiIzUc49XKMemxrM7zTF+nt/f8fHxwfa7Tbe3t60DklXlCmxPvFmwUQFebApORRs4wjV+76vRyJFkY441nDIxp+0B9tw08vLixrx5M3gi8dj0eDzI/QdfvOQ1M73pQDfiMPh8MCzjCjWqbeXN5t4JIUe8siFd4vFQgPN2PKNfgaTxJDQe56IesVJHklhuTAhDD4TQnLgmM9ipVJBtVpFrVbTrJbLZe2ZPBLHr1+/VHa5XDRMpeJNKBiGCq+vr2qMm4tS4bzwmG9Ho9GAIQQyFbahYYbhYZBp55GpSAWWim3berzr9frjfw+PpBD0ih5E98ebnjAczxn+Ltu/GeRNYL0x0DTMwHOdHp7P52/B+LGnI4+yYQnw6Cwf3sviH8LX9/dv+A83iWif7C7ByAAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Błędy SonarQube&apos; title=&apos;Błędy SonarQube&apos; src=&apos;/static/b5ffb0ff59787de27c936591f52f0768/10600/Sonar-less-errors.png&apos; srcset=&apos;/static/b5ffb0ff59787de27c936591f52f0768/fb933/Sonar-less-errors.png 301w,
/static/b5ffb0ff59787de27c936591f52f0768/10600/Sonar-less-errors.png 362w&apos; sizes=&apos;(max-width: 362px) 100vw, 362px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Błędy SonarQube&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;h2&gt;Podsumowanie&lt;/h2&gt;
&lt;p&gt;Podsumowując. Stworzyliśmy zadanie w Jenkinsie, które uruchamia nasz projekt, wysyła rezultat do SonarQube. W Sonarze możemy zweryfikować &quot;jakość&quot; naszego kodu i sprawdzić poszczególny błędy.&lt;/p&gt;
&lt;p&gt;W artykule tym nie poruszyłem wszystkiego bo i tak rozrósł się już za bardzo. Przykładowo nie wspominałem o tzw &quot;Build triggers&quot; czyli możliwości cyklicznego sprawdzania naszego repozytorium w celu sprawdzenia czy istnieje nowy kod, który można zbudować i przeskanować. W praktyce prawdopodobnie chcesz dodać taki wyzwalacz do swojego Jenkinsowego zadania. O wyzwalaczach tych możesz znaleźć wiele informacji w dokumentacji czy innych blogach.&lt;/p&gt;
&lt;p&gt;Kolejnym krokiem automatyzacji może być automatyczne uruchomienie testów, automatyczny deployment czy np. wysyłka maili w przypadku błędów w budowaniu kodu (sekcja post-build actions w konfiguracji zadania) albo może coś jeszcze innego, specyficznego dla procesu w Twojej firmie czy twoim zespole.&lt;/p&gt;
&lt;p&gt;Jenkins daje nam wiele możliwości i zachęcam do dokładniejszego zapoznania się z nimi, niezależnie czy do projektów w Liferayu czy jakichkolwiek innych. Warto również zwrócić uwagę na to, że istnieje masa pluginów, które pozwalają nam się integrować z różnymi narzędziami co dodatkowo poszerza możliwości Jenkinsa.&lt;/p&gt;
&lt;p&gt;Mam nadzieję, że przydał Ci się ten artykuł i pamiętaj, że jeśli masz jakieś pytania to możesz napisać komentarz lub &lt;a href=&quot;/kontakt/&quot;&gt;napisać do mnie&lt;/a&gt;.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Podpisywanie plików XML w Javie. Część druga - podpis pliku XML]]></title><description><![CDATA[Podpisywanie plików XML w Javie. Część druga – podpis pliku XML O czym będzie ten wpis? Poniższy wpis jest drugą częścią artykułu dotyczącego podpisywania plików XML w Javie. W pierwszej części dotyczącej obsługi podpisów elektronicznych w Javie przedstawiłem, czym są podpisy elektroniczne oraz cyfrowe, a także przedstawiłem sposób, w jaki można odczytać dane certyfikatu z podpisu elektronicznego. W drugiej części...]]></description><link>https://pydyniak.pl/podpisywanie-plikow-xml-w-javie-2/</link><guid isPermaLink="false">https://pydyniak.pl/podpisywanie-plikow-xml-w-javie-2/</guid><pubDate>Tue, 17 Apr 2018 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Poniższy wpis jest drugą częścią artykułu dotyczącego podpisywania plików XML w Javie. W pierwszej części dotyczącej &lt;a href=&quot;/podpisywanie-plikow-xml-w-javie-1&quot;&gt;obsługi podpisów elektronicznych w Javie&lt;/a&gt; przedstawiłem, czym są podpisy elektroniczne oraz cyfrowe, a także przedstawiłem sposób, w jaki można odczytać dane certyfikatu z podpisu elektronicznego.&lt;/p&gt;
&lt;p&gt;W drugiej części najpierw przybliżę podstawy teorii standardów podpisywania plików XML, a następnie przedstawię proces podpisywania takiego pliku w języku Java. Wpis zawiera najważniejsze części kodu - całość aplikacji demo można także znaleźć pod adresem &lt;a href=&quot;https://github.com/RafalPydyniak/XmlDigitalSignatureDemo&quot; target=&quot;_self&quot; rel=&quot;nofollow&quot;&gt;https://github.com/RafalPydyniak/XmlDigitalSignatureDemo&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Standardy podpisów XML&lt;/h2&gt;
&lt;p&gt;Podpisywanie pliku XML polega na wygenerowaniu struktury XML, w której zawarte są informacje zarówno o podpisywanym pliku (np. hash danych, które podpisujemy), jak i informacje o samym podpisie (np. wartość podpisu). Co istotne, istnieją standardy definiujące, jak taka struktura powinna wyglądać. Takimi standardami są:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;XML Signature (XMLDSig)&lt;/li&gt;
&lt;li&gt;XAdES&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;XML Signature&lt;/h3&gt;
&lt;p&gt;XML Signature jest rekomendacją W3C co do tego, jak powinny wyglądać &lt;strong&gt;podpisy cyfrowe plików XML&lt;/strong&gt;. Co ciekawe, mimo nazwy, standard ten pozwala podpisywać nie tylko pliki XML, ale również inne dane, które posiadają adres URL.&lt;/p&gt;
&lt;p&gt;Warto zaznaczyć, że XML Signature określa więcej niż jeden sposób wyglądu podpisu. Możemy wyróżnić:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Enveloped Signature&lt;/strong&gt; &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Enveloping Signature&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Detached Signature&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Powyższe sposoby różnią się miedzy sobą sposobem reprezentowania podpisu: w Enveloped oraz Enveloping Signature podpisywane dane znajdują się bezpośrednio w pliku z podpisem, natomiast w Detached Signature wskazujemy jedynie URL, pod którym znajdują się  podpisywane dane. Wszelkie szczegóły można oczywiście znaleźć w &lt;a href=&quot;https://www.w3.org/TR/xmldsig-core1/&quot; target=&quot;_self&quot; rel=&quot;nofollow&quot;&gt;dokumencie opisującym ten standard&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;XAdES&lt;/h3&gt;
&lt;p&gt;XAdES (XML Advanced Electronic Signatures) jest z kolei rozszerzeniem XML Signature definiującym format &lt;strong&gt;podpisu elektronicznego&lt;/strong&gt;. Jest to standard rekomendowany przez Unię Europejską. Jego zastosowanie w Polsce to m.in. wysyłka PIT&apos;ów drogą elektroniczną. XAdES dodaje całą gamę dodatkowych pól do podpisywanego pliku, które różnią się w zależności od wersji samego XAdES&apos;a. Dokładny format XAdES zostanie przedstawiony w późniejszej części wpisu.&lt;/p&gt;
&lt;h2&gt;Podpisywanie pliku XML w Javie&lt;/h2&gt;
&lt;p&gt;Skoro mamy już podstawową wiedzę na temat standardów podpisów cyfrowych i elektronicznych, a także wiemy jak wyciągnąć dane o certyfikacie z podpisu elektronicznego, to możemy przystąpić do podpisania samego pliku XML.&lt;/p&gt;
&lt;p&gt;Podpisywać będziemy XML&apos;a o następującej zawartości:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x3C;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
&amp;#x3C;example&gt;
  &amp;#x3C;message&gt;Hello World!&amp;#x3C;/message&gt;
&amp;#x3C;/example&gt; 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Na początku możemy stworzyć interfejs &lt;em&gt;XmlSigner&lt;/em&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; public interface XmlSigner {
    File sign(Document xmlToSign,File destinationFile, PrivateKey privateKey, X509Certificate certificate) throws XmlSigningException;
    File sign(File xmlToSign,File destinationFile, PrivateKey privateKey, X509Certificate certificate) throws XmlSigningException;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Mamy w nim dwie wersje metody &lt;em&gt;sign&lt;/em&gt;, różniące się jedynie pierwszym parametrem - wynika to z tego, iż chcemy móc dać możliwość wskazania pliku XML do podpisu bądź też samych danych (obiektu klasy org.w3c.Document). Poza danymi w parametrach przekazujemy także certyfikat X509 oraz klucz prywatny - informacje jak wyciągnąć te dane z podpisu elektronicznego (np. Certum, KIR) znajdują się w pierwszej części artykułu.&lt;/p&gt;
&lt;p&gt;W obu wersjach metody &lt;em&gt;sign&lt;/em&gt; będziemy rzucać wyjątek &lt;em&gt;XmlSigningException&lt;/em&gt; - w związku z tym, iż w demo obsługa wyjątków została pominięta to Exception ten będzie leciał zawsze gdy poleci jakikolwiek wyjątek w kodzie - w prawdziwej aplikacji chcemy oczywiście poprawnie obsługiwać wyjątki.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class XmlSigningException extends Exception{
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Implementacja interfejsu XmlSigner&lt;/h3&gt;
&lt;p&gt;Najważniejszym etapem tej części artykułu jest implementacja wcześniej zdefiniowanego interfejsu XmlSigner. Poniżej przedstawiam najważniejsze etapy tej implementacji.&lt;/p&gt;
&lt;p&gt;Zacznijmy od samej metody &lt;em&gt;sign&lt;/em&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Override
    public File sign(Document xmlToSign,File destinationFile, PrivateKey privateKey, X509Certificate certificate) throws XmlSigningException {
        try {
            return tryToSignOrThrow(xmlToSign, destinationFile, privateKey, certificate);
        } catch (Exception e) {
            //TODO this is just a demo, in real life we want to do something better with exceptions
            e.printStackTrace();
            throw new XmlSigningException();
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Jak widzimy zawiera ona tak naprawdę jedynie obsługę wyjątków (a przynajmniej powinna ją zawierać ;)). Dalej idąc mamy metodę &lt;em&gt;tryToSignOrThrow&lt;/em&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private File tryToSignOrThrow(Document xmlToSign,  File resultFile, PrivateKey privateKey, X509Certificate certificate) throws NoSuchAlgorithmException, InvalidAlgorithmParameterException, KeyException, ParserConfigurationException, MarshalException, XMLSignatureException, TransformerException, FileNotFoundException {
        XMLSignature xmlSignature = prepareXmlSignature(certificate.getPublicKey(), xmlToSign, certificate);
        Document doc = getEmptyDocument();
        DOMSignContext domSignContext = new DOMSignContext(privateKey, doc);
        domSignContext.setDefaultNamespacePrefix(&quot;ds&quot;);
        xmlSignature.sign(domSignContext);
 
        saveDocumentToFile(doc, resultFile);
        return resultFile;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;W kolejnych liniach tej metody wykonujemy kilka czynności:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Przygotowujemy obiekt XMLSignature (linijka 2)&lt;/li&gt;
&lt;li&gt;Tworzymy nowy dokument, do którego zostanie zapisany wyjściowy XML (linia 3)&lt;/li&gt;
&lt;li&gt;Przygotowujemy obiekt klasy DOMSignContext, przy użyciu, którego dokonujemy następnie podpisu (linie 4-6)&lt;/li&gt;
&lt;li&gt;Zapisujemy końcowy XML do pliku i go zwracamy (linie 8-9)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Kroki 2-4 powyższej listy są dość proste i raczej nie wymagają dodatkowego wyjaśnienia. Kluczowy jest natomiast krok pierwszy, w którym przygotowujemy całą strukturę zgodną z formatem XAdES, którą możemy podpisać.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private XMLSignature prepareXmlSignature(PublicKey publicKey, Document xmlToSign, X509Certificate certificate) throws NoSuchAlgorithmException, InvalidAlgorithmParameterException, KeyException, ParserConfigurationException {
        XMLSignatureFactory xmlSignFactory = XMLSignatureFactory.getInstance(&quot;DOM&quot;);
        List&amp;#x3C;XMLObject&gt; objects = getObjectsFromData(xmlToSign, xmlSignFactory, certificate);
        SignedInfo signedInfo = getSignedInfo(xmlSignFactory);
        KeyInfo keyInfo = getKeyInfo(publicKey, xmlSignFactory, certificate);
        return xmlSignFactory.newXMLSignature(signedInfo, keyInfo, objects, &quot;Signature&quot;, &quot;SignatureValue&quot;);
    }
 
private List&amp;#x3C;XMLObject&gt; getObjectsFromData(Document xmlToSign, XMLSignatureFactory xmlSignFactory, X509Certificate certificate) throws ParserConfigurationException {
        List&amp;#x3C;XMLObject&gt; objects = new ArrayList&amp;#x3C;&gt;();
        XMLObject dataXmlObject = prepareDataXmlObject(xmlToSign, xmlSignFactory);
        XMLObject xadesXmlObject = prepareXadesXmlObject(xmlSignFactory, certificate);
        objects.add(dataXmlObject);
        objects.add(xadesXmlObject);
 
        return objects;
    }
 
private XMLObject prepareDataXmlObject(Document xmlToSign, XMLSignatureFactory xmlSignFactory) {
        XMLStructure content = new DOMStructure(xmlToSign.getFirstChild());
        return xmlSignFactory.newXMLObject(Collections.singletonList(content), &quot;Data&quot;, &quot;text/xml&quot;, null);
    }
 
private XMLObject prepareXadesXmlObject(XMLSignatureFactory xmlSignFactory, X509Certificate certificate) throws ParserConfigurationException {
        Element qualifyingPropertiesElement = createXadesObject(certificate);
        return xmlSignFactory.newXMLObject(Collections.singletonList(new DOMStructure(qualifyingPropertiesElement)), null, null, null);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;W linijce 3 pobieramy (tworzymy) obiekty. Pierwszym z obiektów jest po prostu obiekt z samymi podpisywanymi danymi (choć potencjalnie mogłoby go nie być, a do danych moglibyśmy się odnosić poprzez adres URL - wtedy byłby to &quot;Detached Signature&quot;, o którym pisałem wcześniej). Drugim obiektem jest natomiast struktura zdefiniowana w formacie XAdES, zawierająca dodatkowe informacje o podpisie. Tworzenie tych obiektów widać dodatkowo w linijkach 11 i 12. Samo przygotowanie pliku z danymi (w metodzie &lt;em&gt;prepareDataXmlObject&lt;/em&gt;) jest bardzo proste i polega jedynie na stworzeniu osobnego obiektu z całością podpisywanych danych. Przygotowanie obiektu XAdES (metoda &lt;em&gt;prepareXadesXmlObject&lt;/em&gt;) jest z kolei bardziej skomplikowane i opisane zostanie poniżej.&lt;/p&gt;
&lt;h3&gt;Przygotowanie obiektu XAdES&lt;/h3&gt;
&lt;p&gt;Pełna struktura obiektu XAdES jest następująca:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x3C;ds:Object&gt;
   &amp;#x3C;QualifyingProperties&gt;
     &amp;#x3C;SignedProperties&gt;
       &amp;#x3C;SignedSignatureProperties&gt;
         (SigningTime)
         (SigningCertificate)
         (SignaturePolicyIdentifier)
         (SignatureProductionPlace)?
         (SignerRole)?
       &amp;#x3C;/SignedSignatureProperties&gt;
       &amp;#x3C;SignedDataObjectProperties&gt;
         (DataObjectFormat)*
         (CommitmentTypeIndication)*
         (AllDataObjectsTimeStamp)*
         (IndividualDataObjectsTimeStamp)*
       &amp;#x3C;/SignedDataObjectPropertiesSigned&gt;
     &amp;#x3C;/SignedProperties&gt;
     &amp;#x3C;UnsignedProperties&gt;
       &amp;#x3C;/UnsignedSignatureProperties&gt;
         (CounterSignature)*
         (SignatureTimeStamp)+
         (CompleteCertificateRefs)
         (CompleteRevocationRefs)
         ((SigAndRefsTimeStamp)*
         (RefsOnlyTimeStamp)*
         (CertificatesValues)
         (RevocationValues)
         (ArchiveTimeStamp)
       &amp;#x3C;/UnsignedSignatureProperties&gt;
     &amp;#x3C;/UnsignedProperties&gt;
   &amp;#x3C;/QualifyingProperties&gt;
 &amp;#x3C;/ds:Object&gt;   
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;W powyższym kodzie nie wszystkie elementy są wymagane - znaki &lt;strong&gt;*&lt;/strong&gt;, &lt;strong&gt;+&lt;/strong&gt; oraz &lt;strong&gt;?&lt;/strong&gt; mają takie samo znaczenie jak w wyrażeniach regularnych, a co za tym idzie, wszystkie elementy oznaczone gwiazdką lub znakiem zapytania są opcjonalne. Inna sprawa - schemat, choć pobrany z oficjalnej dokumentacji, nie oddaje wszystkiego. Przykładowo, UnsignedProperties nie są wymagane, mimo że na schemacie tego nie widać. Dodatkowo niektóre z elementów istnieją tylko w wyższych wersjach XAdES&apos;a.&lt;/p&gt;
&lt;p&gt;Do podstawowych elementów tego obiektu należą:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;QualifyingProperties&lt;/strong&gt; - jest kontenerem dla obiektów SignedProperties i UnsignedProperties&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SignedProperties&lt;/strong&gt; - zawiera właściwości, które zostaną podpisane&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;UnsignedProperties&lt;/strong&gt; - zawiera właściwości, które nie będą podpisywane&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Oczywiście dokładny opis każdego z elementów (wraz z definicją XSD) można znaleźć w oficjalnym dokumencie opisującym XAdES&apos;a.&lt;/p&gt;
&lt;p&gt;Gdy już wiemy, co powinien zawierać podpis, możemy przejść do jego implementacji. Co istotne, w naszym podpisie zupełnie pominiemy UnsignedProperties, które zazwyczaj nie są wymagane i nie wnoszą wiele do podpisu.&lt;/p&gt;
&lt;p&gt;Poniżej najważniejsze części kodu (reszta dostępna na Githubie):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private Element createXadesObject(X509Certificate certificate) throws ParserConfigurationException {
        Document doc = getEmptyDocument();
        Element qualifyingProperties = createQualifyingPropertiesObject(doc);
        Element signedProperties = prepareSignedPropertiesElement(doc, certificate);
        qualifyingProperties.appendChild(signedProperties);
        return qualifyingProperties;
    }
 
private Element createQualifyingPropertiesObject(Document doc) {
        Element qualifyingProperties = createElementInDocument(doc, &quot;xades:QualifyingProperties&quot;, &quot;QualifyingProperties&quot;);
        qualifyingProperties.setAttribute(&quot;Target&quot;, &quot;#Signature&quot;);
        qualifyingProperties.setAttributeNS(&quot;http://www.w3.org/2000/xmlns/&quot;, &quot;xmlns:xades&quot;, &quot;http://uri.etsi.org/01903/v1.3.2#&quot;);
        return qualifyingProperties;
    }
 
private Element prepareSignedPropertiesElement(Document doc, X509Certificate certificate) {
        Element signedProperties = createElementInDocument(doc, &quot;xades:SignedProperties&quot;,&quot;SignedProperties&quot; );
 
        Element signedSignatureProperties = prepareSignedSignatureProperties(doc, certificate);
        Element signedDataObjectProperties = prepareSignedDataObjectProperties(doc);
 
        signedProperties.appendChild(signedSignatureProperties);
        signedProperties.appendChild(signedDataObjectProperties);
        return signedProperties;
    }
 
    private Element prepareSignedSignatureProperties(Document doc, X509Certificate certificate) {
        Element signedSignatureProperties = createElementInDocument(doc, &quot;xades:SignedSignatureProperties&quot;, &quot;SignedSignatureProperties&quot;);
        Element signingTime = prepareSigningTimeElement(doc);
        Element signingCertificate = prepareSigningCertificateElement(doc, certificate);
        signedSignatureProperties.appendChild(signingTime);
        signedSignatureProperties.appendChild(signingCertificate);
        return signedSignatureProperties;
    }
 
    private Element prepareSigningCertificateElement(Document doc, X509Certificate certificate) {
        Element signingCertificate = doc.createElement(&quot;xades:SigningCertificate&quot;);
        Element certificateElement = doc.createElement(&quot;xades:Cert&quot;);
        Element certDigest = doc.createElement(&quot;xades:CertDigest&quot;);
        certificateElement.appendChild(certDigest);
 
        Element issuerSerial = doc.createElement(&quot;xades:IssuerSerial&quot;);
        certificateElement.appendChild(issuerSerial);
        Element x509IssuerName = doc.createElement(&quot;X509IssuerName&quot;);
        issuerSerial.appendChild(x509IssuerName);
        x509IssuerName.appendChild(doc.createTextNode(certificate.getIssuerDN().getName()));
        Element x509SerialNumber = doc.createElement(&quot;X509SerialNumber&quot;);
        issuerSerial.appendChild(x509SerialNumber);
        x509SerialNumber.appendChild(doc.createTextNode(certificate.getSerialNumber().toString()));
 
        signingCertificate.appendChild(certificateElement);
        return signingCertificate;
    }
 
    private Element prepareSignedDataObjectProperties(Document doc) {
        Element signedDataObjectProperties = doc.createElement(&quot;xades:SignedDataObjectProperties&quot;);
        signedDataObjectProperties.setAttribute(&quot;Id&quot;, &quot;SignedDataObjectProperties&quot;);
        Element dataObjectFormat = doc.createElement(&quot;xades:DataObjectFormat&quot;);
        dataObjectFormat.setAttribute(&quot;ObjectReference&quot;, &quot;#Data-Reference&quot;);
        Element mimeType = doc.createElement(&quot;xades:MimeType&quot;);
        mimeType.appendChild(doc.createTextNode(&quot;text/xml&quot;));
        dataObjectFormat.appendChild(mimeType);
        signedDataObjectProperties.appendChild(dataObjectFormat);
 
        Element commitmentTypeIndicationElement = doc.createElement(&quot;xades:CommitmentTypeIndication&quot;);
        signedDataObjectProperties.appendChild(commitmentTypeIndicationElement);
        Element commitmentTypeId = doc.createElement(&quot;xades:CommitmentTypeId&quot;);
        commitmentTypeIndicationElement.appendChild(commitmentTypeId);
        Element identifier = doc.createElement(&quot;xades:Identifier&quot;);
        identifier.appendChild(doc.createTextNode(&quot;http://uri.etsi.org/01903/v1.2.2#ProofOfApproval&quot;));
        commitmentTypeId.appendChild(identifier);
        Element allSignedDataObjects = doc.createElement(&quot;xades:AllSignedDataObjects&quot;);
        commitmentTypeIndicationElement.appendChild(allSignedDataObjects);
        return signedDataObjectProperties;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I kolejno:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;W linijce 3 tworzymy element QualifyingProperties&lt;/li&gt;
&lt;li&gt;W linijce 4 tworzymy element SignedProperties, który następnie dodajemy jako &quot;dziecko&quot; elementu QualifyingProperties w linijce 5&lt;/li&gt;
&lt;li&gt;Samo tworzenie SignedProperties odbywa się w metodzie &lt;em&gt;prepareSignedPropertiesElement&lt;/em&gt;. Tworzone są dwa elementy: SignedSignatureProperties oraz SignedDataObjectProperties, które następnie dodawane są pod elementem &quot;signedProperties&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;W powyższym zrzucie kodu można ponadto znaleźć informacje o tworzeniu różnych elementów, takich jak np. IssuerSerial czy SigningTime, ale są to już szczegóły implementacyjne - dokładny opis, co oznacza każdy z tworzonych elementów, można znaleźć w dokumentacji, natomiast sam proces ich tworzenia jest na tyle prosty, że raczej nie trzeba go szczegółowo opisywać. Generalna idea jest taka, że tworzymy elementy z odpowiednimi danymi i dopinamy je do innych, bardziej ogólnych elementów.&lt;/p&gt;
&lt;h3&gt;Wygenerowanie danych z standardu XML Signature&lt;/h3&gt;
&lt;p&gt;Gdy już mamy nasze obiekty, jeden z danymi, a drugi z informacjami XAdES, to następnym krokiem jest wygenerowanie elementów określonych w XML Signature. Uproszczony schemat XML Signature znajduje się poniżej:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x3C;Signature&gt;
  &amp;#x3C;SignedInfo&gt;
    &amp;#x3C;CanonicalizationMethod /&gt;
    &amp;#x3C;SignatureMethod /&gt;
    &amp;#x3C;Reference&gt;
       &amp;#x3C;Transforms /&gt;
       &amp;#x3C;DigestMethod /&gt;
       &amp;#x3C;DigestValue /&gt;
    &amp;#x3C;/Reference&gt;
    &amp;#x3C;Reference /&gt; etc.
  &amp;#x3C;/SignedInfo&gt;
  &amp;#x3C;SignatureValue /&gt;
  &amp;#x3C;KeyInfo /&gt;
  &amp;#x3C;Object /&gt;
&amp;#x3C;/Signature&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Elementy, które musimy sami wygenerować to SignedInfo oraz KeyInfo. Element SignatureValue zostanie za nas wygenerowany podczas samego podpisu, który jest realizowany przy pomocy klasy &lt;em&gt;XMLSignatureFactory&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;W wcześniej przytoczonym fragmencie z metodą &lt;em&gt;prepareXmlSignature&lt;/em&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private XMLSignature prepareXmlSignature(PublicKey publicKey, Document xmlToSign, X509Certificate certificate) throws NoSuchAlgorithmException, InvalidAlgorithmParameterException, KeyException, ParserConfigurationException {
        XMLSignatureFactory xmlSignFactory = XMLSignatureFactory.getInstance(&quot;DOM&quot;);
        List&amp;#x3C;XMLObject&gt; objects = getObjectsFromData(xmlToSign, xmlSignFactory, certificate);
        SignedInfo signedInfo = getSignedInfo(xmlSignFactory);
        KeyInfo keyInfo = getKeyInfo(publicKey, xmlSignFactory, certificate);
        return xmlSignFactory.newXMLSignature(signedInfo, keyInfo, objects, &quot;Signature&quot;, &quot;SignatureValue&quot;);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Po wygenerowaniu obiektów, w liniach 4-5, generowane są właśnie elementy signedInfo oraz keyInfo metodami &lt;em&gt;getSignedInfo&lt;/em&gt; oraz &lt;em&gt;getKeyInfo&lt;/em&gt;. Poniżej przedstawiony jest ich kod:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private SignedInfo getSignedInfo(XMLSignatureFactory xmlSignFactory) throws NoSuchAlgorithmException, InvalidAlgorithmParameterException {
        Reference dataReference = createDataReference(xmlSignFactory, &quot;#Data&quot;, null, &quot;Data-Reference&quot;);
        SignatureMethod signatureMethod = xmlSignFactory.newSignatureMethod(SignatureMethod.RSA_SHA1, null);
        CanonicalizationMethod canonicalizationMethod = xmlSignFactory.newCanonicalizationMethod(CanonicalizationMethod.INCLUSIVE, (C14NMethodParameterSpec) null);
        return xmlSignFactory.newSignedInfo(canonicalizationMethod, signatureMethod, Collections.singletonList(dataReference));
    }
 
    private Reference createDataReference(XMLSignatureFactory xmlSignFactory, String s, String o, String s2) throws NoSuchAlgorithmException, InvalidAlgorithmParameterException {
        DigestMethod digestMethod = xmlSignFactory.newDigestMethod(DigestMethod.SHA1, null);
        Transform transform = xmlSignFactory.newTransform(CanonicalizationMethod.INCLUSIVE_WITH_COMMENTS, (TransformParameterSpec) null);
        return xmlSignFactory.newReference(s, digestMethod, Collections.singletonList(transform), o, s2);
    }
 
private KeyInfo getKeyInfo(PublicKey publicKey, XMLSignatureFactory xmlSignFactory, X509Certificate certificate) throws KeyException {
        KeyInfoFactory keyInfoFactory = xmlSignFactory.getKeyInfoFactory();
        X509Data x509Data = keyInfoFactory.newX509Data(Collections.singletonList(certificate));
        KeyValue keyValue = keyInfoFactory.newKeyValue(publicKey);
        return keyInfoFactory.newKeyInfo(Arrays.asList(keyValue, x509Data));
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;SignedInfo zgodnie z specyfikacją zawiera referencje do podpisywanych danych oraz bardziej szczegółowe informacje o tym jak podpisywane były dane (np. algorytm generowania hasha). Najważniejsze miejsca kodu:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;W linii 2 tworzona jest referencja do podpisywanych danych (którym wcześniej nadaliśmy odpowiednie ID - Data)&lt;/li&gt;
&lt;li&gt;W linii 3 określamy algorytm hashowania danych - w naszym przypadku użyjemy RSA_SHA1&lt;/li&gt;
&lt;li&gt;W linii 4 określamy metodę kanonizacji (Canonicalization Method), czyli algorytm transformacji, który zostanie użyty na danych przed ich podpisem&lt;/li&gt;
&lt;li&gt;W linii 5 tworzymy ostatecznie obiekt klasy SignedInfo przy użyciu XmlSignFactory, któremu w parametrach podajemy obiekty stworzone w wcześniej opisanych liniach 2-4&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Oprócz SignedInfo tworzymy także KeyInfo, czyli strukturę zawierającą informacje o użytym kluczu. Samym stworzeniem KeyInfo zajmuje się KeyInfoFactory, któremu należy jedynie dostarczyć klucz publiczny i dane certyfikatu X509.&lt;/p&gt;
&lt;h3&gt;Użycie aplikacji&lt;/h3&gt;
&lt;p&gt;Aby skorzystać z aplikacji, użyjemy poniższej klasy Main:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Main {
    /**
     * Just a demo - exceptions are NOT handled and they SHOULD be.
     * Exception can occur at many places like giving wrong PIN or picking not existing slot
     * @param args
     * @throws Exception
     */
    public static void main(String... args) throws Exception {
        SmartCardCertificate certificate = getCertificate();
        XmlSigner xmlSigner = new XadesXmlSigner();
        File signedFile = new File(&quot;out/signed.sig&quot;);
        xmlSigner.sign(new File(&quot;test_xml_to_sign.xml&quot;), signedFile, certificate.getPrivateKey(), certificate.getX509Certificate());
        System.out.println(&quot;Signed file saved in: &quot; + signedFile.getAbsolutePath());
    }
 
    private static SmartCardCertificate getCertificate() throws TokenException, IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
 
 
        SmartCardReader smartCardReader = new GeneralSmartCardReader(&quot;CCPkiP11.dll&quot;);
        List&amp;#x3C;Token&gt; avaliableTokens = smartCardReader.getAvaliableTokens();
        avaliableTokens.forEach(System.out::println);
        System.out.println(&quot;Pick which token to use: &quot;);
        String slotId = br.readLine();
        System.out.println(&quot;Enter pin:&quot;);
        String pin = br.readLine();
        List&amp;#x3C;SmartCardCertificate&gt; certificates = smartCardReader.getCertificates(Integer.parseInt(slotId), pin.toCharArray());
        for (int i=0; i&amp;#x3C;certificates.size(); i++) {
            SmartCardCertificate certificate = certificates.get(i);
            System.out.println(i+&quot;: &quot; + certificate.getAlias());
        }
 
        System.out.println(&quot;Pick certificate:&quot;);
        String pickedCertificate = br.readLine();
 
        return certificates.get(Integer.parseInt(pickedCertificate));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Większa część powyższej klasy, dotycząca pobierania certyfikatu z podpisu elektronicznego, pojawiła się już wcześniej w pierwszej części artykułu. Jedyne zmiany to tak naprawdę linijki 10-13, w których dokonujemy podpisu pliku &quot;test_xml_to_sign.xml&quot; i zapisujemy rezultat w katalogu out/ jako signed.sig.&lt;/p&gt;
&lt;p&gt;Efekt uruchomienia aplikacji jest następujący:&lt;/p&gt;
&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 207px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 271.4975845410628%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAA2ABQDASIAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAAAAECBf/EABUBAQEAAAAAAAAAAAAAAAAAAAAB/9oADAMBAAIQAxAAAAHmZoyoLZYCLTKiCwD/xAAWEAEBAQAAAAAAAAAAAAAAAAAAEUD/2gAIAQEAAQUCx1X/xAAUEQEAAAAAAAAAAAAAAAAAAAAw/9oACAEDAQE/AQ//xAAUEQEAAAAAAAAAAAAAAAAAAAAw/9oACAECAQE/AQ//xAAUEAEAAAAAAAAAAAAAAAAAAABA/9oACAEBAAY/Ai//xAAfEAADAAEDBQAAAAAAAAAAAAAAAREQICFxMUFRYYH/2gAIAQEAAT8hSUH12xeTfEIT0Iuj6IqKirwR2RwR/9oADAMBAAIAAwAAABDDDr5r7wD/xAAYEQADAQEAAAAAAAAAAAAAAAAAEBEhAf/aAAgBAwEBPxBThhDH/8QAFBEBAAAAAAAAAAAAAAAAAAAAMP/aAAgBAgEBPxAP/8QAIBABAAIBAwUBAAAAAAAAAAAAAQARQSFhgRAxUZHhcf/aAAgBAQABPxDUU/XiVwV0MTHvRUvTG1tlkK8S+0G4e4qPkpv6ljlOJrRVytO76leUcRAa7R8kdxgZDgeSU+E//9k=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Wybór certyfikatu&apos; title=&apos;Wybór certyfikatu&apos; src=&apos;/static/68901316b29108cd12240e333f0b20c8/5ed84/wybor-cert.jpg&apos; srcset=&apos;/static/68901316b29108cd12240e333f0b20c8/5ed84/wybor-cert.jpg 207w&apos; sizes=&apos;(max-width: 207px) 100vw, 207px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Wybór certyfikatu&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;Początek wygląda dokładnie tak samo jak w pierwszej części artykułu - wybieramy token, wpisujemy pin, a następnie wybieramy jeden z dostępnych certyfikatów.&lt;/p&gt;
&lt;p&gt;Następnie w tle podpisywany jest plik i otrzymujemy informacje o tym, gdzie został on zapisany:&lt;/p&gt;
&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 670px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 2.3255813953488373%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAABABQDASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAAAAEE/8QAFAEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEAMQAAABwAoP/8QAFBABAAAAAAAAAAAAAAAAAAAAEP/aAAgBAQABBQJ//8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAwEBPwE//8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAgEBPwE//8QAFBABAAAAAAAAAAAAAAAAAAAAEP/aAAgBAQAGPwJ//8QAFBABAAAAAAAAAAAAAAAAAAAAEP/aAAgBAQABPyF//9oADAMBAAIAAwAAABAAD//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQMBAT8QP//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQIBAT8QP//EABcQAQEBAQAAAAAAAAAAAAAAAAABMcH/2gAIAQEAAT8QicTH/9k=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Informacja o poprawnym zapisie podpisanego pliku&apos; title=&apos;Informacja o poprawnym zapisie podpisanego pliku&apos; src=&apos;/static/7b2730a6a4e5a994da5c28706aca5623/63a7e/saved-in.jpg&apos; srcset=&apos;/static/7b2730a6a4e5a994da5c28706aca5623/e8ddc/saved-in.jpg 301w,
/static/7b2730a6a4e5a994da5c28706aca5623/e49d1/saved-in.jpg 602w,
/static/7b2730a6a4e5a994da5c28706aca5623/63a7e/saved-in.jpg 670w&apos; sizes=&apos;(max-width: 670px) 100vw, 670px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Informacja o poprawnym zapisie podpisanego pliku&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;Gdy otworzymy ten plik, zobaczymy mniej więcej taką zawartość:&lt;/p&gt;
&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1204px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 21.92691029900332%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAEABQDASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAAAAIF/8QAFgEBAQEAAAAAAAAAAAAAAAAAAQID/9oADAMBAAIQAxAAAAHXo0KEv//EABQQAQAAAAAAAAAAAAAAAAAAABD/2gAIAQEAAQUCf//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQMBAT8BP//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQIBAT8BP//EABQQAQAAAAAAAAAAAAAAAAAAABD/2gAIAQEABj8Cf//EABcQAAMBAAAAAAAAAAAAAAAAAAABERD/2gAIAQEAAT8hrxU//9oADAMBAAIAAwAAABAH7//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQMBAT8QP//EABYRAQEBAAAAAAAAAAAAAAAAAAEAEf/aAAgBAgEBPxBCy//EABoQAQACAwEAAAAAAAAAAAAAAAEAoREhMUH/2gAIAQEAAT8QA9gpsaIsO0T/2Q==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Zawartość podpisanego XML&amp;#39;a&apos; title=&apos;Zawartość podpisanego XML&amp;#39;a&apos; src=&apos;/static/71d22d55e99b785ab5f8099ffc6a04cb/902ab/Podpisany-XML-zawartosc.jpg&apos; srcset=&apos;/static/71d22d55e99b785ab5f8099ffc6a04cb/e8ddc/Podpisany-XML-zawartosc.jpg 301w,
/static/71d22d55e99b785ab5f8099ffc6a04cb/e49d1/Podpisany-XML-zawartosc.jpg 602w,
/static/71d22d55e99b785ab5f8099ffc6a04cb/902ab/Podpisany-XML-zawartosc.jpg 1204w,
/static/71d22d55e99b785ab5f8099ffc6a04cb/e984e/Podpisany-XML-zawartosc.jpg 1495w&apos; sizes=&apos;(max-width: 1204px) 100vw, 1204px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Zawartość podpisanego XML&amp;#39;a&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;Jeśli natomiast otworzymy ten plik w aplikacji sprawdzającej podpisy (np. Szafir), to będziemy mogli zobaczyć, że podpis rzeczywiście jest rozpoznawany i informacje o podpisującym wyświetlają się poprawnie:&lt;/p&gt;
&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 760px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 90.36544850498338%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAASABQDASIAAhEBAxEB/8QAGAABAQEBAQAAAAAAAAAAAAAAAAECAwX/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIQAxAAAAH2dbGFHOhAf//EABgQAAMBAQAAAAAAAAAAAAAAAAAQEUEh/9oACAEBAAEFAs6qUpq//8QAFBEBAAAAAAAAAAAAAAAAAAAAIP/aAAgBAwEBPwEf/8QAFBEBAAAAAAAAAAAAAAAAAAAAIP/aAAgBAgEBPwEf/8QAFBABAAAAAAAAAAAAAAAAAAAAMP/aAAgBAQAGPwIf/8QAGRABAQEBAQEAAAAAAAAAAAAAAQARIXGB/9oACAEBAAE/ITfUAOu/LsDLEhiJmxf/2gAMAwEAAgADAAAAEIMHgP/EABQRAQAAAAAAAAAAAAAAAAAAACD/2gAIAQMBAT8QH//EABURAQEAAAAAAAAAAAAAAAAAABAB/9oACAECAQE/ECn/xAAbEAEAAwADAQAAAAAAAAAAAAABABEhMUFRcf/aAAgBAQABPxApAJqmocFXuJnlZQVtZPK2KWXMiAEALVp3O32f/9k=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Podpisany plik - widok w Szafirze&apos; title=&apos;Podpisany plik - widok w Szafirze&apos; src=&apos;/static/e622f1d875a070beb614aa48091574a2/307d7/Podpisany-plik-widok-w-Szafirze.jpg&apos; srcset=&apos;/static/e622f1d875a070beb614aa48091574a2/e8ddc/Podpisany-plik-widok-w-Szafirze.jpg 301w,
/static/e622f1d875a070beb614aa48091574a2/e49d1/Podpisany-plik-widok-w-Szafirze.jpg 602w,
/static/e622f1d875a070beb614aa48091574a2/307d7/Podpisany-plik-widok-w-Szafirze.jpg 760w&apos; sizes=&apos;(max-width: 760px) 100vw, 760px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Podpisany plik - widok w Szafirze&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;h3&gt;Podsumowanie&lt;/h3&gt;
&lt;p&gt;Podsumowując, w pierwszej części artykułu dowiedzieliśmy się czym właściwie są podpisu elektroniczne i czym różnią się od podpisów cyfrowych, a także udało nam się dostać do danych zapisanych na podpisie elektronicznym. W drugiej części artykułu wykorzystaliśmy natomiast dane zawarte w podpisie elektronicznym do wykonania podpisu pliku XML zgodnego z standardem XAdES. Udało nam się także zweryfikować w zewnętrznej aplikacji, że taki podpis jest poprawnie widziany i odczytywany.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Podpisywanie plików XML w Javie. Część pierwsza - obsługa podpisu elektronicznego]]></title><description><![CDATA[Podpisywanie plików XML w Javie. Część pierwsza – obsługa podpisu elektronicznego O czym będzie ten artykuł? W poniższym artykule zaprezentuję możliwości podpisywania plików XML za pomocą podpisów elektronicznych z wykorzystaniem Javy, a także opiszę, czym właściwie są podpisy elektroniczne, oraz jakie mają zastosowania. Artykuł składa się z dwóch części: Podpis elektroniczny – czym jest oraz jak…]]></description><link>https://pydyniak.pl/podpisywanie-plikow-xml-w-javie-1/</link><guid isPermaLink="false">https://pydyniak.pl/podpisywanie-plikow-xml-w-javie-1/</guid><pubDate>Thu, 08 Mar 2018 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;W poniższym artykule zaprezentuję możliwości podpisywania plików XML za pomocą podpisów elektronicznych z wykorzystaniem Javy, a także opiszę, czym właściwie są podpisy elektroniczne, oraz jakie mają zastosowania.&lt;/p&gt;
&lt;p&gt;Artykuł składa się z dwóch części:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Podpis elektroniczny - czym jest oraz jak obsłużyć go w Javie&lt;/li&gt;
&lt;li&gt;Podpisywanie plików XML - jakie są standardy co do tego jak powinny wyglądać podpisane pliki oraz jak wygenerować taki plik za pomocą Javy&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Poniżej znajduje się pierwsza część dotycząca podpisu elektronicznego.&lt;/p&gt;
&lt;p&gt;W artykule przedstawię kolejne kroki tworzenia aplikacji (choć w niektórych przypadkach w uproszczonej wersji, np. bez informacji o importach), ale całość można także znaleźć na Githubie pod adresem &lt;a href=&quot;https://github.com/RafalPydyniak/XmlDigitalSignatureDemo&quot; target=&quot;_self&quot; rel=&quot;nofollow&quot;&gt;https://github.com/RafalPydyniak/XmlDigitalSignatureDemo&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Podpisy elektroniczne&lt;/h2&gt;
&lt;h3&gt;Podpisy elektroniczne a podpisy cyfrowe&lt;/h3&gt;
&lt;p&gt;Na początku warto rozróżnić czym różnią się podpisy elektroniczne od cyfrowych.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Podpis cyfrowy&lt;/strong&gt; (ang. &lt;em&gt;digital signature&lt;/em&gt;) - w uproszczeniu jest to po prostu matematyczny sposób sprawdzenia autentyczności danego pliku. Protokół podpisu cyfrowego opiera się zazwyczaj na kryptografii asymetrycznej, czyli dostępne mamy dwa klucze - publiczny oraz prywatny. Klucz prywatny służy do podpisania danych, natomiast klucz publiczny do potwierdzenia autentyczności danego pliku. Do generowania kluczy używa się zazwyczaj protokołu RSA lub DSA.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Podpis elektroniczny&lt;/strong&gt; (ang. &lt;em&gt;electronic signature&lt;/em&gt;) - w porównaniu do podpisu cyfrowego termin ten odnosi się do całego mechanizmu składania podpisu pod plikami w formie elektronicznej. Plik podpisany elektronicznie niesie ze sobą takie same skutki prawne jak podpis własnoręczny, choć dokładne regulacje prawne mogą się różnić w poszczególnych państwach. Podpis elektroniczny jest zazwyczaj realizowany za pomocą podpisu cyfrowego w celu zapewnienia jego autentyczności.&lt;/p&gt;
&lt;p&gt;Podsumowując, podpisy cyfrowe oraz podpisy elektroniczne &lt;strong&gt;nie są tym samym&lt;/strong&gt;, jednak należy mieć na uwadze, że w niektórych źródłach terminy te w niewłaściwy sposób używane są zamiennie.&lt;/p&gt;
&lt;p&gt;Sam proces generowania i sprawdzania autentyczności podpisów cyfrowych jest stosunkowo prosty i jego uproszczony opis można znaleźć nawet na Wikipedii.&lt;/p&gt;
&lt;h3&gt;Do czego mogą być użyte podpisy elektroniczne&lt;/h3&gt;
&lt;p&gt;Podpisy elektroniczne mają taką samą moc prawną jak podpisy własnoręczne. Za ich pomocą można na przykład podpisać umowy (co potwierdza art. 78 Kodeksu Cywilnego), załatwić sprawy urzędowe przez Internet, czy też podpisać deklaracje podatkową.&lt;/p&gt;
&lt;p&gt;Właśnie do tego ostatniego, czyli do podpisywania deklaracji podatkowych, może się przydać możliwość podpisywania plików XML. Ministerstwo Finansów udostępnia bramką E-Deklaracje, za pomocą której można złożyć deklaracje online. Bramka ta przyjmuje podpisane pliki XML, a w zamian otrzymujemy identyfikator naszej przesyłki, dzięki któremu możemy śledzić status wysyłki, a następnie pobrać dla niej UPO czyli potwierdzenie złożenia deklaracji podatkowej przez Internet.&lt;/p&gt;
&lt;h2&gt;Obsługa podpisu elektronicznego w Javie - przykład&lt;/h2&gt;
&lt;h3&gt;Biblioteki oraz standard PKCS&lt;/h3&gt;
&lt;p&gt;Do obsługi podpisu elektronicznego wykorzystamy bibliotekę IAIK PKCS#11, choć możliwe jest także skorzystanie z implementacji Sun PKCS#11, która jednak według mojej najlepszej wiedzy nie oferuje tak wiele jak IAIK.&lt;/p&gt;
&lt;p&gt;Warto na początku wspomnieć także o tym, czym jest PKCS#11. Sam akronim PKCS oznacza &quot;Public Key Cryptography Standards&quot; i jest zbiorem standardów kryptograficznych publikowanych przez firmę RSA Security. PKCS#11 jest własnie jednym z tych standardów i dotyczy interfejsu do obsługi tokenów kryptograficznych, takich jak karty z podpisami elektronicznymi, na których znajduje się certyfikat do podpisu elektronicznego. Certyfikaty te wydawane są w Polsce przez kilka instytucji na przykład KIR.&lt;/p&gt;
&lt;h3&gt;Co chcemy uzyskać&lt;/h3&gt;
&lt;p&gt;Początkowo określimy to co właściwie chcemy uzyskać. W naszym podpisie elektronicznym zapisany jest certyfikat oraz klucz prywatny, które razem pozwalają nam dokonać podpisu elektronicznego i właśnie te dane chcemy pobrać z naszego urządzenia.&lt;/p&gt;
&lt;p&gt;Certyfikat ten będziemy reprezentować przez następującą klasę:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class SmartCardCertificate {
    private String alias;
    private X509Certificate x509Certificate;
    private PrivateKey privateKey;

    public SmartCardCertificate(String alias, X509Certificate x509Certificate, PrivateKey privateKey) {
        this.alias = alias;
        this.x509Certificate = x509Certificate;
        this.privateKey = privateKey;
    }

    public String getAlias() {
        return alias;
    }

    public X509Certificate getX509Certificate() {
        return x509Certificate;
    }

    public PrivateKey getPrivateKey() {
        return privateKey;
    }

    @Override
    public String toString() {
        return &quot;SmartCardCertificate{&quot; +
                &quot;alias=&apos;&quot; + alias + &apos;\&apos;&apos; +
                &quot;, x509Certificate=&quot; + x509Certificate +
                &quot;, privateKey=&quot; + privateKey +
                &apos;}&apos;;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Musimy jednak wiedzieć, że w systemie możemy mieć kilka tzw. tokenów, z których każdy może zawierać jeden lub więcej certyfikatów. Przykładowo, w podpisie KIR system wykrywa aż cztery tokeny, z których tylko na jednym dostępny jest certyfikat. Każdy token jest przypisany do danego &quot;slotu&quot; w systemie.&lt;/p&gt;
&lt;p&gt;Aby obsłużyć takie sytuacje, chcemy:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Pobrać listę tokenów (wraz z informacją o slocie, na którym się on znajduje)&lt;/li&gt;
&lt;li&gt;Pobrać listę certyfikatów dla danego tokenu (a dokładniej slotu, pod który jest podpięty ten token)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Zdefiniujmy więc interfejs, który pozwoli nam zrealizować oba te cele:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface SmartCardReader {
    List&amp;#x3C;Token&gt; getAvaliableTokens() throws TokenException;
    List&amp;#x3C;SmartCardCertificate&gt; getCertificates(long slotId, char[] pin) throws CertificateException, NoSuchAlgorithmException, IOException, KeyStoreException;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Wcześniej zdefiniowaliśmy już klasę SmartCardCertificate, więc aby ten kod był kompletny, zdefiniujemy jeszcze klasę Token (dla przejrzystości pominąłem gettery):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Token {
    private long slotId;
    private String label;

    public Token(long slotId, String label) {
        this.slotId = slotId;
        this.label = label;
    }

    @Override
    public String toString() {
        return slotId + &quot;: &quot; + label;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Za pomocą takiego interfejsu możemy już obsłużyć właściwie całą komunikację z naszym podpisem elektronicznym. Zanim przejdziemy do implementacji tego interfejsu, możemy napisać metodę Main, która pobierze i wyświetli certyfikat.&lt;/p&gt;
&lt;p&gt;Czyli po kolei:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Pobieramy listę dostępnych tokenów za pomocą metody &lt;em&gt;getAvaliableTokens()&lt;/em&gt; wraz z informacją, na którym slocie się znajdują.&lt;/li&gt;
&lt;li&gt;Dajemy użytkownikowi możliwość wybrania interesującego go slotu.&lt;/li&gt;
&lt;li&gt;Pobieramy listę certyfikatów dostępnych na tym slocie za pomocą &lt;em&gt;getCertificates(long slotId, char[] pin),&lt;/em&gt; jako parametr podajemy wybrany slot, a także PIN do tokenu na wybranym slocie.&lt;/li&gt;
&lt;li&gt;Wybieramy jeden z dostępnych certyfikatów.&lt;/li&gt;
&lt;li&gt;Na ten moment tylko wyświetlamy informacje o wybranym certyfikacie.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Metoda main realizująca wszystkie powyższe punkty wygląda następująco (klasę &lt;em&gt;GeneralSmartCardReader&lt;/em&gt; zdefiniujemy za chwile):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String... args) throws Exception {
    SmartCardCertificate certificate = getCertificate();
    System.out.println(certificate);
}
private static SmartCardCertificate getCertificate() throws TokenException, IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException {
    BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    SmartCardReader smartCardReader = new GeneralSmartCardReader(&quot;CCPkiP11.dll&quot;);
    List&amp;#x3C;Token&gt; avaliableTokens = smartCardReader.getAvaliableTokens();
    avaliableTokens.forEach(System.out::println);
    System.out.println(&quot;Pick which token to use: &quot;);
    String slotId = br.readLine();
    System.out.println(&quot;Enter pin:&quot;);
    String pin = br.readLine();
    List&amp;#x3C;SmartCardCertificate&gt; certificates = smartCardReader.getCertificates(Integer.parseInt(slotId), pin.toCharArray());
    for (int i=0; i&amp;#x3C;certificates.size(); i++) {
        SmartCardCertificate certificate = certificates.get(i);
        System.out.println(i+&quot;: &quot; + certificate.getAlias());
    }

    System.out.println(&quot;Pick certificate:&quot;);
    String pickedCertificate = br.readLine();

    return certificates.get(Integer.parseInt(pickedCertificate));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Warto zaznaczyć, że całość odbywa się poprzez konsolę, przez którą podajemy między innymi slot, pin, czy też wybrany certyfikat. Obsługa wyjątków została &lt;strong&gt;celowo pominięta,&lt;/strong&gt; choć oczywiście w rzeczywistości powinny one zostać obsłużone. Jako że jednak jest to tylko i wyłącznie proste demo, to możemy sobie pozwolić na takie niedoskonałości.&lt;/p&gt;
&lt;h3&gt;Główny element, czyli klasa GeneralSmartCardReader komunikująca się z podpisem&lt;/h3&gt;
&lt;p&gt;Klasa GeneralSmartCardReader to po prostu implementacja wcześniej zdefiniowanego interfejsu SmartCardReader. Dodatkowo dodajemy pole driver i przypisujemy je w konstruktorze. W polu tym będziemy zapisywać informacje o tym, jaka biblioteka powinna zostać użyta do odczytywania danych z karty. Biblioteki te różnią się w zależności od podpisu elektronicznego i tak przykładowo dla:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;KIR - driver nazywa się CCPkiP11.dll&lt;/li&gt;
&lt;li&gt;Sigillum/PWPW - analogicznie jak wyżej - jest to związane z tym, iż KIR oraz Sigillum/PWPW używają tych samych kart&lt;/li&gt;
&lt;li&gt;Certum - cryptoCertum3PKCS.dll&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Biblioteki te znajdują się w katalogach z oprogramowaniem do danego podpisu. Driver do KIR&apos;a (a więc i do Sigillum/PWPW) zostanie także dołączony do aplikacji demo na Githubie.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class GeneralSmartCardReader implements SmartCardReader {
    private final String driver;

    public GeneralSmartCardReader(String driver) {
        this.driver = driver;
    }

    @Override
    public Lis&amp;#x3C;Token&gt; getAvaliableTokens() throws TokenException {
        //TODO
    }

    @Override
    public List&amp;#x3C;SmartCardCertificate&gt; getCertificates(long slotId, char[] pin) throws CertificateException, NoSuchAlgorithmException, IOException, KeyStoreException {
       //TODO
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Zaczniemy od pierwszej metody, służącej do pobrania tokenów:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    public List&amp;#x3C;Token&gt; getAvaliableTokens() throws TokenException {
        Properties properties = new Properties();
        properties.put(&quot;PKCS11_NATIVE_MODULE&quot;, driver);
        Module module = IAIKPkcs11.getModule(properties);
        Slot[] slotList = module.getSlotList(true);
        List&amp;#x3C;Token&gt; tokens = Arrays.stream(slotList).map(slot -&gt; {
            try {
                return new Token(slot.getSlotID(), slot.getToken().getTokenInfo().getLabel());
            } catch (TokenException e) {
                return null;
            }
        }).filter(Objects::nonNull).collect(Collectors.toList());
        return tokens;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Po kolei:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;W linijce 4 musimy pobrać odpowiedni moduł do obsługi interfejsu PKCS#11. Aby tego dokonać, jako parametr musimy podać listę propertiesów, w której zawarta jest nazwa modułu (czyli właściwie biblioteki). Propertiese tworzymy w linijkach 2,3.&lt;/li&gt;
&lt;li&gt;W linijce 5 pobieramy listę slotów odczytanych z tego modułu. Parametr true oznacza, że pobieramy tylko sloty, na których znajduje się jakiś token.&lt;/li&gt;
&lt;li&gt;W linjkach 6-12 pobieramy listę tokenów. Dla każdego slotu pobieramy numer slotu oraz opis znajdującego się na nim tokenu. Na podstawie tych informacji tworzymy wcześniej zdefiniowane obiekty klasy Token, które następnie zwracamy.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Mając listę tokenów możemy dać użytkownikowi wybór co do tego, który z nich ma zostać użyty. Przejdźmy teraz do etapu pobrania certyfikatów. Poniżej znajduje się cały kod:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Override
    public List&amp;#x3C;SmartCardCertificate&gt; getCertificates(long slotId, char[] pin) throws CertificateException, NoSuchAlgorithmException, IOException, KeyStoreException {
        Security.insertProviderAt(new IAIK(), 2);
        KeyStore keyStore = getPKCS11KeyStore(driver, pin, slotId);
        return getCertificatesFromKeystore(keyStore);
    }

    private KeyStore getPKCS11KeyStore(String driver, char[] pin, long slotId) throws CertificateException, NoSuchAlgorithmException, IOException {
        Properties properties = getProperties(driver, pin, slotId);
        IAIKPkcs11 provider = new IAIKPkcs11(properties);
        Security.insertProviderAt(provider, 2);
        Properties loginProperties = new Properties();
        DefaultLoginManager loginManager = new DefaultLoginManager(loginProperties);
        provider.setLoginManager(loginManager);
        provider.getTokenManager().getKeyStore();
        KeyStore keyStore = provider.getTokenManager().getKeyStore();
        keyStore.load(null, pin);
        return keyStore;
    }

    private Properties getProperties(String driver, char[] pin, long slotID) {
        Properties properties = new Properties();
        properties.setProperty(Constants.KEY_STORE_SUPPORT_PROVIDER, &quot;IAIK&quot;);
        properties.setProperty(Constants.SESSION_POOL_MAX_SIZE, &quot;100&quot;);
        properties.setProperty(Constants.MULTI_THREAD_INIT, &quot;true&quot;);
        properties.setProperty(Constants.LOGIN_KEYSTORE_SESSION_ON_DEMAND, &quot;false&quot;);
        properties.setProperty(Constants.PKCS11_NATIVE_MODULE, driver);
        properties.setProperty(Constants.CHECK_MECHANISM_SUPPORTED, &quot;true&quot;);
        properties.setProperty(Constants.USER_PIN, String.valueOf(pin));
        properties.setProperty(Constants.SLOT_ID, String.valueOf(slotID));
        return properties;
    }

     /**
     * Returns certificates on keystore.
     * Keep in mind that this method is just a sample method so not all exceptions are handled properly!
     * @param keyStore
     * @return
     * @throws KeyStoreException
     */
    private List&amp;#x3C;SmartCardCertificate&gt; getCertificatesFromKeystore(KeyStore keyStore) throws KeyStoreException {
        return Collections.list(keyStore.aliases()).stream().map(alias -&gt; {
            try {
                Key key = keyStore.getKey(alias, null);
                if (key instanceof RSAPrivateKey) {
                    Certificate[] certificateChain = keyStore.getCertificateChain(alias);

                    X509Certificate signerCertificate = (X509Certificate) certificateChain\[0\];
                    boolean[] keyUsage = signerCertificate.getKeyUsage();
                    if ((keyUsage == null) || keyUsage\[0\] || keyUsage\[1\]) {
                        return new SmartCardCertificate(alias, signerCertificate, (PrivateKey) key);
                    }
                }
                return null;
            } catch (Exception e) {
                throw new RuntimeException();
            }
        }).filter(Objects::nonNull).collect(Collectors.toList());
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Najważniejsze części pobierania listy certyfikatów:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;W linijce 4 pobieramy obiekt KeyStore, w którym przechowywane są klucze oraz certyfikaty. Całe pobranie jest stosunkowo proste i odbywa się w metodzie getPKCS11KeyStore.&lt;/li&gt;
&lt;li&gt;W metodzie getProperties określamy listę parametrów potrzebną do stworzenia Providera IAIKPkcs11, którego następnie użyjemy do pobrania obiektu KeyStore. W parametrach podajemy m.in. driver, pin, a także wybrany slot.&lt;/li&gt;
&lt;li&gt;W metodzie getCertificatesFromKeystore (wywoływanej w linijce 5) pobieramy listę obiektów wcześniej stworzonej klasy SmartCardCertificate. Keystore posiada w sobie jeden lub więcej aliasów - dla każdego z tych aliasów zapisujemy sam alias, klucz prywatny oraz certyfikat X509.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Efekt końcowy - użycie aplikacji&lt;/h3&gt;
&lt;p&gt;Odpalając aplikację z wcześniej zdefiniowanej metody main, na początku zostaniemy poproszeni o wybranie jednego z tokenów.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 189px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 58.201058201058196%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAYAAABiDJ37AAAACXBIWXMAAA7DAAAOwwHHb6hkAAABl0lEQVQoz41S23baMBDkY0rBd7Buli1bBgzmAE3SnOb/P2W6WnDKQ0l42LPatTU7M9rZcrlEHMcIeT7/iWUU8XmxWHxGFMUUEZIk4X9jysmDmFlboSgKVLbGfj9ASQGlDerGwbkGTVNjtSogyjWD8cUp/w/QuesFSwDH4wFGS0gC7HzPA7abDcqyhFbyOUApSmRZdmU47BDqwNDWDbrWobaWBq6gtUJ8k/0lIEvOc5jKYrfbMhMhFUl18L5j2aWQMEY/B9h2LXmYw3Uel8uJJVeW2HU9BhrQ9x5rkmyJ6VOAFTFk06VE27Zsfl6sUQfJXUc9x4BCiH8ePojwfea9Z8B+s8XHxzsOhz1Lz/OM1mh+W5uIg1fmi7g+Cq1JTh5K8s0Ywy+qbr00TT/jnsl9fc+aGZ5OZ4zEahxHCKXw+/2VGO5wvpyx9S0Mvf54POHt7QW/yGOpDP17QKU1HKn7Q6r2w4A8S6+AySqhtUl5dcLkwCycpzpMDufQDzHVaUpMKRdkV6inh5rN9Q9ESfytP9Gj/uTtTfJfM7h5ePT4lzQAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Lista z dostępnymi tokenami oraz możliwość wyboru&apos; title=&apos;Wybór tokenu&apos; src=&apos;/static/aa1b3b46e691eb1ee333d6742a69ec90/0f8c7/Przechwytywanie.png&apos; srcset=&apos;/static/aa1b3b46e691eb1ee333d6742a69ec90/0f8c7/Przechwytywanie.png 189w&apos; sizes=&apos;(max-width: 189px) 100vw, 189px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Wybór tokenu&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;Następnie wpisujemy pin.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 85px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 43.529411764705884%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAYAAAAywQxIAAAACXBIWXMAAA7DAAAOwwHHb6hkAAABmklEQVQoz42S3W7TQBSEXSRKL2wqIllVHKkkau31b9XGruNYJLErUkJIUzsxoaVCCPH+z/CxNikUEQkuRjvac3Z2zuwqgyTBdWyEENi2jSNhmmbD6z3X82TdabjjepwFAfZjr9x3XRev7tmuyvevd8yup7ydXlMUBXmeU5YleZbxfj7HswVhHDNffGA8GnGVjZnOZlxNRiRpSv/8TAr/FGsEHz6tqFYlZXVPuS5YlhVfPm+4WSyoqjXmaY90lLN5+MZ8mrFaLVlvNvLSgndSeBBdSLfOb8HxJGM4TIijiMv4kjAKCcOIeJCQJjGWZRIPU26XN1z0QyaTMYmsxdJ1GPYJfP/XuI2gMGUewm6yanKzhMzLanJzZGMgM6sP1LUalmUhtnxnhuabE8xQoBs6h/ohB+oB+8/3UV+qqK9VtFcamqo1D1WLPzp5ij8cHn084jg5RnmhsPdMQVG22HvCJXRd/8vNLij2uRw3cDkVJ3S7XYy2QbvdptPpYBgGrVYLTdPo9Xr/J+g5ksj/5Xt+M9Iu+DL4Gv8Sq/EDYc4SCU00b0wAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Wpisanie Pinu do podpisu&apos; title=&apos;Pin&apos; src=&apos;/static/9b1e198ca918128e993a609235c5b08a/30f5c/Przechwytywanie-1.png&apos; srcset=&apos;/static/9b1e198ca918128e993a609235c5b08a/30f5c/Przechwytywanie-1.png 85w&apos; sizes=&apos;(max-width: 85px) 100vw, 85px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Pin&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;I na koniec z listy dostępnych certyfikatów wybieramy ten, który nas interesuje. Następnie jego zawartość jest wyświetlana na ekranie.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&apos;gatsby-resp-image-figure&apos; style=&apos;&apos;&gt;
    &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 700px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 22.259136212624586%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAEABQDASIAAhEBAxEB/8QAFwABAAMAAAAAAAAAAAAAAAAAAAECBf/EABQBAQAAAAAAAAAAAAAAAAAAAAD/2gAMAwEAAhADEAAAAc2oQD//xAAUEAEAAAAAAAAAAAAAAAAAAAAQ/9oACAEBAAEFAn//xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAEDAQE/AT//xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAECAQE/AT//xAAUEAEAAAAAAAAAAAAAAAAAAAAQ/9oACAEBAAY/An//xAAUEAEAAAAAAAAAAAAAAAAAAAAQ/9oACAEBAAE/IX//2gAMAwEAAgADAAAAEAPP/8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAwEBPxA//8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAgEBPxA//8QAFxABAQEBAAAAAAAAAAAAAAAAAREAIf/aAAgBAQABPxDiFDMrw03/2Q==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Wybór certyfikatu oraz wyświetlenie informacji na nim&apos; title=&apos;Wybór certyfikatu&apos; src=&apos;/static/7e2583d81b19613ddd9cce3a3368a348/29d31/Clipboard01.jpg&apos; srcset=&apos;/static/7e2583d81b19613ddd9cce3a3368a348/e8ddc/Clipboard01.jpg 301w,
/static/7e2583d81b19613ddd9cce3a3368a348/e49d1/Clipboard01.jpg 602w,
/static/7e2583d81b19613ddd9cce3a3368a348/29d31/Clipboard01.jpg 700w&apos; sizes=&apos;(max-width: 700px) 100vw, 700px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Wybór certyfikatu&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;h3&gt;Co dalej?&lt;/h3&gt;
&lt;p&gt;Na podstawie pobranych certyfikatów (a dokładniej X509 oraz klucza prywatnego) możemy już dokonywać podpisów. W następnej części artykułu, dostępnej pod adresem &lt;a href=&quot;/podpisywanie-plikow-xml-w-javie-2&quot;&gt;https://pydyniak.pl/podpisywanie-plikow-xml-w-javie-2/&lt;/a&gt;, opiszę sposób podpisu plików XML, ale oczywiście tak pobrane dane można użyć także do innych celów np. podpisu innego typu plików.&lt;/p&gt;</content:encoded></item></channel></rss>