<?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[Rafał Pydyniak - Liferay Expert Personal Blog]]></title><description><![CDATA[Personal blog of a software engineer passionated with software development, automatization, and devops. Focused mostly on JVM/Liferay related technologies]]></description><link>https://pydyniak.com</link><generator>GatsbyJS</generator><lastBuildDate>Tue, 23 Sep 2025 13:30:04 GMT</lastBuildDate><item><title><![CDATA[Fixing Liferay GA132 Bug: Client Extensions Being Removed After Modification]]></title><description><![CDATA[If you've recently upgraded to Liferay GA132 and noticed that your Client Extensions (CX) keep disappearing from pages after modification, you're not alone. I've encountered this frustrating issue and traced it back to a specific problem in the platform's code.]]></description><link>https://pydyniak.com/why-are-my-cx-to-page-links-are-gone-after-each-deployment/</link><guid isPermaLink="false">https://pydyniak.com/why-are-my-cx-to-page-links-are-gone-after-each-deployment/</guid><pubDate>Wed, 14 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;the-problem-disappearing-client-extensions-in-liferay-ga132&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#the-problem-disappearing-client-extensions-in-liferay-ga132&quot; aria-label=&quot;the problem disappearing client extensions in liferay ga132 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;The Problem: Disappearing Client Extensions in Liferay GA132&lt;/h2&gt;
&lt;p&gt;If you&apos;ve recently upgraded to Liferay GA132 and noticed that your Client Extensions (CX) keep disappearing from pages after modification, you&apos;re not alone. I&apos;ve encountered this frustrating issue and traced it back to a specific problem in the platform&apos;s code.&lt;/p&gt;
&lt;p&gt;The issue originates in the &lt;a href=&quot;https://github.com/liferay/liferay-portal/blob/7.4.3.132-ga132/modules/apps/client-extension/client-extension-type-impl/src/main/java/com/liferay/client/extension/type/internal/configuration/CETConfigurationFactory.java&quot; target=&quot;_self&quot; rel=&quot;nofollow&quot;&gt;CETConfigurationFactory.java&lt;/a&gt;, which contains code that removes Client Extension entries from pages after each deactivation and modification.&lt;/p&gt;
&lt;p&gt;The problematic fragment:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;_clientExtensionEntryRelLocalService.
    deleteClientExtensionEntryRels(
        companyId, _cet.getExternalReferenceCode());
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This code is triggered when a Client Extension is modified or deactivated, causing all relations to pages to be removed. While this issue definitely affects Global JS Client Extensions, it likely impacts other types as well, such as Global CSS and potentially others.&lt;/p&gt;
&lt;p&gt;When looking at the codebase, it appears this behavior was introduced in GA132, though it&apos;s possible it could exist in earlier versions that I haven&apos;t tested.&lt;/p&gt;
&lt;h2 id=&quot;the-impact-development-frustration-and-production-risks&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#the-impact-development-frustration-and-production-risks&quot; aria-label=&quot;the impact development frustration and production risks permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;The Impact: Development Frustration and Production Risks&lt;/h2&gt;
&lt;p&gt;For developers, this bug creates a significant productivity issue. Every time you modify and deploy your Client Extension, you&apos;ll need to manually re-add it to all the pages where it was previously used. In a development environment where frequent changes are the norm, this becomes an enormous time sink.&lt;/p&gt;
&lt;p&gt;More concerning is the potential impact on production environments. If a developer or administrator modifies a Client Extension in production and forgets to re-add it to all the necessary pages, this could result in broken functionality for end users. Since there&apos;s no warning or notification about the removed relations, this type of error could easily go unnoticed until reported by users - creating a poor user experience and potentially affecting business operations.&lt;/p&gt;
&lt;p&gt;The most frustrating part is that there&apos;s no built-in configuration option to disable this behavior. Looking at the code structure, a custom implementation is necessary to solve the problem.&lt;/p&gt;
&lt;h2 id=&quot;official-bug-status&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#official-bug-status&quot; aria-label=&quot;official bug status permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Official Bug Status&lt;/h2&gt;
&lt;p&gt;I did some research and I&apos;ve discovered that this issue has already been reported in the Liferay issue tracking system as LPD-50051, and a fix has been implemented in a pull request.&lt;/p&gt;
&lt;p&gt;However, there&apos;s an important consideration for Community Edition (CE) users: since CE doesn&apos;t receive regular patches, it could be months before this fix makes its way into the next official CE release. If you&apos;re using CE and encountering this issue, you&apos;ll likely need to implement the workaround described in this article until an updated version becomes available.&lt;/p&gt;
&lt;p&gt;Enterprise subscribers may receive the fix sooner through regular patches, but even then, the timeline for patch delivery can vary. The custom implementation described here provides an immediate solution while waiting for the official fix.&lt;/p&gt;
&lt;h2 id=&quot;solutions-creating-a-custom-implementation&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#solutions-creating-a-custom-implementation&quot; aria-label=&quot;solutions creating a custom implementation permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Solutions: Creating a Custom Implementation&lt;/h2&gt;
&lt;p&gt;If you, just like me, cannot wait for next version then you need to figure out a solution.&lt;/p&gt;
&lt;h3 id=&quot;option-1-create-a-custom-servicewrapper&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#option-1-create-a-custom-servicewrapper&quot; aria-label=&quot;option 1 create a custom servicewrapper permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Option 1: Create a Custom ServiceWrapper&lt;/h3&gt;
&lt;p&gt;You could create a custom &lt;code&gt;clientExtensionEntryRelLocalServiceWrapper&lt;/code&gt; that checks if the delete request is coming from the &lt;code&gt;CETConfigurationFactory&lt;/code&gt; class and, if so, prevents the deletion.&lt;/p&gt;
&lt;p&gt;While this approach would work, it requires more complex logic to identify the origin of the delete request, making it potentially fragile if internal implementations change.&lt;/p&gt;
&lt;h3 id=&quot;option-2-replace-the-cetconfigurationfactory-recommended&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#option-2-replace-the-cetconfigurationfactory-recommended&quot; aria-label=&quot;option 2 replace the cetconfigurationfactory recommended permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Option 2: Replace the CETConfigurationFactory (Recommended)&lt;/h3&gt;
&lt;p&gt;A cleaner approach is to provide a custom implementation of the &lt;code&gt;CETConfigurationFactory&lt;/code&gt; itself, where we remove the problematic code, and disable the original factory.&lt;/p&gt;
&lt;p&gt;This is the approach I recommend and will detail below.&lt;/p&gt;
&lt;h2 id=&quot;implementation-guide-replacing-the-cetconfigurationfactory&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#implementation-guide-replacing-the-cetconfigurationfactory&quot; aria-label=&quot;implementation guide replacing the cetconfigurationfactory permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Implementation Guide: Replacing the CETConfigurationFactory&lt;/h2&gt;
&lt;p&gt;Here&apos;s how to implement the solution:&lt;/p&gt;
&lt;h3 id=&quot;step-1-create-a-custom-cetconfigurationfactory&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#step-1-create-a-custom-cetconfigurationfactory&quot; aria-label=&quot;step 1 create a custom cetconfigurationfactory permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Step 1: Create a Custom CETConfigurationFactory&lt;/h3&gt;
&lt;p&gt;Create a custom implementation of &lt;code&gt;CETConfigurationFactory&lt;/code&gt; where you remove the problematic code. Essentially, this will be a copy of the original class with the offending lines removed or commented out.&lt;/p&gt;
&lt;p&gt;The modified version of the &lt;code&gt;modified&lt;/code&gt; method would look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Modified
protected void modified(Map&amp;#x3C;String, Object&gt; properties) throws Exception {
   _properties = properties;

   String externalReferenceCode = _getExternalReferenceCode(properties);

   if (_log.isDebugEnabled()) {
      _log.debug(
         StringBundler.concat(
            &quot;Modifying client extension &quot;, externalReferenceCode,
            &quot;with properties:\n&quot;, MapUtil.toString(properties)));
   }
   else if (_log.isInfoEnabled()) {
      _log.info(&quot;Modifying client extension &quot; + externalReferenceCode);
   }

   ConfigurationFactoryUtil.executeAsCompany(
      _companyLocalService, properties,
      companyId -&gt; {
         try {
            if (_log.isInfoEnabled()) {
               _log.info(
                  StringBundler.concat(
                     &quot;Deleting CET for client extension &quot;,
                     externalReferenceCode, &quot; and company &quot;,
                     companyId));
            }

            _cetManager.deleteCET(_cet);

            if (_log.isInfoEnabled()) {
               _log.info(
                  StringBundler.concat(
                     &quot;Adding CET for client extension &quot;,
                     externalReferenceCode, &quot; and company &quot;,
                     companyId));
            }

            _cet = _cetManager.addCET(
               ConfigurableUtil.createConfigurable(
                  CETConfiguration.class, properties),
               companyId, externalReferenceCode);

            if (_log.isInfoEnabled()) {
               _log.info(
                  StringBundler.concat(
                     &quot;Deleting client extension entry relations &quot;,
                     &quot;for client extension &quot;, externalReferenceCode,
                     &quot; and company &quot;, companyId));
            }
//          commented out code
//          _clientExtensionEntryRelLocalService.
//             deleteClientExtensionEntryRels(
//                companyId, _cet.getExternalReferenceCode());

            if (_log.isInfoEnabled()) {
               _log.info(
                  StringBundler.concat(
                     &quot;Adding client extension entry relations for &quot;,
                     &quot;client extension &quot;, externalReferenceCode,
                     &quot; and company &quot;, companyId));
            }

            if (Objects.equals(
                  _cet.getType(),
                  ClientExtensionEntryConstants.TYPE_THEME_CSS)) {

               _addControlPanelThemeCSSClientExtensionEntryRel(
                  companyId);
            }
         }
         catch (Exception exception) {
            _log.error(
               StringBundler.concat(
                  &quot;Unable to modify client extension &quot;,
                  externalReferenceCode, &quot; for company &quot;, companyId),
               exception);

            throw exception;
         }
      });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; You need to make the same change in the &lt;code&gt;@Deactivate&lt;/code&gt; method as well, since both methods contain the problematic code that removes relations.&lt;/p&gt;
&lt;h3 id=&quot;step-2-disable-the-original-factory&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#step-2-disable-the-original-factory&quot; aria-label=&quot;step 2 disable the original factory permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Step 2: Disable the Original Factory&lt;/h3&gt;
&lt;p&gt;To disable the original implementation, create a configuration file at:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;osgi/configs/com.liferay.portal.component.blacklist.internal.configuration.ComponentBlacklistConfiguration.config
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With the following content:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-properties&quot;&gt;blacklistComponentNames=[ \
  &quot;com.liferay.client.extension.type.internal.configuration.CETConfigurationFactory&quot; ,\
  ]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will prevent the original component from being activated, allowing your custom implementation to take over.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Please note&lt;/strong&gt; if you have such file already: simply add a new line there, no need to create anything new.&lt;/p&gt;
&lt;h2 id=&quot;but-what-about-cleanup&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#but-what-about-cleanup&quot; aria-label=&quot;but what about cleanup permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;But What About Cleanup?&lt;/h2&gt;
&lt;p&gt;You might be wondering: &quot;If we&apos;re preventing the deletion of these relations, what happens when we actually want to remove a Client Extension entirely?&quot;&lt;/p&gt;
&lt;p&gt;The good news is that this shouldn&apos;t cause any issues. When a CET (Client Extension Type) is removed, references to it become orphaned. Even if the &lt;code&gt;ClientExtensionEntryRel&lt;/code&gt; entries remain in the database, they won&apos;t affect anything since the system checks for a valid CET before trying to load or use a Client Extension.&lt;/p&gt;
&lt;p&gt;In other words, with the CET removed, the page won&apos;t try to load the Client Extension anyway, making the cleanup of relations unnecessary for normal operation.&lt;/p&gt;
&lt;h2 id=&quot;conclusion&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#conclusion&quot; aria-label=&quot;conclusion permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;This bug in Liferay GA132 can be quite frustrating for developers working with Client Extensions,
but with the custom implementation described above, you can prevent the automatic removal of Client Extensions from pages.&lt;/p&gt;
&lt;p&gt;The solution is straightforward to implement and shouldn&apos;t cause any issues with the normal operation of your Liferay instance.
By replacing the problematic factory class, you ensure that your Client Extensions remain where you put them, even after modifications or redeployments.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Note: This solution has been tested in specific project environment. As always, thoroughly test any modifications in a staging environment before applying them to production.&lt;/em&gt;&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Resolving Mockito Errors After JDK Upgrades in Liferay Portal]]></title><description><![CDATA[As everyone knows upgrading Liferay Portal or migrating to a newer JDK version (such as JDK 21) can be a pain.  Developers might encounter unexpected errors that can slow down development and testing processes. In this article, we'll explore a specific issue related to Mockito and provide a simple solution.]]></description><link>https://pydyniak.com/resolving-mockito-errors-after-jdk-upgrade/</link><guid isPermaLink="false">https://pydyniak.com/resolving-mockito-errors-after-jdk-upgrade/</guid><pubDate>Wed, 30 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;As everyone knows upgrading Liferay Portal or migrating to a newer JDK version (such as JDK 21) can be a pain.
Developers might encounter unexpected errors that can slow down development and testing processes.
In this article, we&apos;ll explore a specific issue related to Mockito and provide a simple solution.&lt;/p&gt;
&lt;h2 id=&quot;the-problem-mockito-errors-after-jdk-upgrade&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#the-problem-mockito-errors-after-jdk-upgrade&quot; aria-label=&quot;the problem mockito errors after jdk upgrade permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;The Problem: Mockito Errors After JDK Upgrade&lt;/h2&gt;
&lt;p&gt;If you&apos;ve recently upgraded your Liferay environment to use JDK 21 (or possibly JDK11 or JDK17),
you might encounter test failures with error messages similar to:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Caused by: java.lang.reflect.InaccessibleObjectException: Unable to make field static final java.lang.invoke.MethodHandles$Lookup 
java.lang.invoke.MethodHandles$Lookup.IMPL_LOOKUP accessible: module java.base does not &quot;opens java.lang.invoke&quot; to unnamed module
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These errors occur because newer JDK versions have implemented a stronger module system (Project Jigsaw) that restricts access to internal JDK packages.
Mockito relies on accessing these internal classes for its functionality.&lt;/p&gt;
&lt;h2 id=&quot;understanding-the-root-cause&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#understanding-the-root-cause&quot; aria-label=&quot;understanding the root cause permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Understanding the Root Cause&lt;/h2&gt;
&lt;p&gt;The Java Platform Module System (JPMS) introduced in Java 9 enforces stricter encapsulation of internal Java APIs.
Libraries like Mockito that use reflection to access internal JDK classes need explicit permission to do so in the form of &lt;code&gt;--add-opens&lt;/code&gt; directives.&lt;/p&gt;
&lt;p&gt;In our specific case, Mockito needs access to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;java.lang&lt;/code&gt; package from the &lt;code&gt;java.base&lt;/code&gt; module&lt;/li&gt;
&lt;li&gt;&lt;code&gt;java.lang.invoke&lt;/code&gt; package from the &lt;code&gt;java.base&lt;/code&gt; module&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;the-solution-adding-jvm-arguments&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#the-solution-adding-jvm-arguments&quot; aria-label=&quot;the solution adding jvm arguments permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;The Solution: Adding JVM Arguments&lt;/h2&gt;
&lt;p&gt;To resolve this issue and allow your tests to run properly after upgrading to JDK 21,
you need to add specific JVM arguments to your Gradle build configuration. Here&apos;s how to do it:&lt;/p&gt;
&lt;h3 id=&quot;for-gradle-based-liferay-projects&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#for-gradle-based-liferay-projects&quot; aria-label=&quot;for gradle based liferay projects permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;For Gradle-based Liferay Projects&lt;/h3&gt;
&lt;p&gt;Add the following to your &lt;code&gt;build.gradle&lt;/code&gt; file in the test configuration section:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-groovy&quot;&gt;test {
    jvmArgs = [&apos;--add-opens&apos;, &apos;java.base/java.lang=ALL-UNNAMED&apos;,
               &apos;--add-opens&apos;, &apos;java.base/java.lang.invoke=ALL-UNNAMED&apos;]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For multi modules project you will need to add it to every module that uses Mockito.&lt;/p&gt;
&lt;p&gt;My advice? Add this to all subprojects using top level build.gradle:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-groovy&quot;&gt;subprojects { subproject -&gt;
    test {
        jvmArgs = [&apos;--add-opens&apos;, &apos;java.base/java.lang=ALL-UNNAMED&apos;,
                   &apos;--add-opens&apos;, &apos;java.base/java.lang.invoke=ALL-UNNAMED&apos;]
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This way you won&apos;t have to remember about that each time you use Mockito.&lt;/p&gt;
&lt;h3 id=&quot;for-maven-based-liferay-projects&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#for-maven-based-liferay-projects&quot; aria-label=&quot;for maven based liferay projects permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;For Maven-based Liferay Projects&lt;/h3&gt;
&lt;p&gt;If you&apos;re using Maven instead of Gradle, these arguments in your &lt;code&gt;pom.xml&lt;/code&gt; should do the trick:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;plugin&gt;
    &amp;#x3C;groupId&gt;org.apache.maven.plugins&amp;#x3C;/groupId&gt;
    &amp;#x3C;artifactId&gt;maven-surefire-plugin&amp;#x3C;/artifactId&gt;
    &amp;#x3C;version&gt;3.2.2&amp;#x3C;/version&gt;
    &amp;#x3C;configuration&gt;
        &amp;#x3C;argLine&gt;
            --add-opens java.base/java.lang=ALL-UNNAMED
            --add-opens java.base/java.lang.invoke=ALL-UNNAMED
        &amp;#x3C;/argLine&gt;
    &amp;#x3C;/configuration&gt;
&amp;#x3C;/plugin&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;how-these-arguments-work&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#how-these-arguments-work&quot; aria-label=&quot;how these arguments work permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;How These Arguments Work&lt;/h2&gt;
&lt;p&gt;Let&apos;s understand what these arguments do:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;--add-opens java.base/java.lang=ALL-UNNAMED&lt;/code&gt;: This opens the &lt;code&gt;java.lang&lt;/code&gt; package from the &lt;code&gt;java.base&lt;/code&gt; module to allow access from unnamed modules (which includes your application and its dependencies like Mockito).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;--add-opens java.base/java.lang.invoke=ALL-UNNAMED&lt;/code&gt;: Similarly, this opens the &lt;code&gt;java.lang.invoke&lt;/code&gt; package to unnamed modules.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These arguments effectively tell the JVM to relax its module encapsulation rules specifically for these packages, allowing Mockito to function correctly through reflection.&lt;/p&gt;
&lt;h2 id=&quot;is-it-new-issue&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#is-it-new-issue&quot; aria-label=&quot;is it new issue permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Is it new issue?&lt;/h2&gt;
&lt;p&gt;Well not really. I would say now it happens to many of us as Liferay has dropped support for JDK8 some time ago.
Actually this is what happened to us when I first faced the issue: I was migrating the project to newest Liferay version and we were forced to update JDK as well.
Earlier we did not want to do that due to other limitations we had and extra effort needed to resolve them.&lt;/p&gt;
&lt;p&gt;Also just a month earlier someone I met on DevCon asked me on Slack about exactly the same specific error.
This is why I thought I will create this as an article even though it is rather simpler article than the ones I normally try to publish.&lt;/p&gt;
&lt;h2 id=&quot;additional-considerations&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#additional-considerations&quot; aria-label=&quot;additional considerations permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Additional Considerations&lt;/h2&gt;
&lt;p&gt;Besides these &quot;specific&quot; error fixes you should of course also remember about updating your Mockito version to the newest one.
Along with other libraries like Lombok which can also lead to unexpected tests errors for example with @Slf4j
(which also happened to me updating Lombok did the trick so I&apos;m not going into more details)&lt;/p&gt;
&lt;h2 id=&quot;conclusion&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#conclusion&quot; aria-label=&quot;conclusion permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Upgrading to newer Liferay or JDK can bring numerous benefits but it almost always comes with some effort to make everything work.
Nevertheless updates are important and should be done regularly which also simplifies each of them.
Hopefully the solution above can help you at least a little bit to resolve Mockito related errors in your Liferay Portal projects after such upgrades.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Do you have any questions or doubts? Feel free to &lt;a href=&quot;https://pydyniak.com/contact/&quot; target=&quot;_self&quot; rel=&quot;nofollow&quot;&gt;contact me directly&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Looking for expert Liferay development services? &lt;a href=&quot;https://www.innray.com/contact&quot; target=&quot;_self&quot; rel=&quot;nofollow&quot;&gt;Contact InnRay&lt;/a&gt; for a consultation.&lt;/em&gt;&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Liferay: Factory Configurations]]></title><description><![CDATA[Today, I faced an issue with so-called "Factory configurations" in Liferay, which are a way of defining custom configurations with multiple instances. I could not find any documentation or examples online, so I thought I would share the solution in this short article.]]></description><link>https://pydyniak.com/creating-factory-configurations/</link><guid isPermaLink="false">https://pydyniak.com/creating-factory-configurations/</guid><pubDate>Wed, 19 Jun 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Today, I faced an issue with so-called &quot;Factory configurations&quot; in Liferay, which are a way of defining custom configurations with multiple instances.
I could not find any documentation or examples online, so I thought I would share the solution in this short article.&lt;/p&gt;
&lt;h2 id=&quot;custom-configurations-in-liferay&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#custom-configurations-in-liferay&quot; aria-label=&quot;custom configurations in liferay permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Custom configurations in Liferay&lt;/h2&gt;
&lt;p&gt;As you might or might not know, in Liferay, you can create custom configurations by defining a simple interface and using a few out-of-the-box annotations. What is cool about this is that the whole UI gets generated for you, so you do not have to deal with any portlets, JSPs, React portlets, etc. Another cool thing is that you can choose the scope of your configuration (system, instance aka company, site, portlet).&lt;/p&gt;
&lt;p&gt;A basic example of such a configuration could be the following code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package com.pydyniak.custom.configurations;

import aQute.bnd.annotation.metatype.Meta;
import com.liferay.portal.configuration.metatype.annotations.ExtendedObjectClassDefinition;

@Meta.OCD(
        id = &quot;com.pydyniak.custom.configurations.CustomConfiguration&quot;,
        name = &quot;My cool custom configuration&quot;
)
@ExtendedObjectClassDefinition(
        category = &quot;pydyniak&quot;,
        scope = ExtendedObjectClassDefinition.Scope.COMPANY
)
public interface CustomConfiguration {
    @Meta.AD(name = &quot;My custom Config&quot;, description = &quot;This is just an example configuration&quot;)
    String myCustomConfig();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;which would result in the following configuration under Control Panel -&gt; Instance Settings:
&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: 960px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 42.19269102990034%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAICAYAAAD5nd/tAAAACXBIWXMAAAsTAAALEwEAmpwYAAABFUlEQVR42pWSyU4DMRBE5/9vnPgWJOAryCFskQIKQ5LxPu1txkXbiTjABAlLT2XZ7lK17c6NHsZoGK2hpIJmlUJCDAOICHWUUr6Z53mRUmb4ENERBTxtejy+9njefLDusH55x2q9xXYnIG2GMLEhTYKlvIjzE8gndCEkfArCW2+aDlykxwnKVaOEvfQ4qoCDDBh0bIVLjGE+GXo2tNai7/c47I8IHLsSY2qklJmTxphRA/yCjVKeEHi/q307jqw0wVjP6hpSWYiKPCsjeV7XfyKkgXXEhjUhu6N4+NFAKQd9PkAUl9NcoHbQDB0XXt9NuF9l5GBRX72+Jgr+PeqVdG4MuLqZcPswAdm3i/3re1z+NqUl/QLU22vDU8M1dwAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Custom Config&apos; title=&apos;&apos; src=&apos;/static/9fcd5d85a697f0b6727000e4f75986ad/d9199/custom_config.png&apos; srcset=&apos;/static/9fcd5d85a697f0b6727000e4f75986ad/fb933/custom_config.png 301w,
/static/9fcd5d85a697f0b6727000e4f75986ad/32056/custom_config.png 602w,
/static/9fcd5d85a697f0b6727000e4f75986ad/d9199/custom_config.png 960w&apos; sizes=&apos;(max-width: 960px) 100vw, 960px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Custom Config&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;Using such a configuration is easy. We can just use the ConfigurationProvider OSGi service or ConfigurationProviderUtil, for example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public CustomConfiguration getConfiguration(long companyId) throws ConfigurationException {
    return ConfigurationProviderUtil.getCompanyConfiguration(CustomConfiguration.class, companyId);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is quite simple and straightforward.
There are also a bunch of examples in the Liferay code (just look for the Configuration.java suffix to find them easily),
and it is quite well described in the Liferay docs, so I won&apos;t go into more details.&lt;/p&gt;
&lt;h3 id=&quot;factory-configurations&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#factory-configurations&quot; aria-label=&quot;factory configurations permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Factory configurations&lt;/h3&gt;
&lt;p&gt;One thing that is kind of missing in configurations and is not widely used is the so-called &quot;Factory Configuration&quot;.
The factory configuration is a way of creating one configuration class, very similar to the one presented,
but with extra options that make it possible to have multiple &quot;instances&quot; of this configuration.
One example from Liferay is CORS settings, where you can define multiple entries:
&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: 934px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 15.282392026578073%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAADCAYAAACTWi8uAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAdklEQVR42nWOwQ6EIAxE+f8PFYQWMLIBV2AE1GQPbJOX6WE6HRHCB0QW1jo4t0FrGjA7aEMwhiHlOrSUipzLlFqB43tC9MBlkVBqbYdqwMzjQYeIWxjBe49/08NSTEjpgIhtYbY3rem+h9v0Y361N5yRc0V8Ai+dPeif73IRxgAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Cors Settings&apos; title=&apos;&apos; src=&apos;/static/21d911eaf9ba32196f3149c358cff9c5/078fe/cors_settings.png&apos; srcset=&apos;/static/21d911eaf9ba32196f3149c358cff9c5/fb933/cors_settings.png 301w,
/static/21d911eaf9ba32196f3149c358cff9c5/32056/cors_settings.png 602w,
/static/21d911eaf9ba32196f3149c358cff9c5/078fe/cors_settings.png 934w&apos; sizes=&apos;(max-width: 934px) 100vw, 934px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Cors Settings&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;Doing that is really simple: in &lt;code&gt;@Meta.OCD&lt;/code&gt; you have to use &lt;code&gt;factory=true&lt;/code&gt; option and in &lt;code&gt;@ExtendedObjectClassDefinition&lt;/code&gt;
you need to define which field is used as a name in list view using &lt;code&gt;factoryInstanceLabelAttribute = &quot;CONFIGURATION_METHOD_NAME&quot;&lt;/code&gt;
where &lt;code&gt;CONFIGURATION_METHOD_NAME&lt;/code&gt; is just a name of method (field) from your configuration.
A full example could be a configuration where we want to configure multiple SSO partners with different settings (whatever they might be),
and this was exactly the case I needed:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package com.pydyniak.custom.configurations;

import aQute.bnd.annotation.metatype.Meta;
import com.liferay.portal.configuration.metatype.annotations.ExtendedObjectClassDefinition;

@Meta.OCD(
        factory = true,
        id = &quot;com.pydyniak.custom.configurations.SSOPartnerConfiguration&quot;,
        name = &quot;SSO Partner Login Token&quot;
)

@ExtendedObjectClassDefinition(
        category = &quot;pydyniak&quot;,
        factoryInstanceLabelAttribute = &quot;ssoPartnerName&quot;,
        scope = ExtendedObjectClassDefinition.Scope.COMPANY
)
public interface SSOPartnerConfiguration {

    @Meta.AD(required = false, name = &quot;Enabled&quot;, deflt = &quot;false&quot;,
            description = &quot;Should SSO be enabled for this partner&quot;)
    boolean enabled();

    @Meta.AD(name = &quot;SSO Partner Name&quot;)
    String ssoPartnerName();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The example above, of course simplified, shows how to create a factory configuration. In the portal, this would look like this:
&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: 936px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 57.14285714285714%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA90lEQVR42q1T7WrDMAzM+79UYftT2DusFJo6XyRZXDvxR3LTeaOko9Bmm+CQsOyTLJ8zrS8oihJKFej6HrRlWTaD5nxAZoyFSoQlyrKCtSOY34J5XtA0LS7ClU2Tgx402Okg3soiN2xBFPgQ4JxHFkLEfxm5roTrWfzc1HUafa/xqPgN4T1jgbrucD7XyPMKVdXeLfo0IXMkez+ckleqgZeX/FOHbTtgt9vj5fVN4o/fdbg+QwJe9XjM5UXn52dITw0SxozXmBjHSUTrYcVTt+ucSTL7KuRJOIl2oiyEGNN8qKcEiV2CT/pi7FdIue+9PDvLTfhTPgE/TWD8VDoZpQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;SSO Partners configuration&apos; title=&apos;&apos; src=&apos;/static/f2dff025ae60d192203d8b1771877ffc/6d2da/sso_partners.png&apos; srcset=&apos;/static/f2dff025ae60d192203d8b1771877ffc/fb933/sso_partners.png 301w,
/static/f2dff025ae60d192203d8b1771877ffc/32056/sso_partners.png 602w,
/static/f2dff025ae60d192203d8b1771877ffc/6d2da/sso_partners.png 936w&apos; sizes=&apos;(max-width: 936px) 100vw, 936px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;SSO Partners configuration&lt;/figcaption&gt;
  &lt;/figure&gt;
and creating a new SSO partner:
&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: 936px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 47.840531561461795%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA6klEQVR42q2RTU7DMBCFc04uxkVYcwVWbCoVgWgbKT+Nf2bs2E7zmLhNBIgsCFj67PFY8/zsKYwxGMcLZPrC+G2fc5d1IPR9RLF7KaEpwnBCZwOUjbc1LPs5ZziCfFolC+4PCvv3M16PCoeKULYOp8ahFE41L/Gxdmh1DyOX6xX8JNidO1RVjaZpkWLEMAxX0o1PcYzpR0KIy3nBLkBbD0seStMmOmWl3s2C8gzrBM7JK/5XTPVOdMIsSOzxHyM79M6BiMDsMkScu7VZ8O5+wMOTfC532fqfHT4+R7xVSboUsrM0dWsjvg/4AG/JC3T1i31rAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Adding new SSO Partner&apos; title=&apos;&apos; src=&apos;/static/eb2e2f59aaa49c76832e864d1a851842/6d2da/sso_partner_add.png&apos; srcset=&apos;/static/eb2e2f59aaa49c76832e864d1a851842/fb933/sso_partner_add.png 301w,
/static/eb2e2f59aaa49c76832e864d1a851842/32056/sso_partner_add.png 602w,
/static/eb2e2f59aaa49c76832e864d1a851842/6d2da/sso_partner_add.png 936w&apos; sizes=&apos;(max-width: 936px) 100vw, 936px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Adding new SSO Partner&lt;/figcaption&gt;
  &lt;/figure&gt;
and once we create some SSO partners, we have:
&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: 936px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 47.840531561461795%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAkklEQVR42q2SyRLCIBBE+f+/VOMtAcNiWKcdIHoX0lXvwuFVVzNCqRcko41FzmUYIsD7CLEsT9zuj8ZxeIyE2OZ9QAgslFKhsq4S2yZRSmHoL2pDrW1vWAUpJSZjNjEmiL7hDmNdazce6kJtDFf10+3qjk248m7OvX+Po+Scu7B/QsEVOYWEr3QGotLPZtcOV/IBwO8UULQHxlwAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;List of SSO partners, after adding some&apos; title=&apos;&apos; src=&apos;/static/321e337b55cc93ea905e5c0cadc92ced/6d2da/sso_partners_added.png&apos; srcset=&apos;/static/321e337b55cc93ea905e5c0cadc92ced/fb933/sso_partners_added.png 301w,
/static/321e337b55cc93ea905e5c0cadc92ced/32056/sso_partners_added.png 602w,
/static/321e337b55cc93ea905e5c0cadc92ced/6d2da/sso_partners_added.png 936w&apos; sizes=&apos;(max-width: 936px) 100vw, 936px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;List of SSO partners, after adding some&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;Doing that was quite easy. I could easily find examples in the Liferay code, such as the CORS configuration mentioned earlier: &lt;code&gt;PortalCORSConfiguration&lt;/code&gt;, or the OpenID SSO configuration in &lt;code&gt;OpenIdConnectProviderConfiguration&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;What I could not find easily, though, was the way of accessing these configurations from code.&lt;/p&gt;
&lt;h3 id=&quot;accessing-factory-configurations-in-code&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#accessing-factory-configurations-in-code&quot; aria-label=&quot;accessing factory configurations in code permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Accessing factory configurations in code&lt;/h3&gt;
&lt;p&gt;After some research I found a way of doing that -
first of all we need to use ConfigurationAdmin service&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Reference
private ConfigurationAdmin configurationAdmin;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With this service, we can list configurations using the listConfigurations method.
It takes a filter as a parameter, and what would it be? Well, looking at the sources, I found out that it has to have a format like:
&lt;code&gt;(&amp;#x26;(service.factoryPid=CONFIGURATION_CLASS)(companyId=COMPANY_ID))&lt;/code&gt;
where:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;CONFIGURATION_CLASS&lt;/code&gt; is just the class name of our configuration. In my example, it is: SSOPartnerConfiguration.class.getName()&lt;/li&gt;
&lt;li&gt;&lt;code&gt;COMPANY_ID&lt;/code&gt; is simply the company (instance) ID, as my configuration is company-scoped.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When we use that method we get array of &lt;code&gt;org.osgi.service.cm.Configuration&lt;/code&gt; which is generally ok - we can get our
properties out of it.
We can go one step further, though, and find the configuration we need and convert it to our &lt;code&gt;SSOPartnerConfiguration&lt;/code&gt;.
To do that, we can combine the &lt;code&gt;ConfigurableUtil#createConfigurable&lt;/code&gt; method with the &lt;code&gt;Configuration#getProperties&lt;/code&gt;.
A full example code would be:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Reference
private ConfigurationAdmin configurationAdmin;

private Optional&amp;#x3C;SSOPartnerConfiguration&gt; getSsoPartnerConfiguration(long companyId, String ssoPartner) throws IOException, InvalidSyntaxException {
    Configuration[] configurations = configurationAdmin.listConfigurations(String.format(&quot;(&amp;#x26;(service.factoryPid=%s)(companyId=%s))&quot;, SSOPartnerConfiguration.class.getName(), companyId));
    return Arrays.stream(configurations).map(Configuration::getProperties)
            .map(properties -&gt; ConfigurableUtil.createConfigurable(SSOPartnerConfiguration.class, properties))
            .filter(ssoPartnerConfig -&gt; ssoPartnerConfig.ssoPartnerId().equals(ssoPartner))
            .findAny();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;summary&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#summary&quot; aria-label=&quot;summary permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Summary&lt;/h2&gt;
&lt;p&gt;I hope this short article can help you with your development. It would definitely have saved me some time if I had found it in Google or in Liferay docs.&lt;/p&gt;
&lt;p&gt;The configuration options are well described in Liferay docs, so I suggest you read through them if you are not familiar with the functionality. It&apos;s just this one piece of information that was missing; hopefully, this article is easy enough to follow.&lt;/p&gt;
&lt;p&gt;As always, if you have any questions, do not hesitate to &lt;a href=&quot;https://pydyniak.com/contact/&quot; target=&quot;_self&quot; rel=&quot;nofollow&quot;&gt;contact me&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Also, you can find related code on my &lt;a href=&quot;https://github.com/RafalPydyniak/liferay-blog-samples/tree/master/modules/custom-configurations&quot; target=&quot;_self&quot; rel=&quot;nofollow&quot;&gt;Github&lt;/a&gt;.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Using Navigation Entry for custom functionality in Liferay core views]]></title><description><![CDATA[In some projects there is a need to override some functionalities provided by Liferay out of the box. There are multiple ways of achieving what we need and they are described in Liferay documentation. This extensibility is, in my humble opinion, one of the biggest assets of Liferay platform. Today I would like to share how can you avoid using OSGi fragments for JSP overrides in some cases by using custom Navigation Entries which can save you a lot of work during Liferay version updates.]]></description><link>https://pydyniak.com/using-custom-menu-entry-instead-of-fragments-for-adding-custom-functionalit/</link><guid isPermaLink="false">https://pydyniak.com/using-custom-menu-entry-instead-of-fragments-for-adding-custom-functionalit/</guid><pubDate>Mon, 30 Oct 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In some projects there is a need to override some functionalities provided by Liferay out of the box.
There are multiple ways of achieving what we need and they are described in Liferay documentation.
This extensibility is, in my humble opinion, one of the biggest assets of Liferay platform.
Today I would like to share how can you avoid using OSGi fragments for JSP overrides in some cases by using custom Navigation Entries
which can save you a lot of work during Liferay version updates.&lt;/p&gt;
&lt;h2 id=&quot;background&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#background&quot; aria-label=&quot;background permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Background&lt;/h2&gt;
&lt;p&gt;First lets describe some background and lets find an answer to following questions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#what-are-osgi-fragments&quot;&gt;What are OSGi fragments&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#what-are-osgi-fragments&quot;&gt;Why should you avoid using fragments for JSP overrides and why they are an issue while upgrading your Liferay version&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#why-would-you-even-use-osgi-fragments-for-jsp-overrides-in-real-life-projects&quot;&gt;Why even use OSGi fragments in real life projects&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;what-are-osgi-fragments&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#what-are-osgi-fragments&quot; aria-label=&quot;what are osgi fragments permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;What are OSGi Fragments&lt;/h3&gt;
&lt;p&gt;OSGi fragment is a feature which can be used to extend or modify the existing functionality without
modifying the code of original bundle. In Liferay context OSGi fragments can be used for example to override the complete
JSP file from Liferay sources. Overriding JSPs is the most common use case for OSGi fragments in Liferay but not the only one.
They can also be used for other things like exporting internal package which sometimes can be super handy.&lt;/p&gt;
&lt;h3 id=&quot;why-should-you-avoid-using-fragments-for-jsp-overrides-and-why-they-are-an-issue-while-upgrading-your-liferay-version&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#why-should-you-avoid-using-fragments-for-jsp-overrides-and-why-they-are-an-issue-while-upgrading-your-liferay-version&quot; aria-label=&quot;why should you avoid using fragments for jsp overrides and why they are an issue while upgrading your liferay version permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Why should you avoid using fragments for JSP overrides and why they are an issue while upgrading your Liferay version&lt;/h3&gt;
&lt;p&gt;The overriding of JSPs is powerful but comes with cost. The main issue with this approach is that
when you upgrade Liferay you basically have to verify every single overridden JSP and check
if it is still working. In many cases it won&apos;t because:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Other JSPs which you didn&apos;t override changed and there are some dependencies between them&lt;/li&gt;
&lt;li&gt;API changed: methods, or even classes, used in JSP do not exist anymore&lt;/li&gt;
&lt;li&gt;Version mentioned in fragment host changed and your fragment does no longer attach to the overridden bundle&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Moreover, you might easily miss that something is not working because JSPs generally do not produce compile errors
and you can not rely on IDE as well as it will generally show a lot of errors in overridden JSPs anyway (because of
dependencies which IDE does not know).&lt;/p&gt;
&lt;p&gt;Even if you are not upgrading the fragments can sometimes be tricky and confusing, for example they can stop working
without any errors on startup or during runtime. I even wrote why this could happen some time ago and how
&lt;a href=&quot;/using-custom-system-checker-for-finding-unresolved-bundles/&quot;&gt;custom system checkers can help for such cases&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Of course if you have one or two JSPs overrides it&apos;s fine. The issue is when you have dozens of them.&lt;/p&gt;
&lt;h3 id=&quot;why-would-you-even-use-osgi-fragments-for-jsp-overrides-in-real-life-projects&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#why-would-you-even-use-osgi-fragments-for-jsp-overrides-in-real-life-projects&quot; aria-label=&quot;why would you even use osgi fragments for jsp overrides in real life projects permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Why would you even use OSGi fragments for JSP overrides in real life projects&lt;/h3&gt;
&lt;p&gt;To override JSPs of course ;) This is actually really handy in many occasions where Liferay has some
out of the box solution which only slightly does not fit to your use case. Some real examples from projects I&apos;ve worked on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Modification of login portlet to change redirection behaviour and add new fields/validations to registration form&lt;/li&gt;
&lt;li&gt;Custom fields in addresses form and custom permissions handling&lt;/li&gt;
&lt;li&gt;Adding custom field in Liferay Commerce Channel to describe what is the country of origin of channel for tax purposes&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In fact this functionality can be used in many parts of Liferay code portlets. Both Liferay Portal and Liferay Commerce ones.&lt;/p&gt;
&lt;h2 id=&quot;navigation-entries&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#navigation-entries&quot; aria-label=&quot;navigation entries permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Navigation Entries&lt;/h2&gt;
&lt;p&gt;So what are navigation entries, the &quot;heroes&quot; of this post and screen navigation in general.
Long story short - Screen Navigation framework allows you to create custom navigations for your application.
In JSP it can be used with &lt;code&gt;liferay-frontend:screen-navigation&lt;/code&gt; taglib. For examples you can of course
look into the Liferay code where there are dozens of them. For example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x3C;liferay-frontend:screen-navigation
	context=&quot;&amp;#x3C;%= commerceCountriesDisplayContext.getCountry() %&gt;&quot;
	key=&quot;&amp;#x3C;%= CommerceCountryScreenNavigationConstants.SCREEN_NAVIGATION_KEY_COMMERCE_COUNTRY_GENERAL %&gt;&quot;
	portletURL=&quot;&amp;#x3C;%= currentURLObj %&gt;&quot;
/&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There is also a Liferay documentation describing
&lt;a href=&quot;https://help.liferay.com/hc/en-us/articles/360029046531-Screen-Navigation-Framework&quot; target=&quot;_self&quot; rel=&quot;nofollow&quot;&gt;screen navigation framework&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;how-and-when-navigation-entries-can-be-used-as-osgi-fragment-replacement&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#how-and-when-navigation-entries-can-be-used-as-osgi-fragment-replacement&quot; aria-label=&quot;how and when navigation entries can be used as osgi fragment replacement permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;How and when Navigation Entries can be used as OSGi fragment replacement&lt;/h3&gt;
&lt;p&gt;So as mentioned the Screen Navigation is used in dozens of places in Liferay core code. We can take advantage of that
and in use cases where we need to add new code (new fields to a form for example) we can define
new Screen Navigation Entry instead of overriding the whole JSP.&lt;/p&gt;
&lt;p&gt;This approach has some great advantages:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It is much easier to upgrade Liferay code - in most cases you will not need to do anything!&lt;/li&gt;
&lt;li&gt;These are standard Liferay modules for which you will get normal runtime errors&lt;/li&gt;
&lt;li&gt;Easier to find dependencies issues like using internal package, as there will be more verbose errors in logs&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;going-through-the-code&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#going-through-the-code&quot; aria-label=&quot;going through the code permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Going through the code&lt;/h3&gt;
&lt;p&gt;If you would like to use this approach you start exactly as you would like to find a JSP to override.
My proposal is to check what Portlet is used on a page, then find it in sources and then figure out which JSPs
you would like to override. Instead of overriding JSP with OSGi fragment though you will want to find out perhaps
there is some navigation to which you could add your custom entry.&lt;/p&gt;
&lt;p&gt;Let&apos;s take a real example. In one of the projects I&apos;ve worked on there was a need to add a custom field to country.
We needed to gather some extra information about each country.
For the showcase purpose lets assume it is one field &quot;EU country&quot; which is needed to distinguish EU countries for tax purposes.&lt;/p&gt;
&lt;p&gt;If you look into the portal (Commerce -&gt; Countries) you will see that there is no way of defining that by default:
&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: 393px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 225.58139534883722%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAtCAYAAACu/EtoAAAACXBIWXMAAAsTAAALEwEAmpwYAAADzklEQVR42s1Xu24bVxDdJj+Q0kCalIFh+BPSpE2XJq0bA4Y7w66dIggCJEB+IX2a2Ibtym83ESk+l+JDpChK3OVrl/t+78ncIbmJ5Bi6K7nIBQ53Sa6Gc2fOOXOlpGkGsfIcxRL3l4WSZTnCKIamL3A6nWGqzbEyLLheCMcNSoMD+n6EZnuAZquLWkPFaDyF7fgw1w7WllsKymbL+RbbV04/L8qQ0U2aZVJQ6HG87QK/PsnR1QDbtjCd6rRlD4vFEqZp0lZcyC4O+NtT4OuHOd73gePJBHt7++gc9NFqdVCrNTCZnG6blV8ccNfltbdBse1LLs7whz+A6/dyvOoAo9EEf+1VsV/dR6t9gHqjhXq9iTltXyZLDjimZ1+owNwG0iSB5/twHZe6HyAIQ3hUzzhO5DJkHnoWIs+AbZnEQZMbYdLVMNfwHatUERTxMtVmaKt99AcjqGoXg8EQh4dDboqu67xNUessuxjclIQwOBxTRjbCOIUXRPCDmLabsGLWtgdLErzlIIwoqzEV3oBleVifU4hQjSwK2pxZ50q2UY0cuIa6rqHdUnF8PCFC9zA+GqPXGxCFjqimBwhdG7IMVQStHKLIyYlGUltxgwTnZrMF5vMlvxf0iciRZMBbjqkR9UYHaqcPl5zHccMCoikm1dVcu1JQRKuTJKU/JjKTn4nu+tzly4Ez3BW0sN2raRlM5EplH81mmxQj34CPS4+KOaMGLIiHwr09L7g0NgHDGN3eEN3+iNjus2IMIrf5L5QaASKowEazaVFT1udWxzG5kAw4Q5FquzPC6EiDT1al6TOslisaBzaPAcHTUjUUqb5510C11qNAS7o20CelnJxOyYGGWK6M8iPgUy2mzYQGU4Vsv9FUueNnsmFulqSN5/lbDa9YaldSCp8cSG71Zg/HJzP2NMN0pLX7gZY3NUwJMbI0ZqNIko1hRFFSGjz1qiPg99fAeEFho4jo4khPuf8coz//Cdx8kOMljVKNalmt1NgfPzjnyQTc0SZJc0QfSapwI9mALD+SnBjycRyTtiN2X7HthD4TjdvYnGRA8esiwGolhrxFUvP43jDW1DmbDVjoWlop/wzq/MzQPj/cS9Xwk0nv/x8wTsTZGAWy/IoBZYstHfBVO8WjSo7HBHGtDQUfIz6R7bouKCW4KXh5EZRrt3N8cQe4cR/47Hvg258i+p2IjiEG/SM0h0YQpzPZQaV8eTfHV/eAb34EPr8FfPdLxM6j60uaMadkvlPOUDwsdT58r/p4Vg3wrBLgOV1rA/rCctgXXRpe4niyG6UyAf8GWSqfE/qCjIYAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Country Details Page&apos; title=&apos;&apos; src=&apos;/static/e728adda690b13efc01f9bd873e29b40/d72ec/country-details-page.png&apos; srcset=&apos;/static/e728adda690b13efc01f9bd873e29b40/fb933/country-details-page.png 301w,
/static/e728adda690b13efc01f9bd873e29b40/d72ec/country-details-page.png 393w&apos; sizes=&apos;(max-width: 393px) 100vw, 393px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Country Details Page&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;So we need to find out which portlet is that. Just looking at the URL you can tell this is &lt;code&gt;CommerceCountryPortlet&lt;/code&gt;.
Next step would be to look into the Liferay sources where you can find all related JSP files:
&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: 448px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 75.74750830564784%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAPCAYAAADkmO9VAAAACXBIWXMAAAsTAAALEwEAmpwYAAAB8UlEQVR42pWT7W7TMBiFezMbQgipdFDGmiZ2PpvEie0kzWe7lkn8QuIeuPKz124lJLaO8uMokeM8PsfHngUBw/t7jgUXWKcay6gkFfjoRJi7CeZegndfGW6WHm6+XNbtksFxV5iFIcPd2sNnl4NHMYI0QyAkScHPJb75Ee5WHuYrF58cdlGLNYfrOZglMcO25KgExyA5nroUh1HhOEo8DgX2fYGRtNumaAp+Ua30kQRnoCJYkdEHwXBoc+zGCrtBo6426ESAH6EDLWOUGUOZ81elCh+xBUYMmoC1StA3JYYmpQV8VGUISRNl6uH3h1sMwQPyIoAyY69I/w3cVhmmvsaul5i6Ev02p7gKWsWQ5OzX/RzaOBRXAmu9wdAq1PLkzLi0ovecJo/hCj8fFiho7G0g7aEZqAl6aBKCltalcTjS+7AV5LjAMCg86YwWvwYo/gBH+nE/aowEMc+pL0nSLtK1whZzFdAcm2O7oSMiLeBxoqZpD6ez4z213pybvtIhw3cD7KSN2ZO6Jj9FNyU1wu6p/GfLMT879LEnwEROxragCaEFlDmzLcuMX4S9ACphmo7R1xVFO8UzkVs62KeI/puwlw7JhSIH+4auGBVhC6hSCxIbl6DefwAjZqN1OsHR3NvuXACdS+OwbzI0dIuuBT4DRJwu1YUG0yoAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Countries Portlet JSP files&apos; title=&apos;&apos; src=&apos;/static/f2d59874d918640d798e1ba1b4d0b3a4/33b38/countries-portlet-jsps.png&apos; srcset=&apos;/static/f2d59874d918640d798e1ba1b4d0b3a4/fb933/countries-portlet-jsps.png 301w,
/static/f2d59874d918640d798e1ba1b4d0b3a4/33b38/countries-portlet-jsps.png 448w&apos; sizes=&apos;(max-width: 448px) 100vw, 448px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Countries Portlet JSP files&lt;/figcaption&gt;
  &lt;/figure&gt;
In the screenshot above you can see three JSPs used for navigation in country details portlet:
&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: 280px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 63.21428571428571%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAYAAACpUE5eAAAACXBIWXMAAAsTAAALEwEAmpwYAAABfElEQVR42q1Ta3OCMBDk//+m2tZxRqeireITdIaCj2pVQALySGB7PPvFD9hpZhbC3WXJ7iUS/nlI2YNzjiCIIOIIvZ6Mj5GCTqcHuS+j9zbE8fuUF6dp2oxQcEGEIdIkga4b0LQ1lksNprmBSnPLdpoTZkUVEiK8N4pckf+tLZDPS2Rz6e7iNKkXI/2Dhzffw3OrhfZrG0+tF3wdDnnSsmwM5DGU8RzKRMNuf8zjPI4xn68opmKxXGMyWeBwtApCQSodT8DYW/jcEbYWTnaIWACJELheXVwuNlzm5T5XKnz/hqvLwCjOmI8wjAvCkxPhXU8x2QLTEkP6Nk70pzSTXajO3+SZoB1kqOzIPaV8UuakjL1cUiJBWVnvpu5uky677EZNACLSyLwQQcgRRQIxT0hiTNLChyAx71YYTQSGuUe3K2MwHKE/UKCudPIngO14cK5+I9SEUcRxPjt0sE3MZiqm1MXN7gjmkvlU2BQ1YWYqpxtTHdbqoGexR/ADyb/vaUgAfS4AAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Countries Details&apos; title=&apos;&apos; src=&apos;/static/51d3325c33a002d943a8f088ebe0967f/908b1/country-details-navigation.png&apos; srcset=&apos;/static/51d3325c33a002d943a8f088ebe0967f/908b1/country-details-navigation.png 280w&apos; sizes=&apos;(max-width: 280px) 100vw, 280px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Countries Details&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;Now we need to add new field. One approach would be to override the &lt;code&gt;details.jsp&lt;/code&gt;
with OSGi fragment with all the disadvantages mentioned before.
Another approach is to see how is the navigation menu created and if we can extend it. In this case (any most of them)
the Screen Navigation framework is used which we can see in &lt;code&gt;edit_commerce_country.jsp&lt;/code&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: 927px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 16.943521594684384%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAADCAYAAACTWi8uAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAh0lEQVR42k2OQRLDIAwD85ZOA9gWOAn9/9dUkebQw440lrDZgM6B5OgnE8FjOGc6AWdpzmqiNdZSWGrlLv2x/3mhbOXbKoeWnnPygPHq4BSfrpnIdVCdjODIg66syZvmBtACt6/W+Nrf3FxhKBi5fil1Y5qQQiVvD+q5erH6z5sbLcStTjPjFzzyWbWZSs7tAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Screen Navigation in edit_commerce_country.jsp&apos; title=&apos;&apos; src=&apos;/static/ff003179fe9c9f3f25302c2c5c7dde28/e4374/country-details-screen-navigation.png&apos; srcset=&apos;/static/ff003179fe9c9f3f25302c2c5c7dde28/fb933/country-details-screen-navigation.png 301w,
/static/ff003179fe9c9f3f25302c2c5c7dde28/32056/country-details-screen-navigation.png 602w,
/static/ff003179fe9c9f3f25302c2c5c7dde28/e4374/country-details-screen-navigation.png 927w&apos; sizes=&apos;(max-width: 927px) 100vw, 927px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Screen Navigation in edit_commerce_country.jsp&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;Once we found a screen navigation we know that instead of overriding the whole JSP we can simply
add new menu entry for additional fields we need.&lt;/p&gt;
&lt;p&gt;The steps to do that are:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create new Liferay module&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#adding-web-contextpath&quot;&gt;Make sure to define Web-ContextPath in bnd.bnd file.&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#navigation-category-and-navigation-entry&quot;&gt;Add custom menu entry you will need two components: navigation category and navigation entry&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#custom-jsp&quot;&gt;Add JSP which should be rendered&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#mvc-action-command&quot;&gt;Then of course we need to save our data somehow so we will need a MVC action command&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Creating new module is something you probably know how to do but lets go through steps 2-5&lt;/p&gt;
&lt;h4 id=&quot;adding-web-contextpath&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#adding-web-contextpath&quot; aria-label=&quot;adding web contextpath permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Adding Web-ContextPath&lt;/h4&gt;
&lt;p&gt;This is really straightforward but if you have never done that before you might not know what to do here.
Simply this is only one line which you need to add in bnd.bnd file of your module:
&lt;code&gt;Web-ContextPath: /country-custom-settings&lt;/code&gt;
and it will define a ContextPath for the bundle rendering purposes.
It is required as otherwise we won&apos;t be able to include our custom JSP.&lt;/p&gt;
&lt;h4 id=&quot;navigation-category-and-navigation-entry&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#navigation-category-and-navigation-entry&quot; aria-label=&quot;navigation category and navigation entry permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Navigation category and navigation entry&lt;/h4&gt;
&lt;p&gt;Next step is to define two classes and mark them as OSGi components. First one is the implementation of
&lt;code&gt;ScreenNavigationCategory&lt;/code&gt; which defines a category in screen navigation. The class we need is really simple:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Component(
        property = &quot;screen.navigation.category.order:Integer=100&quot;,
        service = ScreenNavigationCategory.class
)
public class CountryCustomSettingsScreenNavigationCategory
        implements ScreenNavigationCategory {
    @Override
    public String getCategoryKey() {
        return &quot;pydyniak-country-custom-settings&quot;;
    }

    @Override
    public String getLabel(Locale locale) {
        return LanguageUtil.get(locale, &quot;custom-settings&quot;);
    }

    @Override
    public String getScreenNavigationKey() {
        return &quot;commerce.country.general&quot;;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Three methods we use here are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;getCategoryKey: used to define unique key for our category&lt;/li&gt;
&lt;li&gt;getLabel: label used in navigation menu&lt;/li&gt;
&lt;li&gt;getScreenNavigationKey: defines the key of screen navigation for which we want to add the entry. It has to be the same as the key defined in &lt;code&gt;liferay-frontend:screen-navigation&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Next we need an implementation of ScreenNavigationEntry. It has some more code which can be found in full version on
&lt;a href=&quot;https://github.com/RafalPydyniak/liferay-blog-samples/blob/master/modules/country-custom-settings/src/main/java/com/pydyniak/blog/samples/country/custom/settings/CountryCustomSettingsScreenNavigationEntry.java&quot; target=&quot;_self&quot; rel=&quot;nofollow&quot;&gt;my github&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Here are the crucial parts:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Component(
	property = &quot;screen.navigation.entry.order:Integer=10&quot;,
	service = ScreenNavigationEntry.class
)
public class CountryCustomSettingsScreenNavigationEntry
	extends CountryCustomSettingsScreenNavigationCategory
	implements ScreenNavigationEntry&amp;#x3C;Country&gt; {
    
	}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We are defining the navigation entry and marking it as OSGi component. The &lt;code&gt;screen.navigation.entry.order:Integer&lt;/code&gt;
can be used to define the order of navigation entries&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Override
	public void render(
			HttpServletRequest httpServletRequest,
			HttpServletResponse httpServletResponse)
		throws IOException {

		long countryId = ParamUtil.get(httpServletRequest, &quot;countryId&quot;, 0L);
		Country country = countryService.fetchCountry(countryId);
		long companyId = portal.getCompanyId(httpServletRequest);
		setupEuCountryExpando(companyId);
		boolean euCountry = (boolean) country.getExpandoBridge().getAttribute(CountryCustomSettingsConstants.EU_COUNTRY_EXPANDO_KEY);

		httpServletRequest.setAttribute(CountryCustomSettingsConstants.PARAM_COUNTRY_ID, countryId);
		httpServletRequest.setAttribute(CountryCustomSettingsConstants.EU_COUNTRY_EXPANDO_KEY, euCountry);
		jspRenderer.renderJSP(servletContext, httpServletRequest, httpServletResponse, &quot;/pydyniak/country_custom_settings_view.jsp&quot;);
	}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This method renders our custom JSP. Please note that I&apos;ve used expando bridge framework to store the
custom boolean value. The method &quot;setupEuCountryExpando&quot; source can be seen below:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;private void setupEuCountryExpando(long companyId) {
		try {
			ExpandoBridge expandoBridge = expandoBridgeFactory.getExpandoBridge(companyId, Country.class.getName());
			if (!expandoBridge.hasAttribute(CountryCustomSettingsConstants.EU_COUNTRY_EXPANDO_KEY)) {
				expandoBridge.addAttribute(CountryCustomSettingsConstants.EU_COUNTRY_EXPANDO_KEY, ExpandoColumnConstants.BOOLEAN,
						false, false);
			}
		}catch (PortalException ex) {
			LOG.error(&quot;Error while setting up eu country expando&quot;, ex);
		}
	}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and it sets up the expando for Country. If it already exists then nothing changes.&lt;/p&gt;
&lt;h4 id=&quot;custom-jsp&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#custom-jsp&quot; aria-label=&quot;custom jsp permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Custom JSP&lt;/h4&gt;
&lt;p&gt;The render method from previous point mentions the JSP which is just a simple JSP (some parts skipped, see Github for full code):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x3C;%@include file=&quot;init.jsp&quot; %&gt;

&amp;#x3C;%
    long countryId = (long) request.getAttribute(CountryCustomSettingsConstants.PARAM_COUNTRY_ID);
    boolean euCountry = (boolean) request.getAttribute(CountryCustomSettingsConstants.EU_COUNTRY_EXPANDO_KEY);
%&gt;
&amp;#x3C;portlet:actionURL name=&quot;/commerce_country/edit_country_custom_settings&quot;
                   var=&quot;editCountryCustomSettingsActionURL&quot;/&gt;

&amp;#x3C;aui:form action=&quot;&amp;#x3C;%= editCountryCustomSettingsActionURL %&gt;&quot; cssClass=&quot;container-fluid container-fluid-max-xl&quot;
          method=&quot;post&quot; name=&quot;fm&quot;&gt;
    &amp;#x3C;div class=&quot;lfr-form-content&quot;&gt;
        &amp;#x3C;div class=&quot;sheet&quot;&gt;
            &amp;#x3C;div class=&quot;panel-group panel-group-flush&quot;&gt;
                &amp;#x3C;aui:fieldset&gt;
                    &amp;#x3C;aui:input name=&quot;redirect&quot; type=&quot;hidden&quot; value=&quot;&amp;#x3C;%= PortalUtil.getCurrentURL(request) %&gt;&quot;/&gt;
                    &amp;#x3C;aui:input name=&quot;countryId&quot; type=&quot;hidden&quot; value=&quot;&amp;#x3C;%= countryId %&gt;&quot;/&gt;
                    &amp;#x3C;aui:input checked=&apos;&amp;#x3C;%=euCountry %&gt;&apos;
                               inlineLabel=&quot;right&quot; labelCssClass=&quot;simple-toggle-switch&quot; type=&quot;toggle-switch&quot;
                               name=&quot;&amp;#x3C;%=CountryCustomSettingsConstants.EU_COUNTRY_EXPANDO_KEY%&gt;&quot;/&gt;

                    &amp;#x3C;aui:button-row&gt;
                        &amp;#x3C;aui:button cssClass=&quot;btn-lg&quot; type=&quot;submit&quot;/&gt;
                    &amp;#x3C;/aui:button-row&gt;
                &amp;#x3C;/aui:fieldset&gt;
            &amp;#x3C;/div&gt;
        &amp;#x3C;/div&gt;
    &amp;#x3C;/div&gt;
&amp;#x3C;/aui:form&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The form is defined with only one field for user. Then on submit we call custom MVC action command&lt;/p&gt;
&lt;h4 id=&quot;mvc-action-command&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#mvc-action-command&quot; aria-label=&quot;mvc action command permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;MVC Action Command&lt;/h4&gt;
&lt;p&gt;Once the form is submitted we want to call action url &lt;code&gt;/commerce_country/edit_country_custom_settings&lt;/code&gt;.
This MVC Action Command of course does not exist. We need to define one&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Component(immediate = true,
        property = {
                &quot;javax.portlet.name=&quot; + CommercePortletKeys.COMMERCE_COUNTRY,
                &quot;mvc.command.name=/commerce_country/edit_country_custom_settings&quot;
        },
        service = MVCActionCommand.class
)
public class EditCountryCustomSettingsMVCActionCommand extends BaseMVCActionCommand {
    @Reference
    private CountryService countryService;

    @Override
    protected void doProcessAction(
            ActionRequest actionRequest, ActionResponse actionResponse) {
        long countryId = ParamUtil.getLong(actionRequest, CountryCustomSettingsConstants.PARAM_COUNTRY_ID);
        boolean euCountry = ParamUtil.getBoolean(actionRequest, CountryCustomSettingsConstants.EU_COUNTRY_EXPANDO_KEY);
        Country country = countryService.fetchCountry(countryId);
        country.getExpandoBridge().setAttribute(CountryCustomSettingsConstants.EU_COUNTRY_EXPANDO_KEY, euCountry);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We create a class extending &lt;code&gt;BaseMVCActionCommand&lt;/code&gt;, then we define an action in which we set the value from the form
as expando of Country object.&lt;/p&gt;
&lt;h3 id=&quot;results&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#results&quot; aria-label=&quot;results permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Results&lt;/h3&gt;
&lt;p&gt;If we deploy the module and open the country details again we will see new menu entry called &quot;Custom Settings&quot;:
&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: 420px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 15.9468438538206%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAADCAYAAACTWi8uAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAjUlEQVR42o2OzQ7CIBCE+/5vZ4x6UAmgpaUSTUz5KWUZAdOLJzeZ7E6+zex2lDNq5dI3bfWP/2WdeUw47HdgjDUwjhPOFw6tTfPGPCFkDyF6+LAgpQQpFeRtgFIanN8bS0Rtv7sOEUf+xkk6vCwQ44J5tvA+tIsxrrDWNdUwygTnPGyRD+E7F0YlsH74AaOa6Bu/YfcFAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Custom Settings in Navigation&apos; title=&apos;&apos; src=&apos;/static/501778fb7c3abb501f9dfba83aa62deb/d10fb/custom-settings-in-navigation.png&apos; srcset=&apos;/static/501778fb7c3abb501f9dfba83aa62deb/fb933/custom-settings-in-navigation.png 301w,
/static/501778fb7c3abb501f9dfba83aa62deb/d10fb/custom-settings-in-navigation.png 420w&apos; sizes=&apos;(max-width: 420px) 100vw, 420px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Custom Settings in Navigation&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;and the form:
&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: 425px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 49.83388704318937%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAAAsTAAALEwEAmpwYAAABOUlEQVR42q2RvUrDYBSGs3hH7l6Bg5Org4s6eAEO6uLs5qQIDgr1LkTRSXCwCY1NWpNWKIW2X9K0Sb7v8TRpEQuKVV84HM7fe/4sYwyL4Kv0md/KspxmI8RvtNFaE8cx9XqTMHwj10bsIa9BiyBo0x9EUqhp+D4vrovveTiOg23bJElSEua5RkliFMUFYZpm9Hp9BuLLp7aS2CSejlO6yvDs97FFnKai6vV4qnUJOqnUTyecIEkhzczcGobPJ5GJRwY1gkik0ONSlPi1bGRBzsE1rBzCo4+s61Fz61SrDkHY4mcX/siyMDl+Bx7csqvOEkZJzEAphnKX+aeZb6QklAkvb2HnDHbPYeMEru7KoJx3YRSE60ewtAbLe+JYNWwelz3lH78jvLiBrVPYr8C26Mr9Hyacffm/8A6p5wC1kKGg5AAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Custom settings form&apos; title=&apos;&apos; src=&apos;/static/40ee9ad5fd06b6a8fd3b3edb06d25c97/2fbbf/eu-country-form.png&apos; srcset=&apos;/static/40ee9ad5fd06b6a8fd3b3edb06d25c97/fb933/eu-country-form.png 301w,
/static/40ee9ad5fd06b6a8fd3b3edb06d25c97/2fbbf/eu-country-form.png 425w&apos; sizes=&apos;(max-width: 425px) 100vw, 425px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Custom settings form&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt; please note that I didn&apos;t mention all the code, especially the language module which provides the translation.
For example can be found
&lt;a href=&quot;https://github.com/RafalPydyniak/liferay-blog-samples/tree/master/modules&quot; target=&quot;_self&quot; rel=&quot;nofollow&quot;&gt;on GitHub in my Liferay Blog Samples repository&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;summary&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#summary&quot; aria-label=&quot;summary permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Summary&lt;/h2&gt;
&lt;p&gt;I hope you liked it and I encourage you to use this approach whenever you can instead of standard OSGi fragments.
Sadly this approach is not always replacement, and it can only help us if we need to add new fields. It won&apos;t help in replacing existing ones
and cannot be used to change things like order of fields, custom validations etc. For these you will still need the old approach.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Liferay: Why am I getting "Incorrect string value" error]]></title><description><![CDATA[Some time ago a colleague of mine struggled with an error "Incorrect string value: '...' for column 'description' at row 1" and asked me for a help. He tried to look into google and he found out that the error is connected to the database charset or collation settings but he couldn't find out why does it happen to him even though the settings seemed correct.
Let's find out why is that in this short article.]]></description><link>https://pydyniak.com/why-am-i-getting-incorrect-string-value-in-liferay/</link><guid isPermaLink="false">https://pydyniak.com/why-am-i-getting-incorrect-string-value-in-liferay/</guid><pubDate>Tue, 13 Dec 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Some time ago a colleague of mine struggled with an error &quot;Incorrect string value: &apos;\xE2\x86\x92)&amp;#x3C;/...&apos; for column &apos;description&apos; at row 1&quot;
and asked me for a help. He tried to look into google and he found out that the error is connected to the
database charset or collation settings but he couldn&apos;t find out why does it happen to him even though the
settings seemed correct.&lt;/p&gt;
&lt;p&gt;Let&apos;s find out why is that in this short article.&lt;/p&gt;
&lt;h2 id=&quot;database-charset-vs-database-collation&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#database-charset-vs-database-collation&quot; aria-label=&quot;database charset vs database collation permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Database charset vs database collation&lt;/h2&gt;
&lt;p&gt;First we should understand what is the difference between these two to have better understanding of databases.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Charset&lt;/strong&gt; is a set of characters that a database can store and manipulate.
Each character in a charset is represented by a unique code point, which is a numerical value that corresponds to a specific character.
The charset determines which characters can be stored and used in a database.
Different charsets support different sets of characters, and they can be used to support different languages and writing systems.
For example, the ASCII charset includes the basic letters, numbers, and symbols used in English,
while the Unicode charset includes a much wider range of characters from many different languages and writing systems.&lt;/p&gt;
&lt;p&gt;On the other hand, &lt;strong&gt;database collation&lt;/strong&gt; is a set of rules that determine how the data in a database is sorted and compared.
This includes rules for how character strings are ordered and how case, accent marks, and other language-specific features are treated.
Different collation settings can be used to support different languages and regional conventions,
and they can affect the way that queries and other operations are performed on a database.
The difference between collations can be seen on example - letter Ł in polish is after L and before M and some collations
will support that (such as utf8_unicode_ci) while others won&apos;t and will treat these two letters equally or they will
just put the Ł after the Z. This could of course lead to wrong results of your queries.&lt;/p&gt;
&lt;p&gt;As you can see these two topics are closely related, but they are not the same. Of course the way both are set depends on the
particular database.&lt;/p&gt;
&lt;h3 id=&quot;examples-of-charsets-and-collations&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#examples-of-charsets-and-collations&quot; aria-label=&quot;examples of charsets and collations permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Examples of charsets and collations&lt;/h3&gt;
&lt;p&gt;Few examples of charsets and collations could be:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Charset: latin1, collation: latin1_swedish_ci - default charset/collation for MySQL up to version 5.7&lt;/li&gt;
&lt;li&gt;Charset: utf8mb4, collation: utf8mb4_0900_ai_ci - default charset/collation for MySQL 8.0&lt;/li&gt;
&lt;li&gt;Charset: utfmb4, collation: utf8mb4_unicode_ci&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It&apos;s important to note that different databases have different default charset and collation settings,
and these settings may need to be changed depending on the specific requirements of your application.
Consult the documentation for your database to learn more about how to configure the charset and collation settings for your database.&lt;/p&gt;
&lt;h2 id=&quot;different-levels-for-charset-and-collation-settings&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#different-levels-for-charset-and-collation-settings&quot; aria-label=&quot;different levels for charset and collation settings permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Different &quot;levels&quot; for charset and collation settings&lt;/h2&gt;
&lt;p&gt;It&apos;s important to know that charset and collation can be set on different &quot;levels&quot;. For example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Server level&lt;/li&gt;
&lt;li&gt;Database level&lt;/li&gt;
&lt;li&gt;Table level&lt;/li&gt;
&lt;li&gt;Column level&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This of course can slightly differ for each database (MySQL, PostgreSQL, ...) so please check your database documentation for
more details.
Anyway what is important for us at this stage is that even though your database can have a proper settings the specific
column can have wrong settings. How can this happen? Well for example if following situation happened:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Schema was created with wrong charset/collation&lt;/li&gt;
&lt;li&gt;Liferay was started&lt;/li&gt;
&lt;li&gt;Someone fixed the schema settings&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When Liferay starts it creates a lot of tables. By default, it uses the database level charset/collation settings
for all the tables. Changing the database settings won&apos;t fix the wrong settings for specific columns.
And this is a possible case when database seems to have correct settings but Liferay still doesn&apos;t start up properly.&lt;/p&gt;
&lt;h2 id=&quot;solution-to-incorrect-string-value-issue&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#solution-to-incorrect-string-value-issue&quot; aria-label=&quot;solution to incorrect string value issue permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Solution to &quot;Incorrect string value&quot; issue&lt;/h2&gt;
&lt;p&gt;If you read the explanation I bet the solution might already be clear for you - you just have to make sure that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Your database has correct charset and collation settings&lt;/li&gt;
&lt;li&gt;Your existing tables have correct charset and collation settings&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Please note it&apos;s not enough to just fix already created tables as the new ones could still get the default,
wrong settings. That&apos;s why you always it&apos;s better to always change settings on both levels.&lt;/p&gt;
&lt;p&gt;Consult the documentation for your database to learn how to properly configure the charset and collation settings.
For MySQL examples see commands below.&lt;/p&gt;
&lt;h2 id=&quot;useful-mysql-commands-related-with-charsets-and-collations&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#useful-mysql-commands-related-with-charsets-and-collations&quot; aria-label=&quot;useful mysql commands related with charsets and collations permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Useful MySQL Commands related with charsets and collations&lt;/h2&gt;
&lt;p&gt;I will only describe MySQL commands here as an example but you should be easily transfer them to your
database or find a solution online.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Checking DB default settings&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;SELECT * FROM information_schema.SCHEMATA
WHERE schema_name = &apos;liferay&apos;;
&lt;/code&gt;&lt;/pre&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: 1131px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 11.295681063122922%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAACCAYAAABYBvyLAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAbUlEQVR42k2KSRKEIBAE/RIQLM2mAoKG/39P2TCXOXRkRWdu9Rpo40G/X4znReer/cbZOgq7yb005LMiHWXxtyfb+v37zccEIg+tDax1iHmHNrwdIeVj0RLBsJNSInA/mxDi6pVS8D5w5yCEwAcPxUTGwLCz5wAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;&amp;quot;Renaming existing columns&amp;quot;&apos; title=&apos;&apos; src=&apos;/static/286eb04e65dbb6533984242022943749/a0730/database_level_charset_settings.png&apos; srcset=&apos;/static/286eb04e65dbb6533984242022943749/fb933/database_level_charset_settings.png 301w,
/static/286eb04e65dbb6533984242022943749/32056/database_level_charset_settings.png 602w,
/static/286eb04e65dbb6533984242022943749/a0730/database_level_charset_settings.png 1131w&apos; sizes=&apos;(max-width: 1131px) 100vw, 1131px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;&amp;quot;Renaming existing columns&amp;quot;&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Checking specific table settings&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;SELECT T.table_name, CCSA.character_set_name as column_character_set,
       CCSA.COLLATION_NAME as collation_name
       FROM information_schema.`TABLES` T, information_schema.`COLLATION_CHARACTER_SET_APPLICABILITY` CCSA
WHERE CCSA.collation_name = T.table_collation
  AND T.table_schema = &apos;liferay&apos;
AND T.TABLE_NAME=&apos;AccountEntryOrganizationRel&apos;;
&lt;/code&gt;&lt;/pre&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: 800px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 15.282392026578073%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAADCAYAAACTWi8uAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAlklEQVR42m2OSxKDIBBEvY8LBSOg8pcQYqVy/9t0hnGbxaOLV0wPQ6kVtb0RckH7fPFsF3w64ULG4SMOF2F7+jutT8w/70LCEFOmwgvl1aj0RCQCOR8TFWfkUtlFWti5hwMS+8If6W+t89Bmx7DSsW07NKG0wao0pxACUkrsh8WyPPguhMQ8z5imCcZsMDTTWZViN44jfl3KZmjdky5yAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;&amp;quot;Renaming existing columns&amp;quot;&apos; title=&apos;&apos; src=&apos;/static/bc44b3c4949f18e651aef549278b4166/5a190/table_level_charset_settings.png&apos; srcset=&apos;/static/bc44b3c4949f18e651aef549278b4166/fb933/table_level_charset_settings.png 301w,
/static/bc44b3c4949f18e651aef549278b4166/32056/table_level_charset_settings.png 602w,
/static/bc44b3c4949f18e651aef549278b4166/5a190/table_level_charset_settings.png 800w&apos; sizes=&apos;(max-width: 800px) 100vw, 800px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;&amp;quot;Renaming existing columns&amp;quot;&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Changing all the tables settings&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;It&apos;s a little bit more complicated - there is no out of the box command to change settings for all the tables at once.
It can be done with a little workaround though.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mysql&quot;&gt;SELECT
    CONCAT(&apos;ALTER TABLE &apos;, T.TABLE_SCHEMA, &apos;.&apos;, T.TABLE_NAME, &apos; CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;&apos;) AS SQL_UPDATE,
    T.table_name, CCSA.character_set_name as column_character_set,
    CCSA.COLLATION_NAME as collation_name
FROM information_schema.`TABLES` T, information_schema.`COLLATION_CHARACTER_SET_APPLICABILITY` CCSA
WHERE CCSA.collation_name = T.table_collation
  AND T.table_schema = &apos;liferay&apos;
  AND CCSA.CHARACTER_SET_NAME=&apos;latin1&apos;;
&lt;/code&gt;&lt;/pre&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: 1158px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 16.943521594684384%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAADCAYAAACTWi8uAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAnElEQVR42j2OSw7DIAwFc4qqTQhSE34x2Ai4/9leDYsuLNuYGb1tjI7WO1gYUgUis3jtVfecCX0MMBcEf2s5fa/gUhCjR0oBrTX03kD0YJtw1iMRoWhnVhnL6iIVMYR1o5zhVGhPo5IEd33xeb9gzK58XqxzF7aZoiicFShLIks0P4gmSSmueUrtucMcbzyaxLsbh87WHv8Q3t/4AXsgWPZYUa1rAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;&amp;quot;Renaming existing columns&amp;quot;&apos; title=&apos;Renaming existing columns&apos; src=&apos;/static/3e3af8b27a6979b227071285adf4fa27/1132d/renaming_existing_columns.png&apos; srcset=&apos;/static/3e3af8b27a6979b227071285adf4fa27/fb933/renaming_existing_columns.png 301w,
/static/3e3af8b27a6979b227071285adf4fa27/32056/renaming_existing_columns.png 602w,
/static/3e3af8b27a6979b227071285adf4fa27/1132d/renaming_existing_columns.png 1158w&apos; sizes=&apos;(max-width: 1158px) 100vw, 1158px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Renaming existing columns&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;And then in first column you have a list of ALTER TABLE which you have to run.
Just copy &amp;#x26; paste the list and run it. Each table collation will be changed one by one.&lt;/p&gt;
&lt;h2 id=&quot;final-words&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#final-words&quot; aria-label=&quot;final words permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Final words&lt;/h2&gt;
&lt;p&gt;As you can see this error is not too complicated. It&apos;s good to have the basic understanding of charsets and collations
though that&apos;s why I tried to explain it a little bit as well.&lt;/p&gt;
&lt;p&gt;I hope you liked it and found it useful.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Is Liferay Commerce production ready? Personal Experience]]></title><description><![CDATA[Some time ago Liferay Commerce has been introduced. First it has been shipped as a separate product. Then it got included in Liferay and no separate installation was required. For new versions of Liferay you do not even have to turn it on as it is turned on by default (you can still turn it off if you want).
If you use Liferay then you have probably already came across this new functionality but have you ever wondered - is this really useful and production ready?
I will try to answer this question in this post based on my personal experience.]]></description><link>https://pydyniak.com/is-liferay-commerce-ready-personal-experience/</link><guid isPermaLink="false">https://pydyniak.com/is-liferay-commerce-ready-personal-experience/</guid><pubDate>Mon, 03 Oct 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Some time ago Liferay Commerce has been introduced. First it has been shipped as a separate product.
Then it got included in Liferay and no separate installation was required.
For new versions of Liferay you do not even have to turn it on as it is turned on by default (you can still turn it off if you want).&lt;/p&gt;
&lt;p&gt;If you use Liferay then you have probably already came across this new functionality but have you ever wondered - is this
really useful and production ready?&lt;/p&gt;
&lt;p&gt;I will try to answer this question in this post based on my personal experience.&lt;/p&gt;
&lt;h2 id=&quot;few-words-about-my-experience-with-liferay-commerce-and-why-did-i-even-create-this-post&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#few-words-about-my-experience-with-liferay-commerce-and-why-did-i-even-create-this-post&quot; aria-label=&quot;few words about my experience with liferay commerce and why did i even create this post permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Few words about my experience with Liferay Commerce and why did I even create this post?&lt;/h2&gt;
&lt;p&gt;You might wonder what kind of experience do I have with Liferay Commerce and why did I create this post in the first place.
Let&apos;s start with the experience - I have worked with Liferay Commerce for about a year.
During this year I&apos;ve used probably every aspect of it - from the basics like channels, catalogs
to things more detailed like price lists, relations of articles, detailed settings of shipping options and more.
Not only I have used out of the box functionalities, but I also extended/overrode different parts of the system and
looked into tons of different options to see how we can use what is already there for our particular needs
(or to be more precise - for our customers needs).
Especially with multiple interfaces with ERP systems we needed to support.&lt;/p&gt;
&lt;p&gt;With all of that experience I feel like now I know it quite well, or at least fairly well given it is still quite a new product.&lt;/p&gt;
&lt;p&gt;Because this is a new product and there are not too many people who could share their success (or failure) stories
I thought I will at least share some of my thoughts.
This way some of you who already use Liferay might be in a better position
to decide if you should also use it for your company&apos;s shop site.&lt;/p&gt;
&lt;h2 id=&quot;pros-and-cons-of-liferay-commerce&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#pros-and-cons-of-liferay-commerce&quot; aria-label=&quot;pros and cons of liferay commerce permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Pros and cons of Liferay Commerce&lt;/h2&gt;
&lt;p&gt;In my opinion Liferay based on their previous experience (including mistakes) has learned a lot,
and they tend to do new functionalities much better, stable and easier to extend.
This also includes the commerce connected modules - in general they work surprisingly good but of course
there are also some downsides.&lt;/p&gt;
&lt;p&gt;I will go through its few pros and cons, so you have better feeling what to expect from it.&lt;/p&gt;
&lt;h3 id=&quot;pros-variety-of-options&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#pros-variety-of-options&quot; aria-label=&quot;pros variety of options permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Pros: Variety of options&lt;/h3&gt;
&lt;p&gt;To be honest I haven&apos;t worked with different shop engines like PrestaShop, WooCommerce, Magento etc.
This means I can&apos;t fully compare the functionality between them and Liferay Commerce. I looked briefly on documentation of some of them
but of course this doesn&apos;t make me an expert.&lt;/p&gt;
&lt;p&gt;What I can tell you though is that the shopping configuration possibilities in Liferay Commerce are really
impressive. Moreover, you can use the possibility to have different settings per instance / per site and,
in commerce context, per channel. This means you can host one site meant only for B2C, another one only for B2B
and third one could of course be B2X. You can have different shipping/payment/tax options for each of them.
You can have different price lists applied for different customer accounts or accounts groups.
You can have multiple catalogs which could be shared on different channels. You can have different flows,
price types (net vs gross), different relations between products, payment/shipping methods and restrictions, different
notification (emails) templates and many more.&lt;/p&gt;
&lt;p&gt;I won&apos;t of course describe everything as it&apos;s not the point here. What I would like to say though is
that I personally used many of these options and indeed the out-of-the-box functionalities seems to be
production ready. Of course, you can still have specific needs. This is a standard thing for big projects.
The good thing is that extensions options are huge and quite simple to do.&lt;/p&gt;
&lt;h3 id=&quot;pros-documentation&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#pros-documentation&quot; aria-label=&quot;pros documentation permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Pros: Documentation&lt;/h3&gt;
&lt;p&gt;Many developers I&apos;ve worked with have the feeling that documentation has never been a strong side of Liferay.
Luckily, at least in my opinion, it has changed quite significantly recently.
With new, revamped documentation I feel it gets much better. Regarding Liferay Commerce the documentation describes
not only basic stuff but also shows examples of how to implement things which are less popular like
&lt;a href=&quot;https://learn.liferay.com/commerce/latest/en/developer-guide/sales/implementing-a-new-tax-engine.html&quot; target=&quot;_self&quot; rel=&quot;nofollow&quot;&gt;custom tax engine&lt;/a&gt;
, &lt;a href=&quot;https://learn.liferay.com/commerce/latest/en/developer-guide/sales/implementing-a-new-payment-method.html&quot; target=&quot;_self&quot; rel=&quot;nofollow&quot;&gt;new payment method&lt;/a&gt;
and more.&lt;/p&gt;
&lt;h3 id=&quot;pros-extending-options&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#pros-extending-options&quot; aria-label=&quot;pros extending options permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Pros: Extending options&lt;/h3&gt;
&lt;p&gt;Like I mentioned before - the documentation describes quite a lot of options of extending Liferay Commerce.
If you have worked with portal before and if you have extended it you probably already know that
there are variety of options for overrides/extensions: fragments, portlet filters, custom implementations of services
leveraging OSGI etc.&lt;/p&gt;
&lt;p&gt;All of these things still exist, and you can use your knowledge with Commerce modules as well.
Moreover, the design of newly developed modules is, in most cases, created in a way that
creating the overrides are even simpler than before. For example many tables use the Clay datasets which
makes it easier to change tiny things - you don&apos;t have to touch the JSP view but instead you can just override
Java components. It&apos;s quite convenient in many cases.&lt;/p&gt;
&lt;p&gt;Having said all of that, be prepared that you will still have to take a look on the Liferay sources
to find extensions points.&lt;/p&gt;
&lt;p&gt;Nevertheless, I would say that possibilities of extensions
and ease of doing them is one of the biggest advantages of Liferay. Both for the standard CMS and the Commerce modules.&lt;/p&gt;
&lt;h3 id=&quot;pros-liferay-huge-ecosystem&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#pros-liferay-huge-ecosystem&quot; aria-label=&quot;pros liferay huge ecosystem permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Pros: Liferay huge ecosystem&lt;/h3&gt;
&lt;p&gt;If we talk about the biggest advantages of Liferay another thing that comes to my mind is huge ecosystem of Liferay itself.
Implementing a shop is one thing but there is a lot Liferay can offer beside that.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You need regular content pages? No problem. Just use standard CMS functionalities&lt;/li&gt;
&lt;li&gt;You need advanced workflow for creating content? You&apos;ve got it.&lt;/li&gt;
&lt;li&gt;Advanced users/organizations/permissions options? Of course!&lt;/li&gt;
&lt;li&gt;Multitenancy?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href=&quot;https://pydyniak.com/achieving-multi-tenancy-using-liferay-virtual-instances/&quot; target=&quot;_self&quot; rel=&quot;nofollow&quot;&gt;Well I even wrote an article about that&lt;/a&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Integration with LDAP for synchronizing the users? Or perhaps SSO made with Kerberos or SAML? Sure!&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Basically everything &lt;a href=&quot;https://pydyniak.com/why-should-you-use-cms-such-as-liferay-for-your-web-application/&quot; target=&quot;_self&quot; rel=&quot;nofollow&quot;&gt;I wrote about advantages of Liferay&lt;/a&gt;
and many other things I haven&apos;t (because there are so many)
applies to Commerce as well as, after all, it&apos;s the same product.&lt;/p&gt;
&lt;p&gt;This is huge advantage. Especially if you&apos;re providing portal (/shop) to multiple customers/suppliers and everyone has
a slightly different requirements.&lt;/p&gt;
&lt;h3 id=&quot;cons-some-issues--poor-design-still-happens&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#cons-some-issues--poor-design-still-happens&quot; aria-label=&quot;cons some issues  poor design still happens permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Cons: Some issues / poor design still happens&lt;/h3&gt;
&lt;p&gt;While they fix issues quite regularly and that&apos;s a definitely a pro, there are still some things
which were not designed too well. Example? Here is one but it requires a little introduction.&lt;/p&gt;
&lt;p&gt;Generally in Liferay there are some built-in shipment methods. For now these are FedEx,
Flat Rate and Variable Rate.
&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: 28.23920265780731%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAGCAYAAADDl76dAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA3ElEQVR42m1Q2ZKDIBD0/38z+yZREIYbe3tIdJOqhWqdm+leHo8frKtBznkixnQjpf+Rc/mzU75t7VlKKei9z4RzB87zxHXU/gKviMCYJ7RPT6DfWtNi/juWyk+pDd4HrGaDtW7CB0FlPJf6gQZ3eBysTdwyMy/bjhToMxdCxHJtpXQP72eDtXYODRw6xvhCjJHx8PJ1K+cwSLf18R7IAY2UPYvMc8e2O+pBLd8otd6opGa5gPa8Yg27d5AUKUHlQoJFKKSnIRKn2EpNtbigtD+heW2+/etBxlIq+AVuINN7tT/PxAAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Out of the box shipment methods&apos; title=&apos;&apos; src=&apos;/static/2770e430d4ae3528fb638f6e33a18fbf/35252/shipment-methods.png&apos; srcset=&apos;/static/2770e430d4ae3528fb638f6e33a18fbf/fb933/shipment-methods.png 301w,
/static/2770e430d4ae3528fb638f6e33a18fbf/32056/shipment-methods.png 602w,
/static/2770e430d4ae3528fb638f6e33a18fbf/35252/shipment-methods.png 1204w,
/static/2770e430d4ae3528fb638f6e33a18fbf/4fa52/shipment-methods.png 1241w&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; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Out of the box shipment methods&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;The variable rate and flat rate can contain some additional options. Meaning the method is variable rate
but the particular option can be anything you want - for example you can have: &quot;Self pickup&quot;,
&quot;Carrier&quot;, &quot;Carrier express&quot;. For each option you can set up costs for this option based on the
weight of the order, destination and warehouse. This is really great out of the box functionality.
The issue? Well in the early versions of 7.4 the shipping option picked for order hasn&apos;t had
any key or ID reference. Only the shipment option name was saved in the database, and it was saved
for language in which the checkout has been made...
Problem? Well name of the option can change.
It&apos;s a totally configurable thing, so you never should depend on it.&lt;/p&gt;
&lt;p&gt;This thing &lt;a href=&quot;https://issues.liferay.com/browse/COMMERCE-8465&quot; target=&quot;_self&quot; rel=&quot;nofollow&quot;&gt;has been fixed&lt;/a&gt; but if you are like me
and have a huge project which you don&apos;t upgrade every month or two, because of costs and time restrictions,
then you have to stick with that.&lt;/p&gt;
&lt;p&gt;Of course, it can happen in any product, but I believe it should not happen in such core functionality
especially that it is not some issue introduced by a mistake but has been in Liferay Commerce for a long time.&lt;/p&gt;
&lt;h3 id=&quot;cons-responsiveness&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#cons-responsiveness&quot; aria-label=&quot;cons responsiveness permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Cons: Responsiveness..&lt;/h3&gt;
&lt;p&gt;This might be surprising but Liferay still is not fully mobile friendly.
Of course, I think no one would expect that they make the whole administration panel responsive
but at least the things meant for end-user should be responsive.
Sadly they are not. Even the Liferay Commerce which is kind of new product has some issues.&lt;/p&gt;
&lt;p&gt;Examples? Here are some:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Products list in checkout summary step&lt;/li&gt;
&lt;li&gt;Accounts management - list of accounts&lt;/li&gt;
&lt;li&gt;Placed orders list&lt;/li&gt;
&lt;/ul&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: 380px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 119.26910299003322%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAYCAYAAAD6S912AAAACXBIWXMAAAsTAAALEwEAmpwYAAACIElEQVR42rVVi3LaMBDk/3+OJCRlMm2gYIwpfj8kv7nuCquxA00mtPHMjiVZWu3tneRZWVbyPzH7V4Kqqg0mhBzo+/4mNE1jYElnWldS14189jmdzu84TiUIIpC2onX5qrDrOmmBzqAftcforwDr2u7Sw7Zth4+tUWwJGA77VMD3W9jxP4SUyQGttURxIkppBiRJmkqIfoMN6FXdXNpSKCV772BCnhByFz8I5QhwkM9268h640jXn7CJkiCMR/6dDQyDQO4enuT5+8r4N8kyJ9mJdtG4f+2hPUqVkudKyjEhwbCrGjVFbwZfbFuXtWR5IUWhRdEifgea9pwQRjhJih4mMbN8l6wpQHMj9D34dL94ksfHpWwd1/hp59h5b7Jcw6MIvu2MV1Q2Jjz6vnxbPsOrF5nfLSQv1LmQrxFSXVU1IHNlPr+XjbMfwhysQDsMQ0MWp7mZyxDtplcVkpTh9kgCfakG/ziZhPu9Jw+Lpfx4WQFrWa23xtNxJBNCGp1mOWovA3LTTtHOmD0siJMU9XYUZ+fKBpHs3IPkqNeS9fs3hUmSSTqA7QRnNMuKIcTX48hCb4bEkcji4ujZa2gCjB8Ovrg7T7zDEapzs7kt4k/dh3YRFft+hJMUoQ7VbRcs1cU4xz83rjjI+vGXj/MagziE4gBnWN+mkEeKCcoAquPFod4J98NfgA2RanOUCTf5KOx3Ca2PVt147Mt+Ul9O+Btyj0el+elQMQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;This is how placed orders look like on Iphone 12 Pro&apos; title=&apos;&apos; src=&apos;/static/79e6b9d56781e06ca1edb6f7c5241604/3f520/placed-orders-responsive.png&apos; srcset=&apos;/static/79e6b9d56781e06ca1edb6f7c5241604/fb933/placed-orders-responsive.png 301w,
/static/79e6b9d56781e06ca1edb6f7c5241604/3f520/placed-orders-responsive.png 380w&apos; sizes=&apos;(max-width: 380px) 100vw, 380px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;This is how placed orders look like on Iphone 12 Pro&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;Basically Liferay still has an issue with proper, responsive display of lists.
What we get is simple horizontal scroll for the list.
That&apos;s a shame in my opinion as there are better solutions.
You might be able to make it look better with only styling in theme but some solutions might also require changing
the HTML structure which would require using JSP fragment, which is
not exactly recommended as it makes the further Liferay upgrades harder.&lt;/p&gt;
&lt;p&gt;Of course for some projects it might be enough. I&apos;ve spent a lot of time for lists display
changes though.&lt;/p&gt;
&lt;p&gt;Just to make it clear - I even tried the newest Liferay version (7.4.3.44-ga44) to check if maybe it looks better now,
but it&apos;s not.&lt;/p&gt;
&lt;h3 id=&quot;cons-community&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#cons-community&quot; aria-label=&quot;cons community permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Cons: Community&lt;/h3&gt;
&lt;p&gt;Liferay is not the most popular CMS nor shopping engine. There are a bunch of companies
using it and being happy about it but the fact is that its market share is not too high.
Or to be more precise it is quite low.
This also means that the community is rather small.
Often you might not get the help you need, or you will get it after long time. This means
more time spend on debugging of the core code.&lt;/p&gt;
&lt;p&gt;I feel like this is getting a little better though - especially on Liferay&apos;s official Slack which I highly recommend.
From my experience questions asked there gets much more answers than the ones asked on the forum.
Still I can see that many questions, mostly the harder ones, gets unanswered. With more popular systems
there are more people experiencing the same issues and higher chance to get an answer.&lt;/p&gt;
&lt;h3 id=&quot;cons-its-still-liferay---most-likely-not-for-small-business&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#cons-its-still-liferay---most-likely-not-for-small-business&quot; aria-label=&quot;cons its still liferay   most likely not for small business permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Cons: It&apos;s still Liferay - most likely not for small business&lt;/h3&gt;
&lt;p&gt;Like I have already mentioned in one of my previous articles
&lt;a href=&quot;https://pydyniak.com/why-should-you-use-cms-such-as-liferay-for-your-web-application/&quot; target=&quot;_self&quot; rel=&quot;nofollow&quot;&gt;about pros and cons of using Liferay&lt;/a&gt;
I think Liferay in general is not for small business. With Liferay Commerce it&apos;s the same story.&lt;/p&gt;
&lt;p&gt;Why?
Mostly because of small community (see previous point) which makes it hard to find answers for your questions.
Another thing is that you won&apos;t find too many plugins so if you can&apos;t make something using
out of the box functionality and you are not developer yourself then you will be forced to hire
someone to extend the portal for you. Of course that&apos;s the case with any other solution
but the thing with Liferay is that it is not too common which also makes it harder to find
employees or contractors who know it. This means slightly higher rates as well.
Of course, it still can make sense to use Liferay &amp;#x26; Liferay Commerce! It&apos;s just probably not what you want if you&apos;re
just a tiny business with two or three employees and your budget is not too high.&lt;/p&gt;
&lt;h2 id=&quot;summary&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#summary&quot; aria-label=&quot;summary permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Summary&lt;/h2&gt;
&lt;p&gt;In my opinion usage of Liferay Commerce makes sense. In some cases it might be actually
a great solution. Having said that, in small projects I would not decide to use it, unless
you already know Liferay or have Liferay team.
Of course after all it all depends on your specific needs. I just wanted to give you
some overview which I hope can help you to make a decision if you should even take it for consideration.&lt;/p&gt;
&lt;p&gt;If you have some more questions feel free to contact me at &lt;a href=&quot;mailto:rafal@pydyniak.pl&quot; target=&quot;_self&quot; rel=&quot;nofollow&quot;&gt;rafal@pydyniak.pl&lt;/a&gt; or through my LinkedIn.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Using Custom System Checkers for finding unresolved bundles]]></title><description><![CDATA[In this post I would like to explain what System Checkers in Liferay are, how you can implement one and what you can achieve using it. As an example I will use a real life scenario - what if you have a fragment which depends on another OSGi module and this module stops exporting the package you need in your fragment. In such scenario Liferay will sadly not report any error even though your fragment basically stops working. Lets find out how we can use system checkers to find such situations or how you can use them for any other checks you need.]]></description><link>https://pydyniak.com/using-custom-system-checker-for-finding-unresolved-bundles/</link><guid isPermaLink="false">https://pydyniak.com/using-custom-system-checker-for-finding-unresolved-bundles/</guid><pubDate>Fri, 22 Jul 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In this post I would like to explain what System Checkers in Liferay are, how you can implement one and what you can achieve using it.
As an example I will use a real life scenario - what if you have a fragment which depends on another OSGi module
and this module stops exporting the package you need in your fragment. In such scenario Liferay will sadly not report any error
even though your fragment basically stops working. Lets find out how we can use system checkers to find such situations or how you can use them
for any other checks you need.&lt;/p&gt;
&lt;h2 id=&quot;some-introduction&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#some-introduction&quot; aria-label=&quot;some introduction permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Some introduction&lt;/h2&gt;
&lt;p&gt;So the situation I described at the beginning is the real issue I&apos;ve experienced. In a project I work on we have quite a lot of different fragments.
Some of these fragments make some huge changes in the basic views while others do only a tiny changes like adding a field, button or some text.
Sometimes these fragments use constants from another API modules.&lt;/p&gt;
&lt;p&gt;In my blog samples project I created such
&lt;a href=&quot;https://github.com/RafalPydyniak/liferay-blog-samples/tree/master/modules/not-resolved-bundles/not-resolved-bundles-users-admin-web-fragment&quot; target=&quot;_self&quot; rel=&quot;nofollow&quot;&gt;fragment as an example&lt;/a&gt;.
This fragment is meant to add a simple text in the &quot;Password&quot; page of editing user view
(Under Control Panel -&gt; Users and Organizations -&gt; Edit one of the users).&lt;/p&gt;
&lt;p&gt;This fragment uses the API module for a constant text (lets skip translations in this example).
The added code looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x3C;p&gt;
    &amp;#x3C;%=FakeConstants.FAKE_CONSTANT%&gt;
&amp;#x3C;/p&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nothing really fancy here if you are familiar with fragments modules.&lt;/p&gt;
&lt;p&gt;Anyway the issue with this fragment is that the FakeConstants class package is not exported by the API module&apos;s bnd.bnd file.
Because of this the fragment will not start. It will stay in the &lt;strong&gt;Installed&lt;/strong&gt; OSGi state&lt;/p&gt;
&lt;h3 id=&quot;but-hey---developer-would-notice-that-the-fragment-has-not-started&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#but-hey---developer-would-notice-that-the-fragment-has-not-started&quot; aria-label=&quot;but hey   developer would notice that the fragment has not started permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;But hey - developer would notice that the fragment has not started&lt;/h3&gt;
&lt;p&gt;Of course. He would. And even if he pushed that without testing someone else would. For example tester or project manager or even a client. That&apos;s true.&lt;/p&gt;
&lt;p&gt;But what if the module has been created few months ago, everything was working but then someone moved our class to another package because
he thought it&apos;s only used internally in the same module? Or even removed the Export-Package line from bnd.bnd?&lt;/p&gt;
&lt;p&gt;Well in such scenario the edit password page, which has worked for long time, might not be retested and the issue could&apos;ve been overseen.
Of course the page would still work but without changes from our fragment module. It would just use the default Liferay&apos;s view.
This can be fine in some cases but maybe in some cases it&apos;s a legal requirement which your fragments fulfills for example adds some
specific warning text required in your country. Or maybe your fragment adds some extra fields to the form
and another module you use needs these fields otherwise the form won&apos;t work.&lt;/p&gt;
&lt;h3 id=&quot;doesnt-liferay-give-you-some-error-on-startup-if-the-bundle-couldnt-be-resolved&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#doesnt-liferay-give-you-some-error-on-startup-if-the-bundle-couldnt-be-resolved&quot; aria-label=&quot;doesnt liferay give you some error on startup if the bundle couldnt be resolved permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Doesn&apos;t Liferay give you some error on startup if the bundle couldn&apos;t be resolved?&lt;/h3&gt;
&lt;p&gt;Well actually it does but only for normal modules. In order to show this I created another module:
&lt;a href=&quot;https://github.com/RafalPydyniak/liferay-blog-samples/tree/master/modules/not-resolved-bundles/not-resolved-bundles-web&quot; target=&quot;_self&quot; rel=&quot;nofollow&quot;&gt;not-resolved-bundles-web&lt;/a&gt;
which is a basic portlet and it also uses the same, not exported API.
In this case on startup there are errors in the logs:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;2022-07-22 07:32:59.859 ERROR [fileinstall-directory-watcher][DirectoryWatcher:1173] Unable to start bundle: file:/opt/liferay/osgi/modules/com.pydyniak.blog.samples.not.resolved.bundles.web.jar
2022-07-22T07:32:59.860166363Z com.liferay.portal.kernel.log.LogSanitizerException: org.osgi.framework.BundleException: Could not resolve module: com.pydyniak.blog.samples.not.resolved.bundles.web [1503]_  Unresolved requirement: Import-Package: com.pydyniak.blog.samples.not.resolved.bundles.api_ [Sanitized]
2022-07-22T07:32:59.860172750Z 	at org.eclipse.osgi.container.Module.start(Module.java:444) ~[org.eclipse.osgi.jar:?]
2022-07-22T07:32:59.860182947Z 	at org.eclipse.osgi.internal.framework.EquinoxBundle.start(EquinoxBundle.java:428) ~[org.eclipse.osgi.jar:?]
2022-07-22T07:32:59.860184568Z 	at com.liferay.portal.file.install.internal.DirectoryWatcher._startBundle(DirectoryWatcher.java:1156) [bundleFile:?]
2022-07-22T07:32:59.860186144Z 	at com.liferay.portal.file.install.internal.DirectoryWatcher._startBundles(DirectoryWatcher.java:1189) [bundleFile:?]
2022-07-22T07:32:59.860187736Z 	at com.liferay.portal.file.install.internal.DirectoryWatcher._process(DirectoryWatcher.java:1046) [bundleFile:?]
2022-07-22T07:32:59.860189692Z 	at com.liferay.portal.file.install.internal.DirectoryWatcher.run(DirectoryWatcher.java:221) [bundleFile:?]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But the fragment doesn&apos;t give you the same errors. In fact Liferay doesn&apos;t give you any message in such case.&lt;/p&gt;
&lt;p&gt;The issue is that the DirectoryWatcher which is responsible for starting modules works differently for fragments and for other modules.
The fragments modules as you might now cannot be started, they can only be resolved (meaning all dependencies have been met) and then they works.&lt;/p&gt;
&lt;h3 id=&quot;then-how-can-we-find-out-that-the-fragment-has-not-resolved&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#then-how-can-we-find-out-that-the-fragment-has-not-resolved&quot; aria-label=&quot;then how can we find out that the fragment has not resolved permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Then how can we find out that the fragment has not resolved?&lt;/h3&gt;
&lt;p&gt;Of course we still can find out that the fragment has not been resolved by using gogo shell. We can use the
&lt;code&gt;lb&lt;/code&gt; command which lists all the bundle. Then we can find bundles which are in &quot;Installed&quot; state and not resolved or active.
Using this method we can find our two bundles:
&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: 713px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 6.312292358803987%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAABCAYAAADeko4lAAAACXBIWXMAAAsTAAALEwEAmpwYAAAASElEQVR42j3ISQ6AIBAFUe9/Q2joAVGIgMMJvsSFi0oqb4lscF5AQWBpxzgfhGgQzdMUjhie5Dfm9L3ahjVXHO1CqX3W0MeNF3S8SOwSO6MEAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Our two bundles in installed state&apos; title=&apos;&apos; src=&apos;/static/2098435f16aec15690d213851010ceee/01267/installed_bundles.png&apos; srcset=&apos;/static/2098435f16aec15690d213851010ceee/fb933/installed_bundles.png 301w,
/static/2098435f16aec15690d213851010ceee/32056/installed_bundles.png 602w,
/static/2098435f16aec15690d213851010ceee/01267/installed_bundles.png 713w&apos; sizes=&apos;(max-width: 713px) 100vw, 713px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Our two bundles in installed state&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Tip&lt;/strong&gt; In the Gogo Shell we can also use grep to filter the results:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;lb | grep &quot;Installed&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then we can use the &lt;code&gt;diag&lt;/code&gt; command to find out why particular bundle hasn&apos;t started:
&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: 828px; &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,iVBORw0KGgoAAAANSUhEUgAAABQAAAAHCAYAAAAIy204AAAACXBIWXMAAAsTAAALEwEAmpwYAAAA7ElEQVR42qWNTW6DMBCFfcGeo2fpIXqCrnqEbit10x+pBYNNDEEJEIJTG4LBr4OlthKrKl18ek8z780wa89oD0dItYO2E8www579xTBDB4uyxVtUIZE18mpAfQIq7S+CGdMD8PAeGJ3HOOFfML7tcffocf8049M6zM7BjX9knXUT2MOLxtVNg+vbAUXdwxiLThMnCx3oV7r2vxw7C9bUDd5fn1EoCZnl4FwiFQqZKoMm6SaooN23X3RByBzZZot91RIHNK0G67SBonJCwSUcc0HB4qcYJ1koRbEAJ5/SkY9I0PMisGTCnLTc1fgCXAcTV02wttIAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Example of using diag command&apos; title=&apos;&apos; src=&apos;/static/ac04337118ae18ae992aa8b54b704d6b/8efc2/diag_example.png&apos; srcset=&apos;/static/ac04337118ae18ae992aa8b54b704d6b/fb933/diag_example.png 301w,
/static/ac04337118ae18ae992aa8b54b704d6b/32056/diag_example.png 602w,
/static/ac04337118ae18ae992aa8b54b704d6b/8efc2/diag_example.png 828w&apos; sizes=&apos;(max-width: 828px) 100vw, 828px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Example of using diag command&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;The main issue I have with that approach is that you have to manully use &lt;code&gt;lb&lt;/code&gt;.
In fact there is even an OSGi command called &lt;code&gt;system:check&lt;/code&gt; which is supposed to find all the issues in the deployed bundles
but it just doesn&apos;t show you &lt;strong&gt;any&lt;/strong&gt; issues in our scenario:
&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: 10.299003322259138%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAACCAYAAABYBvyLAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAcklEQVR42kWNSQ7DMAwD8/8Xxoskr0UdN3GboL0yti89EBqBILlowzBGYK2D6bLkoTRjVdR/gfMZLBEkYXoxPeHDY96Y/1y2A1ttWHQvG8FRNlhpwhgZzC5BfAJxmH7KpQd3lHqgvtrk9r7wOb84r9/UDdNWkjEdTXi/AAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;No errors using system:check command&apos; title=&apos;&apos; src=&apos;/static/718792de9274efdce4beff9c333a3f3b/35252/system%3Acheck-no-errors.png&apos; srcset=&apos;/static/718792de9274efdce4beff9c333a3f3b/fb933/system%3Acheck-no-errors.png 301w,
/static/718792de9274efdce4beff9c333a3f3b/32056/system%3Acheck-no-errors.png 602w,
/static/718792de9274efdce4beff9c333a3f3b/35252/system%3Acheck-no-errors.png 1204w,
/static/718792de9274efdce4beff9c333a3f3b/8d68c/system%3Acheck-no-errors.png 1234w&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; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;No errors using system:check command&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;And this is what we&apos;re going to change&lt;/p&gt;
&lt;h2 id=&quot;system-checkers&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#system-checkers&quot; aria-label=&quot;system checkers permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;System Checkers&lt;/h2&gt;
&lt;h3 id=&quot;what-system-checkers-are&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#what-system-checkers-are&quot; aria-label=&quot;what system checkers are permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;What system checkers are&lt;/h3&gt;
&lt;p&gt;System checkers are the components which do different checks of your OSGi bundles.
The idea is that you can run &lt;code&gt;system:check&lt;/code&gt; and find out if everything is fine.&lt;/p&gt;
&lt;p&gt;Also the system check is run automatically on every startup if you have a following portal properties entry:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-properties&quot;&gt;module.framework.properties.initial.system.check.enabled=true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It works quite well for normal modules but like shown before you can&apos;t fully rely on the default system checkers
if you&apos;re using fragments.&lt;/p&gt;
&lt;h3 id=&quot;how-we-can-implement-a-custom-checker---example-of-creating-one-which-finds-installed-but-not-resolved-bundles&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#how-we-can-implement-a-custom-checker---example-of-creating-one-which-finds-installed-but-not-resolved-bundles&quot; aria-label=&quot;how we can implement a custom checker   example of creating one which finds installed but not resolved bundles permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;How we can implement a custom checker - example of creating one which finds installed (but not resolved) bundles&lt;/h3&gt;
&lt;p&gt;So we know that the default system checkers are not reliable in all situations but we can fix that with custom system checker which we
will create on our own. The idea is really simple. We have to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Create a class implementing SystemChecker interface&lt;/li&gt;
&lt;li&gt;Register our class as a component&lt;/li&gt;
&lt;li&gt;Implement all methods from the interface&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So lets start with an empty class:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class UnresolvedBundlesSystemChecker implements SystemChecker {

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then add a &lt;code&gt;Component&lt;/code&gt; annotation:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Component(
    immediate = true,
    property = {},
    service = SystemChecker.class
)
public class UnresolvedBundlesSystemChecker implements SystemChecker {

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This interface has three methods we have to implement&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public interface SystemChecker {

    public String check();

    public String getName();

    public String getOSGiCommand();

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Lets start with the second and third ones. The &lt;code&gt;getName&lt;/code&gt; one is supposed to just return name of the checker:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Override
public String getName() {
    return &quot;Unresolved Requirement System Checker&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;getOSGiCommand&lt;/code&gt; should return a OSGi command which can be run to execute this system check on its own.
This command name is later logged in the &lt;code&gt;system:check&lt;/code&gt; result.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Override
public String getOSGiCommand() {
    return &quot;pydyniak:unresolved&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can name the command as you want. I will show you the implementation of the command component later.&lt;/p&gt;
&lt;p&gt;Now lets go to the check method. This is the method that is being called when you run the &lt;code&gt;system:check&lt;/code&gt;
and it&apos;s the one where you should implement all your logic. In the end you have to return a String which will be displayed by
&lt;code&gt;system:check&lt;/code&gt; but of course you can add some extra logic there for example use &lt;code&gt;Log&lt;/code&gt; to also log the issues to the log files.&lt;/p&gt;
&lt;p&gt;How the logic should look like in our example? Well lets start with the fact that you can get information about bundles in OSGi
by using BundleContext which you can obtain in the activate method of your component. So lets get the BundleContext and save it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;private BundleContext bundleContext;

@Activate
protected void activate(BundleContext bundleContext) {
    this.bundleContext = bundleContext;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once we have that we can use that in our &lt;code&gt;check&lt;/code&gt; method. I will first create an Util class which will handle our logic:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class UnresolvedBundlesUtil {
    private UnresolvedBundlesUtil(){}

    public static String listUnresolvedBundles(BundleContext bundleContext) {
        return Arrays.stream(bundleContext.getBundles())
                .filter(UnresolvedBundlesUtil::isInInstalledState)
                .map(UnresolvedBundlesUtil::getMessage)
                .collect(Collectors.joining());
    }

    /**
     * We only look for installed bundles. This is the states in which bundles might have unresolved requirements
     * @param bundle
     * @return
     */
    private static boolean isInInstalledState(Bundle bundle) {
        return Bundle.INSTALLED == bundle.getState();
    }

    private static String getMessage(Bundle bundle) {
        StringBuilder message = new StringBuilder();
        message.append(&quot;\n\tBundle {id: &quot;);
        message.append(bundle.getBundleId());
        message.append(&quot;, symbolicName: &quot;);
        message.append(bundle.getSymbolicName());
        message.append(&quot;} is unresolved&quot;);
        return message.toString();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So what are we doing here - the listUnresolvedBundles method take &lt;code&gt;BundleContext&lt;/code&gt; object as a parameter. Then
it uses it to obtain all the bundles in your OSGi container. In our example we want to find bundles that are not resolved.
In OSGi world the bundle which couldn&apos;t be resolved is a bundle for which not all dependencies have been met.
In such scenario it stays in &lt;code&gt;Installed&lt;/code&gt;
state. When new bundles are deployed our installed bundles are checked again in order to verify if the requirements have been met or not.
If yes then the installed bundle gets resolved as well.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt; if you&apos;re not familiar with OSGi states you can read some more about them for example in
&lt;a href=&quot;http://docs.osgi.org/javadoc/r4v43/core/org/osgi/framework/Bundle.html&quot; target=&quot;_self&quot; rel=&quot;nofollow&quot;&gt;JavaDocs of Bundle&lt;/a&gt; or in
&lt;a href=&quot;https://learn.liferay.com/dxp/latest/en/liferay-internals/architecture/module-lifecycle.html&quot; target=&quot;_self&quot; rel=&quot;nofollow&quot;&gt;Liferay docs&lt;/a&gt; (they have some nice pictures)
or in many different places across the Internet.&lt;/p&gt;
&lt;p&gt;Checking the bundle state is quite easy and can be seen in &lt;code&gt;isInInstalledState&lt;/code&gt; method:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;private static boolean isInInstalledState(Bundle bundle) {
    return Bundle.INSTALLED == bundle.getState();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then once we find bundles that are installed we can create some basic message for each of them:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;private static String getMessage(Bundle bundle) {
    StringBuilder message = new StringBuilder();
    message.append(&quot;\n\tBundle {id: &quot;);
    message.append(bundle.getBundleId());
    message.append(&quot;, symbolicName: &quot;);
    message.append(bundle.getSymbolicName());
    message.append(&quot;} is unresolved&quot;);
    return message.toString();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Both methods mentioned above are used in the &lt;code&gt;listUnresolvedBundles&lt;/code&gt; which uses Lambda to do all the work:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static String listUnresolvedBundles(BundleContext bundleContext) {
    return Arrays.stream(bundleContext.getBundles())
        .filter(UnresolvedBundlesUtil::isInInstalledState)
        .map(UnresolvedBundlesUtil::getMessage)
        .collect(Collectors.joining());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Going back to our &lt;code&gt;check&lt;/code&gt; method in &lt;code&gt;UnresolvedBundlesSystemChecker&lt;/code&gt; class - we simply call our &lt;code&gt;listUnresolvedBundles&lt;/code&gt; method
and return the result:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Override
public String check() {
    return UnresolvedBundlesUtil.listUnresolvedBundles(bundleContext);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;creating-a-osgi-command&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#creating-a-osgi-command&quot; aria-label=&quot;creating a osgi command permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Creating a OSGi command&lt;/h3&gt;
&lt;p&gt;In the beginning we defined custom OSGi command called &lt;code&gt;pydyniak:unresolved&lt;/code&gt; which can be executed to run the system checker on its own.
Sadly this won&apos;t call our &lt;code&gt;check&lt;/code&gt; method by default. Luckily we can easily create the OSGi command on our own.
To do that we have to create a simple component which again calls our &lt;code&gt;UnresolvedBundlesUtil&lt;/code&gt; class and then simply
uses &lt;code&gt;System.out.println&lt;/code&gt; to log the result in the gogo shell:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Component(
    immediate = true,
    property = {&quot;osgi.command.function=unresolved&quot;, &quot;osgi.command.scope=pydyniak&quot;},
    service = UnresolvedBundlesOSGICommand.class
)
public class UnresolvedBundlesOSGICommand {
    private BundleContext bundleContext;

    @Activate
    protected void activate(BundleContext bundleContext) {
        this.bundleContext = bundleContext;
    }


    public void unresolved() {
        System.out.println(UnresolvedBundlesUtil.listUnresolvedBundles(bundleContext));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you have created OSGi commands before then it should be simple. If not then it&apos;s enough to understand that the
&lt;code&gt;osgi.command.function&lt;/code&gt; and &lt;code&gt;osgi.command.scope&lt;/code&gt; define how our command can be run and then we just need a function
named exactly the same as the &lt;code&gt;osgi.command.function&lt;/code&gt; in our case it&apos;s &lt;code&gt;unresolved&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;testing&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#testing&quot; aria-label=&quot;testing permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Testing&lt;/h2&gt;
&lt;p&gt;Once we have everything we can get to the testing. First we must deploy our modules. Then we should go to the
&lt;code&gt;Control Panel -&gt; Gogo Shell&lt;/code&gt; and execute &lt;code&gt;system:check&lt;/code&gt; command (it takes a while). In the result we can see our custom
system checker being run and its result:
&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: 1080px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 8.637873754152823%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAACCAYAAABYBvyLAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAaUlEQVR42jWMSQ6AMAwD+f8HKUvXpAXKIn5gLAqHUTKx4s6MDmaw6M2MnnOkD8T6BNEVuVRoXskGpctHLs3/vCwVSRZ0k43wQYjCsHSaQ9tZ6rzAukhYzqfm6c1DyghRedO38Dhv1P3CA+rTkZkgigXxAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Our custom system checker results&apos; title=&apos;&apos; src=&apos;/static/8cd19e379a4e76e74689dc22d4f7a663/302a4/Custom_system_check.png&apos; srcset=&apos;/static/8cd19e379a4e76e74689dc22d4f7a663/fb933/Custom_system_check.png 301w,
/static/8cd19e379a4e76e74689dc22d4f7a663/32056/Custom_system_check.png 602w,
/static/8cd19e379a4e76e74689dc22d4f7a663/302a4/Custom_system_check.png 1080w&apos; sizes=&apos;(max-width: 1080px) 100vw, 1080px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Our custom system checker results&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;We can also test our custom OSGi command, which is mentioned in the &lt;code&gt;system:check&lt;/code&gt; result:
&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: 1081px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 25.91362126245847%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAFCAYAAABFA8wzAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAq0lEQVR42p3MQQ6CMBAF0J7WIxnP4tI7uDFGQFpoLKXFAtKi/Y6YqAs10Ule5nd+UmZMg1JW6PrTZBgCQhj/xkZPHzmLShvo2qDtengqBh9+4gOhzVabiLXAY2KMd5f4zO+89GfKfiQhgs3mJRbLFtY6mC9s037ojlDaYcsdksKB9ccaB1kg5yWRSFJO8umdJDmyfYEd3Th1KeU0E3QTU3/LXEioykApDa0NrpfpfzK7e07PAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Testing the pydyniak:unresolved command&apos; title=&apos;&apos; src=&apos;/static/bd802f17b62357b4d184954e0c16e1ba/71ee9/pydyniak%3Aunresolved-results.png&apos; srcset=&apos;/static/bd802f17b62357b4d184954e0c16e1ba/fb933/pydyniak%3Aunresolved-results.png 301w,
/static/bd802f17b62357b4d184954e0c16e1ba/32056/pydyniak%3Aunresolved-results.png 602w,
/static/bd802f17b62357b4d184954e0c16e1ba/71ee9/pydyniak%3Aunresolved-results.png 1081w&apos; sizes=&apos;(max-width: 1081px) 100vw, 1081px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Testing the pydyniak:unresolved command&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;Now we can be sure that our code works just fine.&lt;/p&gt;
&lt;h2 id=&quot;final-thoughts&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#final-thoughts&quot; aria-label=&quot;final thoughts permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Final thoughts&lt;/h2&gt;
&lt;p&gt;With this quite simple module we can be more certain that if we run &lt;code&gt;system:check&lt;/code&gt; we get all information about the errors.&lt;/p&gt;
&lt;p&gt;As far as I&apos;m concerned there are no other possible errors in bundles which could not be found with &lt;code&gt;system:check&lt;/code&gt; (but I might be wrong ;)).&lt;/p&gt;
&lt;p&gt;System checkers of course could be use for different things, even not related to the bundles or components states.
Same for OSGi command - it&apos;s really cool feature in my opinion which I have used in few occasions and I really like them.
They&apos;re quite well documented so if you have any doubts you can read the Liferay documentation.&lt;/p&gt;
&lt;p&gt;On the other hand, the System Checkers are not documented at all, at least not for the moment. You can only find information about how to run
the system check but not how to implement custom ones. That&apos;s why I decided to create this blog post.&lt;/p&gt;
&lt;p&gt;The whole example (API, Service, Custom Portlet and Fragment) can be found on my Github:
&lt;a href=&quot;https://github.com/RafalPydyniak/liferay-blog-samples/tree/master/modules/not-resolved-bundles&quot; target=&quot;_self&quot; rel=&quot;nofollow&quot;&gt;https://github.com/RafalPydyniak/liferay-blog-samples/tree/master/modules/not-resolved-bundles&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Of course feel free to use the code in any way you want.&lt;/p&gt;
&lt;p&gt;Like always I hope you learned something and of course if you have any questions just email me.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Overriding Liferay Core Permissions]]></title><description><![CDATA[In Liferay you can define custom permissions for your entities/portlets. For example you can say that in your application in the employees management panel there are four actions: "List employees", "Add employee", "Edit employee", "Remove employee". Roles can then be associated with these permissions as required. Definining that is quite simple and described in Liferay docs which you should read if you haven't done that before.
But what if you need to override some permissions which were defined in Liferay core modules? Well you can of course do that as well. If you're wondering how - keep reading.]]></description><link>https://pydyniak.com/overriding-liferay-core-permissions/</link><guid isPermaLink="false">https://pydyniak.com/overriding-liferay-core-permissions/</guid><pubDate>Mon, 23 May 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In Liferay you can define custom permissions for your entities/portlets.
For example you can say that in your application in the employees management panel there are four actions:
&quot;List employees&quot;, &quot;Add employee&quot;, &quot;Edit employee&quot;, &quot;Remove employee&quot;.
Roles can then be associated with these permissions as required.
Definining that is quite simple and described in Liferay docs which you should read if you haven&apos;t done that before.&lt;/p&gt;
&lt;p&gt;But what if you need to override some permissions which were defined in Liferay core modules?
Well you can of course do that as well. If you&apos;re wondering how - keep reading.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt; there are some articles on my blog which do not require any specific knowledge. This article &lt;strong&gt;is not one of them&lt;/strong&gt;.
I&apos;m trying to make it as simple as possible so you don&apos;t need to be a Liferay expert but some basic knowledge about Liferay or OSGi might be required to follow.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note 2&lt;/strong&gt; you can find a complete sources for article on
&lt;a href=&quot;https://github.com/RafalPydyniak/liferay-blog-samples&quot; target=&quot;_self&quot; rel=&quot;nofollow&quot;&gt;new github repository I created for Liferay code samples&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;why-would-you-want-to-override-liferay-core-permissions&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#why-would-you-want-to-override-liferay-core-permissions&quot; aria-label=&quot;why would you want to override liferay core permissions permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Why would you want to override Liferay core permissions?&lt;/h2&gt;
&lt;p&gt;There might be a different answers to the question in this section header.
I have encountered different reasons to override Liferay permissions in different projects but in general
I think there are two main reasons to do that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You&apos;re not happy with default permissions given to Guest or Site member roles.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you&apos;re already familiar with defining permissions in Liferay you probably know that you can define what permissions
are given to guest/site member by default. Liferay core permissions also do that.
For example by default GUEST has VIEW permission on every JournalArticle:
&lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 394px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 23.25581395348837%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAFCAYAAABFA8wzAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA9ElEQVR42mWQW27DIBBFvZvE4AQH8zAYgy2nyv5XdDp1oqpSP47ujBjuPDrnLM86kcLIMj/YFkvND+ZgUUrT91eh/4dSimuvJFaoX9V0zk1Eb2lH4tgCe/VsxdOEmBIpF2bRvKwkIZdKXApTcNLcUbIjx4lWA2mNdMaM6OFOapEvMdyKFKWJozpSWTmeL5a2sbaVfa9srVJbkbfAmieW9LOdZZNBgph3w3DjNmisG6WjrFwelHk81XpHnBeMl2nDnZoMs79TopF85HJ9r30icS90ZgznNLuYBW/Yy/uGa7KYm5HCC/q825/Pn5sNWqM/DMNbvwEU7KBCyTwKzwAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;default view permission on journal article for guest&apos; title=&apos;&apos; src=&apos;/static/04918385ef8dd9590277410da8c60c5d/cc097/default-view-permission-on-journal-article-for-guest.png&apos; srcset=&apos;/static/04918385ef8dd9590277410da8c60c5d/fb933/default-view-permission-on-journal-article-for-guest.png 301w,
/static/04918385ef8dd9590277410da8c60c5d/cc097/default-view-permission-on-journal-article-for-guest.png 394w&apos; sizes=&apos;(max-width: 394px) 100vw, 394px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
What if we don&apos;t want that? If we want to control it a little bit more and by default don&apos;t give anyone a VIEW permission on any article?
Well in such scenario you might want to override Liferay core to remove this part.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Second reason might be that you want to add some custom action for some Liferay core portlet or model permissions.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You might for example want to add a custom permission to check if user can checkout an order.&lt;/p&gt;
&lt;h3 id=&quot;things-to-consider-when-overriding-permissions---when-you-should-not-do-it&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#things-to-consider-when-overriding-permissions---when-you-should-not-do-it&quot; aria-label=&quot;things to consider when overriding permissions   when you should not do it permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Things to consider when overriding permissions - when you should not do it?&lt;/h3&gt;
&lt;p&gt;Even though overriding the permissions is quite simple which you will see in a few moments there are things you need to consider:&lt;/p&gt;
&lt;h4 id=&quot;in-most-cases-you-should-not-remove-actions-or-if-you-really-want-to-do-that---be-really-careful-with-that&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#in-most-cases-you-should-not-remove-actions-or-if-you-really-want-to-do-that---be-really-careful-with-that&quot; aria-label=&quot;in most cases you should not remove actions or if you really want to do that   be really careful with that permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;In most cases you should not &lt;strong&gt;remove&lt;/strong&gt; actions or if you really want to do that - be really careful with that&lt;/h4&gt;
&lt;p&gt;Of course, in general you could do that but keep in mind that overriding the default.xml (or any other *.xml with permissions definition) only defines what permissions are available.
All the permissions checking on the other hand is done in the Java/JSP code. What does it mean for you?
Well if you remove some resource action you might end up with errors connected to checking permissions which (after your changes) do not exist.
If you would really want to remove some permission you need to make sure to remove all calls to it as well which means more Liferay core overrides.&lt;/p&gt;
&lt;h4 id=&quot;adding-permission-does-not-mean-checking-for-permissions-in-permissions-xml-files-you-only-define-what-is-available-in-liferay&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#adding-permission-does-not-mean-checking-for-permissions-in-permissions-xml-files-you-only-define-what-is-available-in-liferay&quot; aria-label=&quot;adding permission does not mean checking for permissions in permissions xml files you only define what is available in liferay permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Adding permission does &lt;strong&gt;not&lt;/strong&gt; mean checking for permissions. In permissions *.xml files you only define what is available in Liferay&lt;/h4&gt;
&lt;p&gt;All the checking if user has the permission is done elsewhere (Java/JSP code).
So even if you add some custom permission to some core portlet or entity you still need to add a logic of checking it.
So in general adding custom actions makes sense mostly if you also hook the module in which you add some extra block of code and where you can add usage of your permission.&lt;/p&gt;
&lt;h2 id=&quot;osgi-vs-non-osgi-modules---two-different-approaches-for-overriding-permissions&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#osgi-vs-non-osgi-modules---two-different-approaches-for-overriding-permissions&quot; aria-label=&quot;osgi vs non osgi modules   two different approaches for overriding permissions permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;OSGi vs non OSGi modules - two different approaches for overriding permissions&lt;/h2&gt;
&lt;p&gt;To start the work on permissions override you first need to determine where the permissions are defined.
How to do that? Well you should have a local copy of Liferay portal (available on &lt;a href=&quot;https://github.com/liferay/liferay-portal&quot; target=&quot;_self&quot; rel=&quot;nofollow&quot;&gt;Github&lt;/a&gt;)
for the version your using (checkout to a specific tag for your version).&lt;/p&gt;
&lt;p&gt;The next step is to find a correct module in the code.
Finding code you need in Liferay core is a topic for a separate article - there are many ways to do that.&lt;/p&gt;
&lt;p&gt;For finding the definition of permissions it&apos;s luckily a little easier.
First go to the &quot;Define permissions&quot; section of any role. Then find the block of permissions where you want to change something.
For example in my case I want to change the &quot;Web Content Article&quot; default permissions:
&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: 736px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 79.06976744186048%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAQCAYAAAAWGF8bAAAACXBIWXMAAAsTAAALEwEAmpwYAAAB4ElEQVR42qVTiW7sIAzM/39ntc0BhJBAIJfrMZCmfa30pK40CofXzIztxtqJ3t5e1LYdvd5b6vuB/vJrtm2jEFaKa6Q18jelLwHXdf2KHxMex0n7tpObFzrP88sl9s7NdBzHf7HDI01KmyR7b3uBtY5Zbsw2kWfmbTdQP+iMXpHSo6wHpSWmIjASq2XJO/8xkBktucXL5bbvAgSM7PFordxrM+Z9Wadtv2OBnZU0kAOW0+RuydUj2AHJwARwzMKPujmfXee/vjaxSO66nl6vTmT7ECmsiRa/fspldFWyynvPMaHAhyRWNaAZY5TXIR8AM7DFHc6dc/ydCS028+N2mphxVvQdIjmsKyl+FS2Dw6MAvphxJK0NMwO0eCl+8h4PHo94QKo8sy+DMoIg/Vgqx2t1JxulstrYvOaz8FOVwRDMUDXvfe4/FIaBFyeWB6m2VFcKVM64JhJ3A0WBZ5gUwwlXZlR9BGLcpC8lWUkIT2vy9IgF9v34lIymhcRUEqFiIlllyZA4DNnDQWULEPMEksroYZ7xojFGmOKsjt4sPeeK1NyHs/Slo+vX0eMArSE53UWJMDrUPlTSgx2PIdhijbMaVwFi0od1buEB8By9xQexZJm99CAYepzx+jl2dfQ+AAqY22KpAhrpAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Web Content Article permissions&apos; title=&apos;&apos; src=&apos;/static/3c1b51de1a72cadebba834f5a47c3356/f941f/web_content_article_permissions.png&apos; srcset=&apos;/static/3c1b51de1a72cadebba834f5a47c3356/fb933/web_content_article_permissions.png 301w,
/static/3c1b51de1a72cadebba834f5a47c3356/32056/web_content_article_permissions.png 602w,
/static/3c1b51de1a72cadebba834f5a47c3356/f941f/web_content_article_permissions.png 736w&apos; sizes=&apos;(max-width: 736px) 100vw, 736px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Web Content Article permissions&lt;/figcaption&gt;
  &lt;/figure&gt;
Once I&apos;m there I can go to the browser&apos;s developer tools and see the id of title element:
&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: 373px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 12.29235880398671%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAACCAYAAABYBvyLAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAi0lEQVR42hWNywqCQBhGff83KiTvuTAzypxRWwRRkjhKjgWtTn+rD853c4rrm6y17Jonue7ZqgW3mEi0Jaxm/NOLSDQoZ9LakqkbqRrwjoawHIn1JJ4hrkbSdsH5F1dRh7s3uBJaHwwbKSeXD97Z4pWWQMm4XkiaRQ4eRLUR3uGrnrgx4vfCBvL7lx+syo0rPTK/VgAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Title of permissions&apos; title=&apos;&apos; src=&apos;/static/f876b985148a11cf1b43c2190f796fe9/67a5d/web_content_article_permissions_title.png&apos; srcset=&apos;/static/f876b985148a11cf1b43c2190f796fe9/fb933/web_content_article_permissions_title.png 301w,
/static/f876b985148a11cf1b43c2190f796fe9/67a5d/web_content_article_permissions_title.png 373w&apos; sizes=&apos;(max-width: 373px) 100vw, 373px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Title of permissions&lt;/figcaption&gt;
  &lt;/figure&gt;
We can see it is &quot;resource_com_liferay_journal_model_&lt;strong&gt;JournalArticle&lt;/strong&gt;&quot; and we&apos;re interested in the last part.
The &quot;JournalArticle&quot; is the model name we&apos;re interested in.
In this case you could probably guess the name if you work with Liferay for some time - JournalArticle is a well known name.
In some other cases though this might let you find what you need.&lt;/p&gt;
&lt;p&gt;Next step is to search for this model name in Liferay sources - you will get &lt;strong&gt;hundreds&lt;/strong&gt; of results - this is too much.
Luckily we can filter that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use the file mask to filter only &quot;*.xml&quot; files - the permissions are defined in XML files so we can use that.&lt;/li&gt;
&lt;li&gt;Use the &amp;#x3C;model-name&gt; tag with the name you found earlier and a little regex.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These two filters should be enough to find the xml file you need:
&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: 684px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 10.96345514950166%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAACCAYAAABYBvyLAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAeElEQVR42j2MSw7CMAxEey4E/aRx7CYhICHYEHoXBOm1p04kWDw9jzSezseEmC4Q7+FEQFxhjMbgOAyNw6lXj8qE2QmcD7DaW+JZuwv6aYah+mPRcbqD1wJZN1AuoFdpmfIXNn+a6fkGhWsbqWMc4t+/W3Q83h7YAXG0S+iLX1ZDAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Find permissions definition XML&apos; title=&apos;&apos; src=&apos;/static/bea55696d459e8670ff780d6887e1991/2c288/find-permissions-definition-xml.png&apos; srcset=&apos;/static/bea55696d459e8670ff780d6887e1991/fb933/find-permissions-definition-xml.png 301w,
/static/bea55696d459e8670ff780d6887e1991/32056/find-permissions-definition-xml.png 602w,
/static/bea55696d459e8670ff780d6887e1991/2c288/find-permissions-definition-xml.png 684w&apos; sizes=&apos;(max-width: 684px) 100vw, 684px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Find permissions definition XML&lt;/figcaption&gt;
  &lt;/figure&gt;
In this case only three files were found.
Thirty seconds later you should be able to find out that the file we&apos;re looking for is the &quot;journal.xml&quot; which is located in journal-service module.
Looking at the module structure you can tell that this is an OSGi module.&lt;/p&gt;
&lt;h2 id=&quot;override-definition-of-permissions-in-osgi-module&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#override-definition-of-permissions-in-osgi-module&quot; aria-label=&quot;override definition of permissions in osgi module permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Override definition of permissions in OSGi module&lt;/h2&gt;
&lt;p&gt;Once you know your module is OSGi one you can start work on overriding permissions.
The idea is simple - you need a fragment which will override permissions XML files.
Just like you would normally override JSP file.&lt;/p&gt;
&lt;p&gt;These are the steps you need to take:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Create a fragment module.&lt;/li&gt;
&lt;li&gt;Specify a Fragment-Host property in bnd.bnd.&lt;/li&gt;
&lt;li&gt;Create resources structure and override XML files.&lt;/li&gt;
&lt;li&gt;Deploy your module.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;create-a-fragment-module&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#create-a-fragment-module&quot; aria-label=&quot;create a fragment module permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Create a fragment module&lt;/h3&gt;
&lt;p&gt;You can crate a fragment using &lt;em&gt;blade create&lt;/em&gt; command or your IDE. You can also take a look on a module I already created
in &lt;a href=&quot;https://github.com/RafalPydyniak/liferay-blog-samples&quot; target=&quot;_self&quot; rel=&quot;nofollow&quot;&gt;blog samples&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;specify-a-fragment-host&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#specify-a-fragment-host&quot; aria-label=&quot;specify a fragment host permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Specify a Fragment-Host&lt;/h3&gt;
&lt;p&gt;Once you have your module you want to make sure that your fragment points to a proper bundle version.
You can do that in bnd.bnd file by using &lt;strong&gt;Fragment-Host&lt;/strong&gt; property. In my example I will override
permissions from journal-service module. Checking the Liferay sources I can tell that I need to override the
&lt;strong&gt;com.liferay.journal.service&lt;/strong&gt; (see the bundle symbolic name in bnd.bnd file of overridden module).
You also need to check the bundle version - either in sources or in gogo shell:
&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: 637px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 49.83388704318937%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAAAsTAAALEwEAmpwYAAABMElEQVR42q2STU7DMBCFfRzExbhCb8KKG9BTsGLBjqJSpSWkSfyTHydO44Q2yWOciKpISC0CS589sjzvjTzDbPMBay2EEDCmwl8Xa9o98ryA570j5hJJmkOqDGmqkWQaKsmhC4OajGvbnoW5Ck1lSagkkYJEFbYhRxRxcIpDilMStpcK7uoW/7lYoht4csCKDxBFj6HvcOg6dCf0fX8xbP7Y4Gp2wPVsj5u7Fv2+pj+rUZoJ80vYYp3gdv6M+weFJ8+OnXZ/Wu2akS/hS2F5lsH3lli/viAKfChqjKTOxrFCsOXQ5WRwypRsfxYsqwZ+ILFYbugU46UblTc/xGq1QRAKRCQeRpIQNAECQqZkKMfYTYVK9NGIuU0X1ZGi3H1zd3e5NiNaT7F7c5qjx5ypwk9OQvylk0hz0QAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Checking bundle version in gogo shell&apos; title=&apos;&apos; src=&apos;/static/484951c38f9a43b87aef23e715e53381/13a9a/checking-bundle-version.png&apos; srcset=&apos;/static/484951c38f9a43b87aef23e715e53381/fb933/checking-bundle-version.png 301w,
/static/484951c38f9a43b87aef23e715e53381/32056/checking-bundle-version.png 602w,
/static/484951c38f9a43b87aef23e715e53381/13a9a/checking-bundle-version.png 637w&apos; sizes=&apos;(max-width: 637px) 100vw, 637px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Checking bundle version in gogo shell&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;Once you have bundle symbolic name and its version you can define Fragment-Host in your module bnd.bnd file:
&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: 574px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 4.651162790697675%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAABCAYAAADeko4lAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAQklEQVR42iWIUQrAIAzFdqAxq+2rdeD9j5UJ+wgkuXbcrPHj1tAslEXWQkpmvaeT0Dx+fiTywnsQXWgkNozujWYPHwPhICrT3DCHAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Checking bundle version in gogo shell&apos; title=&apos;&apos; src=&apos;/static/5f69c460a64f04fa22062083b3615c94/86389/Fragment-Host-property.png&apos; srcset=&apos;/static/5f69c460a64f04fa22062083b3615c94/fb933/Fragment-Host-property.png 301w,
/static/5f69c460a64f04fa22062083b3615c94/86389/Fragment-Host-property.png 574w&apos; sizes=&apos;(max-width: 574px) 100vw, 574px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Checking bundle version in gogo shell&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;h3 id=&quot;creating-resources-structure-and-override-xml-files&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#creating-resources-structure-and-override-xml-files&quot; aria-label=&quot;creating resources structure and override xml files permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Creating resources structure and override XML files&lt;/h3&gt;
&lt;p&gt;Once our fragment points to a proper Liferay&apos;s bundle we can start overriding the permissions files.&lt;/p&gt;
&lt;p&gt;We need following files/directories:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;In the &lt;em&gt;resources&lt;/em&gt; directory we need to create a &lt;em&gt;resource-actions&lt;/em&gt; directory where we will put updated versions of&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;XML files we want to override.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;In my case I want to do the changes in &quot;journal.xml&quot; file. To do that I copy the journal.xml from Liferay sources&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;with changed name - for example &quot;journal-ext.xml&quot;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You also need to copy the &quot;default.xml&quot; and also change its name&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;for example to default-ext.xml.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;As we will create some new resource actions we can also create a &quot;content&quot; directory&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;where we will keep our translations.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The last thing you need to do is to create portlet.properties file&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;in resources directory.&lt;/p&gt;
&lt;p&gt;In the end you should end up with following structure:
&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: 291px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 55.32646048109966%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsTAAALEwEAmpwYAAABtklEQVR42o2RS3PaMBSF+UntkAV+YpsYY8t6WTYlkD5JWtpFd/0r3UOm+04T8vdOr5RAnE6ZdnFGV69PR+cOXr7dY/juDunnO5hvPxFc/0L25R7jzZ7WbhF+3OPFmz3suf/RYLjcYbjcInq/w/zrd+SbH5h+2iK/3roxXd/Q/g3OVnaks6sd1TtXH9d6GkRxgDAKEMURsrzA5foKTEgo3cB0LWrB4fkj+IF3lNerH+4+aRCOMxwUJSm8cAzddZCNRlHWqLhEWQvMGMe0ZCgq7uoZe1iP0wn6DAKmOChOCJrmaFuJpq1QywbdYklO5+heXdAjBlwbmne0p6FMhyQ7RxAnR4YDBnHqXppWNV5fbVBJA2nm4HSJCUWjglANAVsIrTEKY/gkjxT0DB2BB0UWTmolR2sMObAAcmVhlGlJX2WCoqiYOxsl2TPYX4EjcvuhnmAhGZi0rjoHFOS2qrlzbcF/gk4Cz/wYa57j0iiU5IYr7Vxqyq+kBkmqC2qOH43/DbRdUhT4XMxgpHCZcedMuhxtIyy8oKzDXiNOAOlAMsHqQkAtjPuuIHcWahtj85SPedY0P/Xl36+OomVrjajcAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Files structure for journal.xml override&apos; title=&apos;&apos; src=&apos;/static/9cfc8d5fcf2e808deec4868d19bd7c34/78805/resources-structure.png&apos; srcset=&apos;/static/9cfc8d5fcf2e808deec4868d19bd7c34/78805/resources-structure.png 291w&apos; sizes=&apos;(max-width: 291px) 100vw, 291px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Files structure for journal.xml override&lt;/figcaption&gt;
  &lt;/figure&gt;
Now lets start with filling all the files.&lt;/p&gt;
&lt;p&gt;First portlet.properties. In this file we want to specify that our default-ext.xml file should be used
instead of the original default.xml:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;include-and-override=portlet-ext.properties
resource.actions.configs=resource-actions/default-ext.xml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the default-ext.xml itself we want to change only one line from the original file content - like you might guess we
want to use our journal-ext.xml instead of the original journal.xml.
So the original default.xml file content is:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x3C;resource-action-mapping&gt;
    &amp;#x3C;resource file=&quot;resource-actions/journal.xml&quot; /&gt;
    &amp;#x3C;resource file=&quot;resource-actions/journal_ddm_composite_models.xml&quot; /&gt;
&amp;#x3C;/resource-action-mapping&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And our override should look like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x3C;resource-action-mapping&gt;
    &amp;#x3C;resource file=&quot;resource-actions/journal-ext.xml&quot; /&gt;
    &amp;#x3C;resource file=&quot;resource-actions/journal_ddm_composite_models.xml&quot; /&gt;
&amp;#x3C;/resource-action-mapping&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt; We don&apos;t need to copy other files like journal_ddm_composite_models.xml. If we keep the original name
then the original files will be used.&lt;/p&gt;
&lt;p&gt;All the changes above are required to finally make our updates in the journal-ext.xml (which is the copy of original journal.xml!).&lt;/p&gt;
&lt;p&gt;You can do whatever you need there. If you&apos;re not familiar with defining permissions you should check the original Liferay documentation.
In my case, for showcase purpose, I will do a few changes.&lt;/p&gt;
&lt;p&gt;First I will add an extra &quot;TEST&quot; action permission for JournalPortlet:
&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: 450px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 69.43521594684385%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAOCAYAAAAvxDzwAAAACXBIWXMAAAsTAAALEwEAmpwYAAABrklEQVR42p1TW27DMAzbdRo7iR/x20nX+x+Ko5xhWIt2G/YhSK1jmpSot5Q8rtXiqA7XZuG9w2VSUOp/8ab0DLOuuO1+AObooLWG5qHm2aiZ55k1Q43fCvNnlrgDnJRc1uPjku1gW6NBY51qR2kduTYURus7XAqo2aDzvPC7sBlM9wxPwAtzyQ7vzaElg715xNJQB0hCraLAY2fOyQ4Cy6zJXJP1E8CJuWbp4zdAsqpkGFMhsDBs6PvBByJBhaVBZQ7B4PIbYGcWyfv1BpcTWhF2Dr2YL4bSR/XQx9eAvCwM9+OdzA604wYbN7rBYidoL3QE+/foiB8A7QlIhrWfMhuBjjG0dbC88J76CbA8MByS9yuZxTGUo22UvSElWms5hyIh1nrJ8GvKA5BDIWBMGb13tFbHUGwMfxtK5svivxrtAA8E2nIloMNRHDJ7Jv8vy/JpeP18U6QQg6/cGGMMJQVsXMFAqc45ONYxeNYGkYPx3o+Q7ZF40kMe8NVGFrfuxwaIlC1m2BCxhXMzclhROIxh5te7LF5aKYVNr/QY5YqkSU2YpjNOBepuxV4BfgBwSrmfbXGhwgAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Extra &amp;quot;TEST&amp;quot; permission for Journal Portlet&apos; title=&apos;&apos; src=&apos;/static/b4a44a021ac7fb091cfdbb8a221f5b10/fc2a6/TEST-resource-action-journal-portlet.png&apos; srcset=&apos;/static/b4a44a021ac7fb091cfdbb8a221f5b10/fb933/TEST-resource-action-journal-portlet.png 301w,
/static/b4a44a021ac7fb091cfdbb8a221f5b10/fc2a6/TEST-resource-action-journal-portlet.png 450w&apos; sizes=&apos;(max-width: 450px) 100vw, 450px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Extra &amp;quot;TEST&amp;quot; permission for Journal Portlet&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;I will also remove the default ADD_DISCUSSION and VIEW permissions for guest:
&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: 639px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 18.6046511627907%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAECAYAAACOXx+WAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAhElEQVR42oWObQ6CIQyDOQ+f22CgGO9/rMpIeGOM0R9Nu6R7NjcH4XHjJQGVjJwiYowIIVx+8j9ZzxlEmDCGgoXBzGitoda6XUSufGYi2j3zUsp2U1rPuJQSci6rrKtcoaroXaG972wQO3SW37/+JnfvhDkYz1kXkGEHPnUg3vufMAO+AEIwe8mldZ1qAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Removed permissions for Journal Portlet&apos; title=&apos;&apos; src=&apos;/static/cdddc14a247ae47732a0ffa2f1e33e1c/738b8/removed-permissions-journal-portlet.png&apos; srcset=&apos;/static/cdddc14a247ae47732a0ffa2f1e33e1c/fb933/removed-permissions-journal-portlet.png 301w,
/static/cdddc14a247ae47732a0ffa2f1e33e1c/32056/removed-permissions-journal-portlet.png 602w,
/static/cdddc14a247ae47732a0ffa2f1e33e1c/738b8/removed-permissions-journal-portlet.png 639w&apos; sizes=&apos;(max-width: 639px) 100vw, 639px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Removed permissions for Journal Portlet&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;Since we added a new action, which has not been yet translated, we should also create a translation for it.
That&apos;s where we need translation files which we created earlier. In my case I only have a single &quot;Language.properties&quot;
but of course you can add different translations. In these files you can add translation like you would normally do
in any other module. Just keep in mind your translation for resource action has to start with &lt;em&gt;action.&lt;/em&gt; prefix:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;action.TEST=Test permission
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;deploy-module&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#deploy-module&quot; aria-label=&quot;deploy module permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Deploy module&lt;/h3&gt;
&lt;p&gt;The deployment in that case is simple and &quot;standard&quot;. Simply build a JAR &amp;#x26; deploy it to the deploy directory
of your Liferay.&lt;/p&gt;
&lt;h3 id=&quot;testing-our-changes&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#testing-our-changes&quot; aria-label=&quot;testing our changes permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Testing our changes&lt;/h3&gt;
&lt;p&gt;That&apos;s it! Once deployed your permissions are ready:
&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: 329px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 139.8671096345515%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAcCAYAAABh2p9gAAAACXBIWXMAAAsTAAALEwEAmpwYAAADSElEQVR42q2VyXLaQBCGeVY/gV/DvvhJXL4k56Qqh1QcvIDMaidewSxCaEEa7X+6W4wkMNhJVahqRtMzGvX010sjy3IopTAajWEtLMzNBSavU4zHUyxovlgs8DqZYjY3EdC+j34N/pvP5/jy9RtujBtcXhloNi/x/ccFBoMhWi2DdG38PL/AZDaXl/I8339gmqb4Xz/+EB2YyQOPWZatx1x0eq7XWLdP9Hppoe/7CMNQlGEYkV9D2fQv1sVxsnlgHMcIAoXh8DcG/Tssbe9Dn62Pk3/TtIor8xV5TJJUJJNrQPR10dffJXGcytjQiqXtwHFX8IOQQseCuViSziWdB2vpwPVWFDbhfqH3eE9pobW04a1WsB0XzcsWOt0+er2hSKt9g+fRFBH5iF9W7OOasI7Xnl/GdKCQTBFGscBgSWgeJ4lsikivR72+Sxgi7ymhcLZEBCWhgxhMSBt0SIkP3wkZHTYMtTzQcRwJGxUEaBtdyRC+/t9Q1utTyqTGZiAXJHUkVF9fW7JFvS5JklWZwn50yBrP88XJq1VAgAIirrDylThdxuAtkApMJPtLC02qLIzdspY4b15TQWih1x/A6PSF+FXrBvePL5IN26Q15cenERrszERTJtmOr8LKwtpdIVO3kMcNykwpW4cRf0Tncg68S7gqJGl1oOu6QjkkygbVxeHdvVi2k/LWXK/PZmZVvvgLWmKJx3S/RVyq0vSNhRzDZS5zHjPdIIjK3NR+VOwfFiLNTUB1uog+fS6eWUdrEgEqquWyZVHYUC4vl7hud4SuQQHeNjrUTyyhyOmlyPr46AjhcAiVQ3Sa8tMzU04Lypogj97KL8Tz5SOyRnO2KDo7Q3R6WllXo8yxWvqQ/cYxltbpsrO5LrKO5tnDA/Lj4w1/a+H3OEFKyrZtS5tkyu22gd7gF3XBDqbTdaeLQuDkBNQK35Auc3k6qyzUUlRfKllRJOUooevI9sND5AcHRUwSzV1xyJFRUmbCvviEDinbQSZuiAha3GpDUcNXVOqVimVfXdiHYbQOG651c9Ok0HGlSFxT6er1b9HtDiiPB3T1Lm7HEwRJWpDeU7GfuGJryvVFm3rJwrKlHtrSU2y49KzeyWUWrlB/AIvAfAVgbNr7AAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Test permission visible in roles panel&apos; title=&apos;&apos; src=&apos;/static/ad6d1a21d52ff4165da723c9e929dc6c/004a5/test-permission.png&apos; srcset=&apos;/static/ad6d1a21d52ff4165da723c9e929dc6c/fb933/test-permission.png 301w,
/static/ad6d1a21d52ff4165da723c9e929dc6c/004a5/test-permission.png 329w&apos; sizes=&apos;(max-width: 329px) 100vw, 329px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Test permission visible in roles panel&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/static/no-view-permissions-for-guest-18d6de2596b7fdc49ed739e7ad4e0c33.gif&quot; alt=&quot;No view permission for newly created web content&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;override-definition-of-permissions-in-non-osgi-module&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#override-definition-of-permissions-in-non-osgi-module&quot; aria-label=&quot;override definition of permissions in non osgi module permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Override definition of permissions in non OSGi module&lt;/h2&gt;
&lt;p&gt;Sometimes you also have to deal with overriding permissions in modules which are not OSGi ones.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt; I think, at least for now, the portal-impl is the only non osgi module where permissions are defined&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note 2&lt;/strong&gt; The steps I describe worked for me for different 7.4 versions (including GA18 which is used in the blog samples)
and I think it will work in future as well but you can never be sure. This isn&apos;t a standard way of
handling things in Liferay. Sadly I haven&apos;t found any better way of doing that.&lt;/p&gt;
&lt;p&gt;This means you can&apos;t simply use fragments to override them.
Luckily we still can make it work.&lt;/p&gt;
&lt;p&gt;The steps are quite similar to the ones we did for OSGi one so for the same things I will not describe them again.
Basically we need to (I&apos;m overriding documentlibrary.xml from portal-impl but the steps are of course the same for
other XML permissions files):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Create a module.&lt;/li&gt;
&lt;li&gt;Create default-ext.xml and documentlibrary-ext.xml (in my example! You might need another one) files - just like we did&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;in OSGi module override.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Add our default-ext.xml to the portal-ext.properties or to the Liferay environment variables for Docker environments.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deploy our module to proper place.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Like you can see it&apos;s almost the same with tiny changes - we don&apos;t need to define portlet.properties
and we don&apos;t need to define a Fragment-Host in our bnd.bnd. This is because we&apos;re not using fragments here.&lt;/p&gt;
&lt;h3 id=&quot;inform-liferay-about-our-custom-default-extxml-file&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#inform-liferay-about-our-custom-default-extxml-file&quot; aria-label=&quot;inform liferay about our custom default extxml file permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Inform Liferay about our custom default-ext.xml file&lt;/h3&gt;
&lt;p&gt;After you created your module and made your changes in default-ext.xml and documentlibrary-ext.xml,
which I won&apos;t describe again but you can take a look on a
&lt;a href=&quot;https://github.com/RafalPydyniak/liferay-blog-samples/tree/master/modules/resource-permissions-override/portal-impl-permissions-override&quot; target=&quot;_self&quot; rel=&quot;nofollow&quot;&gt;sources&lt;/a&gt;
, you need to inform Liferay about our custom default-ext.xml file
You can do that by adding a line to your:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;portal-ext.properties&lt;/li&gt;
&lt;li&gt;Docker environment variables&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;depending on your environment type.&lt;/p&gt;
&lt;p&gt;For the portal-ext.properties file it should look like that:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;resource.actions.configs=META-INF/resource-actions/default.xml,resource-actions/default.xml,resource-actions/default-ext.xml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;While for docker environments the env variable should look like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;LIFERAY_RESOURCE_PERIOD_ACTIONS_PERIOD_CONFIGS=META-INF/resource-actions/default.xml,resource-actions/default.xml,resource-actions/default-ext.xml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What exactly is the value we used? Well this property defines list of resource action configurations
which are read from classpath. So you first have to check up the original value in
&lt;a href=&quot;https://github.com/liferay/liferay-portal/blob/7.4.3.18-ga18/portal-impl/src/portal.properties&quot; target=&quot;_self&quot; rel=&quot;nofollow&quot;&gt;portal.properties file of Liferay&apos;s portal-impl module&lt;/a&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: 558px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 33.222591362126245%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAHCAYAAAAIy204AAAACXBIWXMAAAsTAAALEwEAmpwYAAAA+ElEQVR42pWRWU4DMRBE5/6HCYgboICEFHKAkCgRX9GAZmm3963wODPJDyCw9FTtpUtluwkhIqVcSFdijFedCFWXeaqEEOb1G1NP0/UDmCWIuCAgmDGtDSMVBLQxUFpDawul9Dw3YCkL6rJnbFVrHRrnXC0W9d7X2liLKf1/R2N9hCuNwnhoF0DaQVpfE4yCa2ISAiNdlARXcs7fG9L+Bfp9C3l8LWygThvQcQtv5HwkY+mdTBZ+THhYr7B7XGG/vsNb4fR8j93TA9rzGSQtPntR0hoQa6RfjG5XdgntaNGzRyccOnZoB4OPThQzglSm/qovz5L/YPgF1SwfmicyLHMAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Original resource-actions value&apos; title=&apos;&apos; src=&apos;/static/5dddbf2da44520f4f6894f0d6a4b7fc8/42a8d/original-resource-actions-value.png&apos; srcset=&apos;/static/5dddbf2da44520f4f6894f0d6a4b7fc8/fb933/original-resource-actions-value.png 301w,
/static/5dddbf2da44520f4f6894f0d6a4b7fc8/42a8d/original-resource-actions-value.png 558w&apos; sizes=&apos;(max-width: 558px) 100vw, 558px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Original resource-actions value&lt;/figcaption&gt;
  &lt;/figure&gt;
and then add your default-xml. Of course, you can name it differently.&lt;/p&gt;
&lt;h3 id=&quot;deploying-our-module&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#deploying-our-module&quot; aria-label=&quot;deploying our module permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Deploying our module&lt;/h3&gt;
&lt;p&gt;Now how do we deploy that? We can&apos;t simply deploy our OSGi module as usual - it won&apos;t work. Instead, we need it to be available
in classpath during Liferay start up. To do that we should place our JAR into &lt;em&gt;tomcat/lib&lt;/em&gt; directory.&lt;/p&gt;
&lt;p&gt;So basically:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Create a JAR&lt;/li&gt;
&lt;li&gt;Put it into liferay/tomcat/lib/ directory&lt;/li&gt;
&lt;li&gt;Restart the server&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In my liferay-blog-samples I also overrode the Gradle&apos;s deploy task to make it more automatic&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;deploy() {
into &quot;$rootDir/docker/liferay/tomcat/lib&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But of course keep in mind this works because I use Gradle and the samples have the docker-compose environment defined
in the same repository.&lt;/p&gt;
&lt;h3 id=&quot;testing-our-changes-1&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#testing-our-changes-1&quot; aria-label=&quot;testing our changes 1 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Testing our changes&lt;/h3&gt;
&lt;p&gt;Once we do that we can verify that our changes work:
&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: 305px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 103.65448504983388%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAVCAYAAABG1c6oAAAACXBIWXMAAAsTAAALEwEAmpwYAAACT0lEQVR42q1U224TQQzNL/ZjQH2C/+lTH/oKD1SkgohKkPs2lyYQstlcupu97+zMwfZkg0TaJJVIZM2Md+zxsY9dw3/+1ZIkQf1zHV++3qPRuMeHj59we1vHxt/KBWPM6xwqpeDOXbiuh4XnYe668Lwl8kId3mbnJx6over5M6KtMaSyLEm0rBai2cMVYT2f7+5gplO71/rv951obWyEWZYhCAJSaNrnCLcRoiiW8/7n+8DlJRCGL0a7d1gUBbg4HNl87qHXH6HXfSBbqxPTqyvg+tpaUsTPZ4QcsteisJDzXEGXFopmoYKxqb65gbm4gEozKLqvqGBKlQdSkF5yGEYJnvwAcZIhzQpaU1pzRNsQTJ783XuoN28R0z6JUyRp/qywfY1TEYYRVqs1OckwHI7R+PYD3V4fzXYffWdgH6DI+JFjkhACgcwbUdIaUjH8gCKjRzhyPsekT9hpde+YQ4YsOShLyWXVGRV9qmJaeumjwn6kylzh5XIphnPqGofgjsZTjB6nmM1+YzAYwluuqloe4b2xETLfKlIHwRYLar/VeiMtyOK6C0pBeEYjsUOhjRLIWV6Ic1P9d+wXuKQviEbHJCc+S4RcgDVFFMUEnar9k2D+mrmkj/fJPkeYDTsextg8+TvaPKLZ6uJ7s0O6QF5NTtDlgDYMlQ1TImfBg4JzKhBtGs4VdirE5hwyz6T05DCnj5JXbj06V0UrZcLgRTmYNsZoTCZTtFpttDsODQiHOsYh2ozQbncwHk/OqPK/M203F6sZKRWkdFTRnnL4ByjwYfOds54JAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Extra XYZ permission for document&apos; title=&apos;&apos; src=&apos;/static/d89ce9ea7c78b3e7d8cf5d5752df3ce2/a3e09/XYZ-permission.png&apos; srcset=&apos;/static/d89ce9ea7c78b3e7d8cf5d5752df3ce2/fb933/XYZ-permission.png 301w,
/static/d89ce9ea7c78b3e7d8cf5d5752df3ce2/a3e09/XYZ-permission.png 305w&apos; sizes=&apos;(max-width: 305px) 100vw, 305px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Extra XYZ permission for document&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/static/changed-permissions-portal-impl-785e219c9557baf8cdaa99bcf1d6aa20.gif&quot; alt=&quot;Changed permissions for GUEST and SITE_MEMBER&quot;&gt;&lt;/p&gt;
&lt;p&gt;As you might&apos;ve seen I haven&apos;t specified any translation for my XYZ resource action. Of course, you can do that just like in the
OSGi example.&lt;/p&gt;
&lt;h3 id=&quot;possible-issue-with-overriding-portal-impl-permissions-on-first-liferay-startup&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#possible-issue-with-overriding-portal-impl-permissions-on-first-liferay-startup&quot; aria-label=&quot;possible issue with overriding portal impl permissions on first liferay startup permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Possible issue with overriding portal-impl permissions on first Liferay startup&lt;/h3&gt;
&lt;p&gt;One issue I have encountered while overriding portal-impl permissions definitions is that it won&apos;t work if
you start up Liferay for the first time, with empty database when Liferay creates tables and basic data.&lt;/p&gt;
&lt;p&gt;So instead you should :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Let Liferay start for the first time.&lt;/li&gt;
&lt;li&gt;Deploy your portal-impl override and add portal-ext/docker env property.&lt;/li&gt;
&lt;li&gt;Restart the server.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Of course this won&apos;t be necessary in existing projects where you already have some data in a database.&lt;/p&gt;
&lt;p&gt;I don&apos;t know why this happens but since this is only an issue if you&apos;re starting a totally fresh database
I didn&apos;t go into the details and didn&apos;t try to find exact reason.&lt;/p&gt;
&lt;h2 id=&quot;final-thoughts&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#final-thoughts&quot; aria-label=&quot;final thoughts permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Final thoughts&lt;/h2&gt;
&lt;p&gt;I believe changing permissions in Liferay core modules should be quite easy for you know.
The ways of doing that are not that hard - especially for OSGi modules it&apos;s easy and straightforward
but at the same time these things are not too well documented and in some projects
I have seen some weird ways of doing such changes. That&apos;s why I decided to create a small blog post
to show how it can be done in quite easy way.&lt;/p&gt;
&lt;p&gt;Of course maybe there is still some place for improvement - if you know some, let me know at &lt;a href=&quot;mailto:rafal@pydyniak.pl&quot; target=&quot;_self&quot; rel=&quot;nofollow&quot;&gt;rafal@pydyniak.pl&lt;/a&gt;
and I will update this article.&lt;/p&gt;
&lt;p&gt;Good luck and if you need any help you can always send me an email. I will do my best to help.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Achieving multitenancy using Liferay's Virtual Instances]]></title><description><![CDATA[Imagine you create an app that you will sell to many different companies. It can be for example some application for managing company's finances or an app that will simplify holidays requests flow in a company. What is important is that you will sell that to different entities (companies, universities or whatever) and one client should not know anything about other clients that use your application. Your clients should not share users, data, pages or anything. You even probably want to have different URLs for different clients...]]></description><link>https://pydyniak.com/achieving-multi-tenancy-using-liferay-virtual-instances/</link><guid isPermaLink="false">https://pydyniak.com/achieving-multi-tenancy-using-liferay-virtual-instances/</guid><pubDate>Fri, 16 Jul 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Imagine you create an app that you will sell to many different companies. It can be for example some application for managing company&apos;s finances or an app that will simplify holidays requests flow in a company. What is important is that you will sell that to different entities (companies, universities or whatever) and one client should not know anything about other clients that use your application. Your clients should not share users, data, pages or anything. You even probably want to have different URLs for different clients for example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;client-a.app.com&lt;/li&gt;
&lt;li&gt;client-b.app.com&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;One way of doing that is duplicating your app instance for each client. Each instance can be put on different machine or they can share one physical machine but can be separated from each other using virtualization or using containers.&lt;/p&gt;
&lt;p&gt;But hey - what if your app gets popular and you get thousands of clients? Does it mean you need virtual machine/container or real machine for each of them? Well... yes. This approach of splitting your clients is called &quot;Single-tenancy&quot; (or &quot;Singletenancy&quot; without a dash).
Another approach is a &quot;Multitenancy&quot; (sometimes referred to as &quot;multi-tenancy&quot; with dash or &quot;multi tenant&quot;). This approach, in simple words, means to have different clients on same server. The clients share the same database and all the services. The clients are only separated from each other on database level by using for example some column which distinguish them.&lt;/p&gt;
&lt;p&gt;I actually got some questions about multitenancy in Liferay from one of the clients I did some consulting for and I thought I will cover part of this topic in an article.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt; some sources also mention something &quot;between&quot; these two where there is a single app like in multitenancy approach but uses different databases (or database shards) for each client like in single-tenancy approach. In this article I will not talk about this approach too deeply. I will just give you some informations about Liferay support of multi databases in the end of this article. Beside that I will stick only to single-tenancy and multitenancy approaches for the rest of the article.&lt;/p&gt;
&lt;h2 id=&quot;single-and-multi-tenancy-comparison&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#single-and-multi-tenancy-comparison&quot; aria-label=&quot;single and multi tenancy comparison permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Single and multi tenancy comparison&lt;/h2&gt;
&lt;p&gt;So now you have the basic understanding of what single and multitenancy is. Lets compare some more aspects of these two.&lt;/p&gt;
&lt;p&gt;We will start with things that are better with single-tenancy approach and then we will move to the aspects that are better in multitenancy.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Warning&lt;/strong&gt; please note that I will not go through &lt;strong&gt;every&lt;/strong&gt; aspect that might be important. There are other articles that might cover the topic more deeply. In this article I want to introduce you to the topic but more importantly I want to show you how multitenancy can be achieved in Liferay.&lt;/p&gt;
&lt;h3 id=&quot;data-separation-and-security-that-comes-with-it&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#data-separation-and-security-that-comes-with-it&quot; aria-label=&quot;data separation and security that comes with it permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Data separation and security that comes with it&lt;/h3&gt;
&lt;p&gt;Probably the main benefit of single-tenancy over multitenancy is the fact that the data is really separated from each other. Even if there is some security issue with your app, client A will not see the client B data.&lt;/p&gt;
&lt;p&gt;With multitenancy each client is on the same database so if there is any flaw in your app there is a chance that clients will see eachother data. This of course would be a huge issue for any application provider - not only because app doesn&apos;t look to good to its clients but also it might come with some legal issues for example with EU GDPR.&lt;/p&gt;
&lt;h3 id=&quot;different-clients-can-have-different-versions-of-your-app&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#different-clients-can-have-different-versions-of-your-app&quot; aria-label=&quot;different clients can have different versions of your app permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Different clients can have different versions of your app&lt;/h3&gt;
&lt;p&gt;Another benefit of single-tenancy might be the fact that you can have slightly different versions provided to each client. In some rare occasions it might be crucial for your app to have different modules versions for different clients. This approach is not recommended as it might get really hard to manage these different versions in the future. Nevertheless if you have just couple of clients then you might want to use that sometimes.&lt;/p&gt;
&lt;p&gt;In multi tenant approach you have one server so you provide everything or nothing. This of course doesn&apos;t mean that you can&apos;t prepare per client solutions. You should simply consider other ways of providing these for example using so called &lt;a href=&quot;https://martinfowler.com/articles/feature-toggles.html&quot; target=&quot;_self&quot; rel=&quot;nofollow&quot;&gt;&quot;Feature flags&quot;&lt;/a&gt;. In Liferay&apos;s portlets you can also simply deploy your new portlets but don&apos;t put them on any page or even disable them per client (we will go into details of this later).&lt;/p&gt;
&lt;h3 id=&quot;deployment-process&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#deployment-process&quot; aria-label=&quot;deployment process permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Deployment process&lt;/h3&gt;
&lt;p&gt;In single-tenancy deployment process will be more difficult. Of course as the number of clients increase you will need a good automatic deployment process (continuous delivery/deployment) but it still might cause you some issues if anything goes wrong and you have hundreds of clients (machines, virtual machines or containers).&lt;/p&gt;
&lt;p&gt;With multitenancy there is one single server (or cluster) so deployment process is simplified but it comes with a cost actually - if there is an issue in your deployment it will affect all the clients.
This is the aspect where both single and multi tenancy have their pros and cons.&lt;/p&gt;
&lt;h3 id=&quot;resources&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#resources&quot; aria-label=&quot;resources permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Resources&lt;/h3&gt;
&lt;p&gt;Single-tenancy doesn&apos;t shine so brightly when it comes to resources though. Since you use container/virtual machine or even real machine for each client then you need more resources. Of course Docker is better solution than real machines when it comes to resources increase but it still comes with some cost.&lt;/p&gt;
&lt;p&gt;With multitenancy you have only one server (or cluster of servers) so you can manage that better.&lt;/p&gt;
&lt;h3 id=&quot;providing-high-availability-ha&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#providing-high-availability-ha&quot; aria-label=&quot;providing high availability ha permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Providing high availability (HA)&lt;/h3&gt;
&lt;p&gt;Providing high availability (HA) in single-tenancy approach might be harder as each client has its own instance of an app - it might be impossible to create a cluster for each one of them as it would be expensive and hard to manage.&lt;/p&gt;
&lt;p&gt;With multitenancy clustering is something you will just need to do at some point as more clients means more resources and and some point you will encounter limits of your server.&lt;/p&gt;
&lt;h2 id=&quot;multitenancy-in-liferay&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#multitenancy-in-liferay&quot; aria-label=&quot;multitenancy in liferay permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Multitenancy in Liferay&lt;/h2&gt;
&lt;p&gt;Now we had a little introduction to the topic of multitenancy lets see how this can be achieved in Liferay. In fact this is quite easy as it&apos;s supported out of the box in Liferay Portal with so called Virtual Instances. Virtual instance is just a virtual thing that lets you split your application. Each virtual instance has its own URL, its own users, organizations, roles, pages and even its own sites. There are also lots of per instance configurations.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt; I will use Liferay 7.4.0 CE in my examples but the settings I will show are quite similiar in all of the versions of Liferay, even in obsolote 6.2. Sometimes they are just hidden in other places of the control panel.&lt;/p&gt;
&lt;h3 id=&quot;instances-routing--default-instance&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#instances-routing--default-instance&quot; aria-label=&quot;instances routing  default instance permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Instances routing &amp;#x26; default instance&lt;/h3&gt;
&lt;p&gt;You might wonder - how Liferay knows which instance user should see when he enters your portal? Well it&apos;s quite simple. Each instance has it&apos;s own domain, or so called &quot;Virtual Host&quot;. If user enters your app by this domain Liferay knows which instance should be served.&lt;/p&gt;
&lt;p&gt;Another important term is a &quot;Default instance&quot;. What is a default instance? Well by default it&apos;s an instance created automatically when you first started your Liferay Portal. This is also the instance that is shown to you if you enter your portal using IP address instead of the domain name.&lt;/p&gt;
&lt;p&gt;Default instance is also like a &quot;root&quot; instance and most priviliged one - some config can be made only there for example changing system settings or configuring virtual instances. Because of these two you probably don&apos;t want to put any tenant on your default instance and simply use it as an administrative one. You can also consider blocking access to your portal by IP so no one has accidental access to this default instance.&lt;/p&gt;
&lt;p&gt;If you use Liferay in single-tenancy way (with only one client) then you probably can stick to using only default instance.&lt;/p&gt;
&lt;p&gt;It&apos;s also important to know that user can&apos;t check all the instances you have created unless he has administration role in your default instance.&lt;/p&gt;
&lt;h4 id=&quot;changing-default-instance&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#changing-default-instance&quot; aria-label=&quot;changing default instance permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Changing default instance&lt;/h4&gt;
&lt;p&gt;Sometimes you might need to change the default instance of your Portal. This option is a little bit hidden and I couldn&apos;t easily find any informations regarding that in the Liferay&apos;s docs. This is possible though using portal.properties config and the key you&apos;re looking for is &quot;&lt;em&gt;company.default.web.id&lt;/em&gt;&quot; with default value set to &quot;liferay.com&quot;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;company.default.web.id=liferay.com
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I assume you&apos;re familiar with portal.properties. If not then you can read some more about it, and its extension in &lt;a href=&quot;https://learn.liferay.com/dxp/latest/en/installation-and-upgrades/reference/portal-properties.html&quot; target=&quot;_self&quot; rel=&quot;nofollow&quot;&gt;Liferay docs&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;instances-panel--creating-new-instance&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#instances-panel--creating-new-instance&quot; aria-label=&quot;instances panel  creating new instance permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Instances panel &amp;#x26; creating new instance&lt;/h3&gt;
&lt;p&gt;If you want to see all of your instances you need to go to the:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Menu button -&gt; Control Panel -&gt; Virtual Instances (under System)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;First open menu button
&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: 9.966777408637874%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAACCAYAAABYBvyLAAAACXBIWXMAAA7DAAAOwwHHb6hkAAAAXElEQVR42o2LWwqAIBREXaqaSNAD8qsttodWUP1YaiXmdF1BDRwGzjBMKgOtB3DeQIj2N5xTyw6Kvn1tMFbkyLNptljWDTFGlOScP0npgd09vL9wuAAXTtzkyvYCEfR6KzFDPB4AAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Menu button&apos; title=&apos;&apos; src=&apos;/static/eaa35a9e1c10ea686b968af24a5c18f3/35252/menu-button.png&apos; srcset=&apos;/static/eaa35a9e1c10ea686b968af24a5c18f3/fb933/menu-button.png 301w,
/static/eaa35a9e1c10ea686b968af24a5c18f3/32056/menu-button.png 602w,
/static/eaa35a9e1c10ea686b968af24a5c18f3/35252/menu-button.png 1204w,
/static/eaa35a9e1c10ea686b968af24a5c18f3/5d942/menu-button.png 1343w&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; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Menu button&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;And then in the panel that shows up select Control Panel -&gt; Virtual Instances&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: 717px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 60.132890365448496%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAIAAADtbgqsAAAACXBIWXMAAA7DAAAOwwHHb6hkAAABPElEQVR42n1Sa4+DIBD0///JS22ba5Tl/VTEjnKl9ENvYyJMdpiZhWFdV60NY9w575zjUnvG5ThO48gv18TFkvOSFlQ6a+lq2Pc95y1F9GwZfeuaQ7SzNKQd6RLT/r2GbStKGTB7NIUouJRCWesbaLRFZymlJ2+cC2edtXDtsQUaQiDiRML70FqFVFIq75HOV7FK5vzo84hUTjI4bGbzNAPEtmpBgxgHCHLVH/DDeVD9sJ0SWuGlNwmOtRbNWLxtw6EB+4xXuyHIhcQRL9Uzs3H9CBqZ2mG1kHlmNE0z7qOBUkhxjPCYzh8ZUmfapdPYAzIzAli2TtlaxoW2bs1bTTNgbkSyfF4g7OC+P8Cc6Xp7/Fw8o/1l57BtjEkx4Yn88x4wQ3e7m9+Hvd1BeNtGBqV0jAlm8Ga+fa0a8gRhD7z2z4KCrQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Control Panelu Virtual Instances&apos; title=&apos;&apos; src=&apos;/static/3f27d111a78a18e23f133e77ea394f66/0ad97/control-panel.png&apos; srcset=&apos;/static/3f27d111a78a18e23f133e77ea394f66/fb933/control-panel.png 301w,
/static/3f27d111a78a18e23f133e77ea394f66/32056/control-panel.png 602w,
/static/3f27d111a78a18e23f133e77ea394f66/0ad97/control-panel.png 717w&apos; sizes=&apos;(max-width: 717px) 100vw, 717px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Control Panelu Virtual Instances&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt; this page is only available on default instance of your Liferay Portal.&lt;/p&gt;
&lt;p&gt;Once you open your virtual instances panel you will see table like on the screen below:
&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: 24.916943521594686%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAFCAYAAABFA8wzAAAACXBIWXMAAA7DAAAOwwHHb6hkAAAAnUlEQVR42p1QSxLDIAjN/a/XG3TfTRVQRF7BJC6yrDNvhveBAQ/VASJGDbg7/n1jAtwdxzTHtAkLzOkbOfzJn9oNwPEpjtc7BtYqoMpgbhjDNpKLnJoFsiaS7bemiycym5y5x8AQVNMk9N73Ca214Lp5eqnd79x2Xrl+1YqjxHbZWCrhWwqG2UJulP+qY8AiLNFILNvfl4gsL3MsDT/rj4lXXj7IogAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Virtual Instances panel&apos; title=&apos;&apos; src=&apos;/static/ed1ab439a729dd32fdb904179a641b0a/35252/virtual-instances.png&apos; srcset=&apos;/static/ed1ab439a729dd32fdb904179a641b0a/fb933/virtual-instances.png 301w,
/static/ed1ab439a729dd32fdb904179a641b0a/32056/virtual-instances.png 602w,
/static/ed1ab439a729dd32fdb904179a641b0a/35252/virtual-instances.png 1204w,
/static/ed1ab439a729dd32fdb904179a641b0a/53ac9/virtual-instances.png 1287w&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; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Virtual Instances panel&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;To create new instance you simply click the blue plus button. New page will open:
&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: 470px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 128.2392026578073%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAaCAYAAAC3g3x9AAAACXBIWXMAAA7DAAAOwwHHb6hkAAAB4UlEQVR42q2VP3LUMBjFdRF6zpDDcAEKDpBzQElJBxUzVGloUoSOEmZ2vfY6zsZ/ZFmWZFnW45PMLqSL7WjmN1rt7D5L73v6zJTSCIRh7Yiuk3Et46zgsWwwzgUCfvKQskeaZjg9lkgPKYriBO+XSTLnJrzkYGFX+90eWZLAGU07neCduwBaL4FZreHKEq5u4DnfDBupAJ4EfV3/o2merhfALB3ZHhKYfYIhP2KgYpjdDq7hQCjIOALh6M+E2fGpT5Md4EkkeLmqKJo8zPOCIvKA/L7A0MttVTaDRd0IVDUnWohOo5PrYQMJhg+iU3RLFM09hFgPG4OHwOVGeD/NxVgfbIkkOdBVOyI5ZOj43+oGzRVED8MxWzETjy7XwwbqMH1vIHvqMDRvEYuCljI3UeZCk/AbvLt42JGHWXaktnVEVdXxy/CAIL6GeOSyatE03RyZjVBsRhglYnPt1ezlpmBL7fAzBRpJPo4W51yu9vBX7vDmA/Dj94hTcY+aruD/QV8sGLqNo/++1JuAOatoO9Rk3fz2k70iL3VErYB9udV4/83g03eNivdRMAS0FRK8XQ67up7w6i3w+t2E7DSA1xXKsqGnmfiD85V8LuzrncHHmwGfbw1qrmJ8YhtbKHQW/APkpeZ4xUJqdwAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Adding new Virtual Instance panel&apos; title=&apos;&apos; src=&apos;/static/ccfa920b1f300e118dd3d33f90bab34f/f96db/new-instance-panel.png&apos; srcset=&apos;/static/ccfa920b1f300e118dd3d33f90bab34f/fb933/new-instance-panel.png 301w,
/static/ccfa920b1f300e118dd3d33f90bab34f/f96db/new-instance-panel.png 470w&apos; sizes=&apos;(max-width: 470px) 100vw, 470px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Adding new Virtual Instance panel&lt;/figcaption&gt;
  &lt;/figure&gt;
There you simply need to fill few fields:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Web ID - something unique, as it&apos;s the id of the instance. Often it&apos;s just a domain name&lt;/li&gt;
&lt;li&gt;Virtual Host - the most important field which tells Liferay which domain should be redirected to this instance&lt;/li&gt;
&lt;li&gt;Mail Domain - used for emails notifications from the instance&lt;/li&gt;
&lt;li&gt;Max Users - maximum number of users for this instance. You can use 0 for &quot;unlimited&quot;&lt;/li&gt;
&lt;li&gt;Active - either the instance is active. If it&apos;s not then it&apos;s not accessible by anyone&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;After filling everything you confirm using &quot;Save&quot; button.&lt;/p&gt;
&lt;p&gt;To test your new instance you need to go to the Virtual Host domain. If you don&apos;t have real domain for testing you can use so called &quot;hosts&quot; file to redirect some fake address to your local machine. You can find more info on that topic for example in &lt;a href=&quot;https://www.howtogeek.com/howto/27350/beginner-geek-how-to-edit-your-hosts-file/&quot; target=&quot;_self&quot; rel=&quot;nofollow&quot;&gt;this article&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;In this case that&apos;s exactly what I did - I changed my hosts file to point the app.pydyniak.com to my local machine:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;127.0.0.1 app.pydyniak.com
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The default user is called &quot;test&quot; and its password is also &quot;test&quot; and the email domain is the domain you used for &quot;Mail Domain&quot; field during your instance creation.&lt;/p&gt;
&lt;p&gt;You might also notice that your admin user from default instance doesn&apos;t work on your new instance - like I mentioned before almost everything is separated per instance and that includes users, even the admins.&lt;/p&gt;
&lt;p&gt;From now on you can start using your new instance totally independently.&lt;/p&gt;
&lt;h3 id=&quot;things-shared-between-instances&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#things-shared-between-instances&quot; aria-label=&quot;things shared between instances permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Things shared between instances&lt;/h3&gt;
&lt;p&gt;Of course there are some things shared between instances. Probably you can thing of some based on the previous part of the article but I will also point few most important:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Database - all instances share the same database (later I will also write few words about using different databases/shards per instance)&lt;/li&gt;
&lt;li&gt;Server - obviously if you use one portal you use same server. Virtual instances are just a partitioning capability&lt;/li&gt;
&lt;li&gt;Installed modules/themes - &lt;strong&gt;but&lt;/strong&gt; OSGI portlets, themes and layouts can be disabled per instance. I will go into more details of this later on.&lt;/li&gt;
&lt;li&gt;System settings - the top level settings in Liferay are the system settings. They&apos;re common for all the instances but some of them can still be overriden per instance and they only act as default unless overriden&lt;/li&gt;
&lt;li&gt;Marketplace plugins&lt;/li&gt;
&lt;li&gt;Gogo shell - as modules are common so is the gogo shell. It includes both gogo shell available through control panel as well as the gogo shell access using your terminal&lt;/li&gt;
&lt;li&gt;Search engine - some search can be configured per instance (fields mappings) but in general same search engine (Elasticsearch) is used across all the virtual instances. Note that fields mappings are configured per instance but the configuration can only be made on default instance.&lt;/li&gt;
&lt;li&gt;Log levels&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Things above are probably the most important things that all virtual instances share. It&apos;s also important to know that in general the options that are shared are only configurable through default instance.&lt;/p&gt;
&lt;h3 id=&quot;things-not-shared-between-instances&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#things-not-shared-between-instances&quot; aria-label=&quot;things not shared between instances permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Things not shared between instances&lt;/h3&gt;
&lt;p&gt;Almost everything not mentioned in the previous point is &lt;strong&gt;not&lt;/strong&gt; shared between instances. It includes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Sites&lt;/li&gt;
&lt;li&gt;Pages&lt;/li&gt;
&lt;li&gt;Users, roles, user groups, organizations&lt;/li&gt;
&lt;li&gt;All the content including web contents, templates, structures, categories, tags, widget templates, fragments, documents &amp;#x26; media and everything else you can think of connected to content&lt;/li&gt;
&lt;li&gt;A lot of instance settings (under control &lt;em&gt;panel -&gt; Instance settings&lt;/em&gt;)&lt;/li&gt;
&lt;li&gt;Active components - I will go into more details of this in a while&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;disabling-liferay-components&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#disabling-liferay-components&quot; aria-label=&quot;disabling liferay components permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Disabling Liferay components&lt;/h3&gt;
&lt;p&gt;Like I said before: in Liferay OSGI modules are shared between all instances. This is connected to the fact that one server has one common place where it stores all the modules (OSGI ones, themes, layouts etc). So if you deploy something new to your server it&apos;s automatically available on all virtual instances. Of course the fact it is available doesn&apos;t mean you have to use these. You can simply not put your portlet or any page or don&apos;t set your theme for any page.&lt;/p&gt;
&lt;p&gt;They&apos;re still visible though but there&apos;s is a solution for that. If you decide that you don&apos;t need some components you can actually disable them and that can be done &lt;strong&gt;per instance&lt;/strong&gt;. To do that you need to go to control panel -&gt; components panel of your instance. In this panel you will see three tabs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Portlets&lt;/li&gt;
&lt;li&gt;Themes&lt;/li&gt;
&lt;li&gt;Layout Templates&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In each of these tabs you have a list of components with their status:
&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: 675px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 56.14617940199336%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAA7DAAAOwwHHb6hkAAAA1ElEQVR42qWS627FIAyDef8HnXbroQUS7ngh7Zkm7cdEZ8mlEqr7xWBCIHgfYO2uLqXip8YYSzYpJjAzKAQNbq3pRusd/fKKTKCM13crZA3BE3IuiPKT43DYxXOCJcK3LeLlI+LTMpwEHn6uT9Nl/tM+RF0NEQtZAFEExyx0657fHo703cwHc/x1GCuavU8wmRiGpS/vPexjx7Y9dPNOYC7lPJSYMmptSlhr1WJXNUaX0H4FXoRO/J+RyzehdJjS6buad/VZlRI65+Te3SecVeV8An0BKKVioYVZAmYAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Components list&apos; title=&apos;&apos; src=&apos;/static/527e968cec2b7267edf91e8fe81a1912/23296/components-list.png&apos; srcset=&apos;/static/527e968cec2b7267edf91e8fe81a1912/fb933/components-list.png 301w,
/static/527e968cec2b7267edf91e8fe81a1912/32056/components-list.png 602w,
/static/527e968cec2b7267edf91e8fe81a1912/23296/components-list.png 675w&apos; sizes=&apos;(max-width: 675px) 100vw, 675px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Components list&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;You might notice that sometimes there are two components with same name like &quot;Accounts&quot; on the screen above. To get some more details you can go into details by clicking on the three dots on the right:
&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: 17.940199335548172%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAECAIAAAABPYjBAAAACXBIWXMAAA7DAAAOwwHHb6hkAAAAV0lEQVR42nWNuw2AMAxEvf8sSJRITAFTJPSJiP8EqADzynv2HRCxRbgbESEGVvcmadOUoZSqqh4hIm91ljovaxtGnGboF1cYcD8Hoqe1GjZgFv9Bv8tPDkDt6/OFV0ZxAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Component details&apos; title=&apos;&apos; src=&apos;/static/4fef56f7883ca9e6338f914660e83d71/35252/component-details.png&apos; srcset=&apos;/static/4fef56f7883ca9e6338f914660e83d71/fb933/component-details.png 301w,
/static/4fef56f7883ca9e6338f914660e83d71/32056/component-details.png 602w,
/static/4fef56f7883ca9e6338f914660e83d71/35252/component-details.png 1204w,
/static/4fef56f7883ca9e6338f914660e83d71/c929c/component-details.png 1218w&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; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Component details&lt;/figcaption&gt;
  &lt;/figure&gt;
And in that panel you can check the Plugin ID to see what is the difference between components. You can also disable the component in that panel by unchecking &quot;Active&quot; checkbox and you also a little summary of permissions connected to this component:
&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: 50.83056478405316%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAAA7DAAAOwwHHb6hkAAAAwUlEQVR42qWSWQ7CMAwFc/+jcACOwB9HSReRxXGWh90FBEItUEsjJVI8fYlrEjNY8D6gtXaAilwYxoeIGCNKqQeFDTkXmBBoSrhVPwmdCJ0LSCmBaCZGmiCaqbWq9Tvh6BLG2yzUa5dSJoGyrteGvcpFryzD6KxFPwwiZdAj2ZxUP6RvrGtNsC9UgTTp4Sf5dc9v+w+wnNE/xjhPsF0/Jdpr2sLaHl7mYYh4mSQO1jKU04VxvsrbNJl2SAjxP6KgCe/RVxPz84/dTgAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Component edition panel&apos; title=&apos;&apos; src=&apos;/static/bc3aa721528ffedf54d2e45271d31738/35252/component-details-panel.png&apos; srcset=&apos;/static/bc3aa721528ffedf54d2e45271d31738/fb933/component-details-panel.png 301w,
/static/bc3aa721528ffedf54d2e45271d31738/32056/component-details-panel.png 602w,
/static/bc3aa721528ffedf54d2e45271d31738/35252/component-details-panel.png 1204w,
/static/bc3aa721528ffedf54d2e45271d31738/8b70b/component-details-panel.png 1266w&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; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Component edition panel&lt;/figcaption&gt;
  &lt;/figure&gt;
Likewise you can go to the details of themes or layouts.&lt;/p&gt;
&lt;p&gt;There are many different scenarios where you might want to disable some of the components. For example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You want to disable some default Liferay portlets you don&apos;t need - for example you might not need Liferay Commerce modules in your intranet instance&lt;/li&gt;
&lt;li&gt;In your SaaS you might want to disable components client didn&apos;t pay for so he can&apos;t find them and put them on the page without your knowledge - of course make sure that client doesn&apos;t have admin account and can&apos;t go to components panel himself :) You can always create a &quot;partial admin&quot; role without this specific permission&lt;/li&gt;
&lt;li&gt;You have only one or two themes/layouts you want to use across all your instance and you don&apos;t want someone to use something not meant for the instance&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Please note that of course OSGI modules are still shared and you can only disable some types of components. In some cases you need some extra logic in your code to make sure that it doesn&apos;t affect all the clients and only the ones you choose. Such examples can be CRONs or Servlet (Portlet) filters.&lt;/p&gt;
&lt;h3 id=&quot;per-instance-databasesharding-support-in-liferay&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#per-instance-databasesharding-support-in-liferay&quot; aria-label=&quot;per instance databasesharding support in liferay permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Per instance database/sharding support in Liferay&lt;/h3&gt;
&lt;p&gt;I will not go too deeply about sharding as this is totally out of the scope of this article. If you don&apos;t know what sharding is you can use your favourite search engine to find out some more or, if you are not interested in using different databases with one portal, you can simply skip that part of the article.&lt;/p&gt;
&lt;p&gt;Some people might wonder if there is any out of the box sharding or multiple databases support in Liferay. If you look for that on the internet you might actually find some old articles how to do that but unfortunately they&apos;re outdated. For the moment there is no functionality like that in Liferay 7.
Back in the 6.x versions you could define multiple database sources and for each instance you could pick which one to use. For example instances A, B, C could use one database and instances D, E, F could use another one.&lt;/p&gt;
&lt;p&gt;This is no longer possible as stated in &lt;a href=&quot;https://learn.liferay.com/dxp/latest/en/installation-and-upgrades/upgrading-liferay/other-upgrade-scenarios/upgrading-a-sharded-environment.html&quot; target=&quot;_self&quot; rel=&quot;nofollow&quot;&gt;docs&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Instead you need to stick with one database source and use sharding/replication options provided by your database vendor.&lt;/p&gt;
&lt;h3 id=&quot;other-use-cases-for-liferays-virtual-instances&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#other-use-cases-for-liferays-virtual-instances&quot; aria-label=&quot;other use cases for liferays virtual instances permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Other use cases for Liferay&apos;s Virtual Instances&lt;/h3&gt;
&lt;p&gt;Please note that achieving multitenancy is just one of the things you can do with Virtual Instances. Depending on your business goal you might have other reasons to use them even if you use it only for a single company. In most cases though sites are the natural choice for single company projects.&lt;/p&gt;
&lt;h2 id=&quot;conclusion&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#conclusion&quot; aria-label=&quot;conclusion permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;I hope this was not too long and at least a little bit interesting. Quite often in Liferay many topics, even the popular ones are not covered enough by the documentation and if you look for them you might find a lot of outdated articles. This is actually the case with multitenancy topic - a lot of articles are written for Liferay 6.x and there are not too many resources about 7.x. Of course you don&apos;t want to use Liferay 6.x anymore and there are lots of reasons for that. If for some reason you still use it you should definitely consider updating it - I wrote some articles about the issues you might encounter in &lt;a href=&quot;/why-migrating-liferay-projects-to-a-newer-version-is-not-easy&quot;&gt;Why migrating Liferay projects to a newer version is not easy&lt;/a&gt; article and the &lt;a href=&quot;/why-migrating-liferay-projects-to-a-newer-version-is-not-easy-v2&quot;&gt;second part&lt;/a&gt; of it. Also if you need any help with migration you can always contact me - migrations are my most popular consulting projects.&lt;/p&gt;
&lt;p&gt;I hope this article will help at least some of you.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[GraphQL as an alternative to REST API]]></title><description><![CDATA[Hello! Few days ago I had an opportunity to do a presentation on MeetJS meetup which is basically a series of meetings of JS/frontend community in Poland. The topic of the presentation could be translated to "GraphQL as an alternative to REST API".  This was the first time I did presentation on public conference/meetup so that was an unique experience for me and I'm really glad I had this opportunity. Because of this event I also thought it will be nice opportunity to write a blog article on…]]></description><link>https://pydyniak.com/graphql-as-an-alternative-to-rest-api/</link><guid isPermaLink="false">https://pydyniak.com/graphql-as-an-alternative-to-rest-api/</guid><pubDate>Mon, 29 Mar 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Hello!&lt;/p&gt;
&lt;p&gt;Few days ago I had an opportunity to do a presentation on MeetJS meetup which is basically a series of meetings of JS/frontend community in Poland. The topic of the presentation could be translated to &quot;GraphQL as an alternative to REST API&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: 1200px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 56.14617940199336%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAACSUlEQVR42mPQkzfRkTbSljDUkTHRljax1LXQVzAFMoBcsIgxBGlJGRkoWhopWpgoWeooWGiDpRjsjMwMDRzUTbxVxPWV+LUD/Mx15HQVBXSUhXXUxPQgOoGmqInp+zmEFGRVpaeWGjqHaKrZaksaMtibWOgrmZhoOoT7xUb6x0cFpsaGpkUFxMcFJ3vbBWpJGsEt15Yy0lW10dJz0ZY3g4gwOJhZaUnpBriEn7545tT5kxvXLFq3dPbVm9ceP3swf/Z8VVE9PTlTXVlTkC+ApJSRjqShDsw4oGZLLWk9f9fwY6eOrl45b3ZJwu7u0vPnTz98en/uzHlA12pJG2tIGAI9CSQ1JQ01pYyAIiA/y5gwOFpYATUHuIQdP3NyzfoVq5sLb66c+uz5oxevniyYvUBNVC/aPyktOifcKzYuKDkxJC0lItPTOkBdXB/oFgYLHXMNCaDm0J071tfUVebGBk+uKZg4afKrN0+XLFiiKqJrrGptqm5nrGJtqe1ooeVgomZrqAz0KSgsGHSkjFUEtQNdg6vLs0RFxSXFpdWV1YUExbv7etauWKMspG2q5Wim62yh72qq7WgCZhsoW2tLgf2so2KlLGce7BocGx0oICikIK+graWtqamdnZW2ftVaFRFdYGhBAwyEwGx4gGl6xignFNtp2iSHBptr6StIqyrJqBloGDYW5FfmV6uL6UOSipaUsYGiibWBuZW+OZAEsoEiDNrKVtoa9lrA8JQ21pUBpS0gAjKAMQwMFUQkg5KKsa4sMMKACGozABF3rtPXVCihAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;The image from meet.js page&apos; title=&apos;The image from meet.js announcing my presentation at the meetup.&apos; src=&apos;/static/654fc4614386f3b800128ee82c2ae719/c1b63/graphql-meetjs.png&apos; srcset=&apos;/static/654fc4614386f3b800128ee82c2ae719/fb933/graphql-meetjs.png 301w,
/static/654fc4614386f3b800128ee82c2ae719/32056/graphql-meetjs.png 602w,
/static/654fc4614386f3b800128ee82c2ae719/c1b63/graphql-meetjs.png 1200w&apos; sizes=&apos;(max-width: 1200px) 100vw, 1200px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;The image from meet.js announcing my presentation at the meetup.&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;This was the first time I did presentation on public conference/meetup so that was an unique experience for me and I&apos;m really glad I had this opportunity.&lt;/p&gt;
&lt;p&gt;Because of this event I also thought it will be nice opportunity to write a blog article on this topic. In this article I will explain what is the GraphQL, what are the benefits of using it and what are downsides. I will use Rest API as a reference in some points. If you don&apos;t know REST already then you probably should catch up and learn a little bit more about it as it&apos;s the most common architectural standard connected to transferring data between systems. Explaining the details of REST Api is out of the scope of this article so if you&apos;re not comfortable with it then you should start with some other resources and then come back here. I will assume at least basic knowledge of REST Api, huge experience is probably not needed though.&lt;/p&gt;
&lt;h2 id=&quot;but-what-is-wrong-with-rest-api&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#but-what-is-wrong-with-rest-api&quot; aria-label=&quot;but what is wrong with rest api permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;But what is wrong with REST Api?&lt;/h2&gt;
&lt;p&gt;But lets start with REST Api and a little explanation what is even wrong with it and why should you even consider any alternatives.&lt;/p&gt;
&lt;h3 id=&quot;overfetching&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#overfetching&quot; aria-label=&quot;overfetching permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Overfetching&lt;/h3&gt;
&lt;p&gt;First issue you might have is overfetching. Overfetching is basically any situation when you get more data than needed. For example you need a name of some user but the only endpoint you have available returns also a lot of unnecessary data:&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: 758px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 29.568106312292358%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAGCAYAAADDl76dAAAACXBIWXMAAA7DAAAOwwHHb6hkAAAA0UlEQVR42o2ROwqFQAxFZ3Puw6XZ2QlWdjYuwt5GRMUf/j+RExiZ4hUvcJlkJjkhGXOepxzHIf/Yfd+ybZvm7/uuIoaBcW+yLJM0TWUcR33suk4TiZum+YrWddXTAoFjz/PoXd/3Mk2TmCRJJI5jTaAIEJBlWdSnOzEQfAvEt1Dqfd+XKIrEFEUheZ5/Y13XJVVVSdu2KmDuyDS1YISFYSie50kQBGIoKsvyK2IEgHVdyzAMMs+zyt0hJ40Bc1KDdIe/Fs+j28DG7qdYMQFgu5oXpCvLTFNZzjQAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Image showing overfetching issue&apos; title=&apos;https://www.howtographql.com/basics/1-graphql-is-the-better-rest/&apos; src=&apos;/static/7cc2bf2047d61af5fbb3bda42a0b5b9e/c8e86/overfetching.png&apos; srcset=&apos;/static/7cc2bf2047d61af5fbb3bda42a0b5b9e/fb933/overfetching.png 301w,
/static/7cc2bf2047d61af5fbb3bda42a0b5b9e/32056/overfetching.png 602w,
/static/7cc2bf2047d61af5fbb3bda42a0b5b9e/c8e86/overfetching.png 758w&apos; sizes=&apos;(max-width: 758px) 100vw, 758px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;https://www.howtographql.com/basics/1-graphql-is-the-better-rest/&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;In the example above we also get id of the user, address and birthday. In real life scenario this could be even worse - sometimes when you use headless CMS you might have for example around 30 different fields when you only need one. This of course means more bandwidth used and in some cases more work needed by the server (like additional DB calls). Both results in worse performance.&lt;/p&gt;
&lt;h3 id=&quot;underfetching&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#underfetching&quot; aria-label=&quot;underfetching permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Underfetching&lt;/h3&gt;
&lt;p&gt;Second issue you might encounter is underfetching. In previous point we fetched too much data and in this, as you might guessed, we download not enough.&lt;/p&gt;
&lt;p&gt;Lets imagine we need to create a small part of the page where we display:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;User name&lt;/li&gt;
&lt;li&gt;User&apos;s posts&lt;/li&gt;
&lt;li&gt;User&apos;s last three followers&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Unfortunately in REST this will often means that we need to perform three requests like on the image below:&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: 894px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 79.06976744186048%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAQCAYAAAAWGF8bAAAACXBIWXMAAA7DAAAOwwHHb6hkAAAB3klEQVR42n2UScoiQRCF60iewhN4KY+iCIIbV4oLN6LgQhAVRZznWZw1mi+6X1G/dHdAkBmVMb4XVGB/5P1+2/l8tsvl4vbn83HlOyr79XrZ7Xazx+Pher/f7fl8egx2gANyvV4tk8lYqVTyoO1269/2+73tdjsPxEZJiE0hZDKZWDabtU6nY8FyubThcOgP9XrdBoOB36l6Op1cj8djGBztkORIuVy2eDxuuVzOAh5wQObzuR0OB6FgFFssFv4NH4qQWN1hAwNTNBoN9w3sS3DQGcVOd06aoFOUpIKNe6AAEaP7v4T3KCEod5KFCUVKrVazZrP534R0Q4LvjtkONIBBsEIAtVKp+B3cqM4qoVQX3gTSgFaMmEQiYfl83gLWYzab+UO32w2TkwSGSQzLSqik2GK53W5bMpn0CX+QggMBkvV6bavVygtuNpuQXY2MAoFWyhc7Sso3+IxEEjEqvyghUXVSvgFXNXWgLqKboFURDD/WRsvKuIVCwarValhgOp3aaDTycbXE0cVWEWzIBfeAoF6v5w8kZOM1Mg7ClVOdCgZOpFgsWiwWs1Qq9fvnICL6/b4TQXXIAEMqixDtmn4QFCR+PB5bOp22Vqv1dwz1m9Kpu0bWz0FkCENY/gVeCM8J9/2R1QAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Image showing three requests we need to perform in standard REST Api&apos; title=&apos;Image from https://www.howtographql.com/basics/1-graphql-is-the-better-rest/&apos; src=&apos;/static/c665e5522f63ab5ea975d467009dfe36/38af3/REST-three-requests-for-data.png&apos; srcset=&apos;/static/c665e5522f63ab5ea975d467009dfe36/fb933/REST-three-requests-for-data.png 301w,
/static/c665e5522f63ab5ea975d467009dfe36/32056/REST-three-requests-for-data.png 602w,
/static/c665e5522f63ab5ea975d467009dfe36/38af3/REST-three-requests-for-data.png 894w&apos; sizes=&apos;(max-width: 894px) 100vw, 894px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Image from https://www.howtographql.com/basics/1-graphql-is-the-better-rest/&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;This is connected to the fact that in REST each resource (or set of resources of given type) is represented by some specific URL. That&apos;s why we do a different HTTP Request for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;User data&lt;/li&gt;
&lt;li&gt;User&apos;s posts data&lt;/li&gt;
&lt;li&gt;User&apos;s followers data&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is not the perfect solution for performance reasons. Not only we will have three HTTP requests but also server will have no chance to optimize the process of preparing data.&lt;/p&gt;
&lt;h3 id=&quot;n1-issue&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#n1-issue&quot; aria-label=&quot;n1 issue permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;N+1 issue&lt;/h3&gt;
&lt;p&gt;The problem that also comes with underfetching is the N+1 problem. You might have heard about it as it&apos;s quite common issue related to fetching data from databases but it&apos;s almost the same in REST architecture.&lt;/p&gt;
&lt;p&gt;Example of this can be following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You have an /books endpoint which only returns ISBN of the book&lt;/li&gt;
&lt;li&gt;In order to get more informations about the book you need to call another URL like /books/{ID}&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Look at the image below to get an idea:&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: 304px; &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/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAASCAYAAABb0P4QAAAACXBIWXMAAA7DAAAOwwHHb6hkAAACOklEQVR42qVUa2+cMBDM//917aekOSAH4Q6Ot7HB+AHTMShS2uZSKbG0WryC3dnZWR6c85i0gVoMFj57Z6H1QlsxKUAFmwDvVzjnYMyCeXZoW2CaPbZtxfvzoLXGuexwrgbU4ww1CnR9j6H3yFLg+bTiUoDJDPquZ6IOXWfw4ycYX+C8+zPh24OzBtGlRnwT0EQ7yYHIJKoGeEmALAOaiqimYCzatRiIUskJ298JQ2DbDh8QX1uB11qgqhucTgkeH0+I6aPTGZf8gppIX24DsqLDeg/hunqMakYjFC7tgGEiV0yu1AgpBXlTREZTEvOyQMwG4zQTyHa/5aysEZUDyk5wOG5/mXNisaOD4L3HHl/3IYXYnYSWCeOiRcpWZ05ZigGjlCjJWxwdHBYlIMPUyW9dtaiqcP+Aw/dnYTs7h83IDzikJMLpOUISJTifM+R5hiZwWAlkt3an6kOEnuT2YkTRSeR1D0VtGmt33gJ/8zztGtR6OuLGQRtzcHGXw4KIyGHVC94trFtBBSF8x+vuwz1M1jBhkJDz6yccXlsKXHADNMTQc7oS13LD09NKLa4ob9vOoRxZlBz2Xdik/3BoFo28ERzOiBtZjxPqL/6F88sz0jRFSpU33KSEhdOy+YRDaqCoux1hTq+NZTuebTuiPyzsvbVH3NjD7urwGIrETUwY5MzKK75y3m3KRqLNPxW/nDAkCu0E/52UD5baupKznEKO+RsLyn/7YXwpoSfBatIYZ7svvLHuWy3/BhymeqoCvmlrAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Example of N+1 problem in REST architecture&apos; title=&apos;https://restfulapi.net/rest-api-n-1-problem/&apos; src=&apos;/static/9bff59065b0282aeaf380259c88a8fb3/c1724/N1-issue.png&apos; srcset=&apos;/static/9bff59065b0282aeaf380259c88a8fb3/fb933/N1-issue.png 301w,
/static/9bff59065b0282aeaf380259c88a8fb3/c1724/N1-issue.png 304w&apos; sizes=&apos;(max-width: 304px) 100vw, 304px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;https://restfulapi.net/rest-api-n-1-problem/&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;So if you need to display a list of 100 books with name for each one you need to perform:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;One /books request - in order to get ISBN for each book&lt;/li&gt;
&lt;li&gt;100 requests for /books/{ID} to get the name for each of the 100 books&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Which sums up to 101 request for 100 books and that&apos;s why it&apos;s called N+1 issue. You need to call another endpoint for each resource. This is an issue for two reasons:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;There are 101 HTTP requests each time someone enters your site. Even if backend responds really quickly and you have cached response it&apos;s still a problem since each HTTP request takes some time&lt;/li&gt;
&lt;li&gt;You don&apos;t give your backend server a chance to fetch the data in the best way. It doesn&apos;t know about 101 requests you&apos;re going to make so it will just fetch a data separately one by one for each request.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;coupling-backend-and-frontend&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#coupling-backend-and-frontend&quot; aria-label=&quot;coupling backend and frontend permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Coupling backend and frontend&lt;/h3&gt;
&lt;p&gt;Someone could argue that these are not that big issues. After all you can simply ask your backend developer to make an endpoint that returns exactly the data you need, can&apos;t you? Well you can but the issue is that it couples backend and frontend together much more. The coupling in software engineering tells you how closely modules or part of your system are connected or, in other words, the strength of the relationship between them. Why is high coupling bad? Well because each time you change your module other modules that are highly coupled need to be changed as well.&lt;/p&gt;
&lt;p&gt;In our example - if we tell our backend developer to make an API that returns exactly the data we need that&apos;s fine until there is a need for a change. Lets imagine that we have the application which displays 100 books (as in example above) and we asked our backend developer to make an API with the exact data we need. So we have something like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;GET /books - returns books we need and for each it also returns the name&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This way we don&apos;t need to make another call for book details and we avoid n+1 issue. That&apos;s good but then client comes and asks for another field - now he wants to include the book publishion date in the books list. In such scenario frontend developer again needs to ask the backend developer and they both need to do changes and synchronize them and also make sure they&apos;re deployed at the same time. This is the issue we have from high coupling between parts of our system.&lt;/p&gt;
&lt;p&gt;Another issue might be that we can&apos;t always change the API easily. There are many cases where it&apos;s hard. For example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;We have some API developed by third party company and it&apos;s expensive to make such changes&lt;/li&gt;
&lt;li&gt;We use some public API for example for some government data or stock market data.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In such scenarios we&apos;re stuck with N+1 issue and performance issues.&lt;/p&gt;
&lt;h2 id=&quot;graphql&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#graphql&quot; aria-label=&quot;graphql permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;GraphQL&lt;/h2&gt;
&lt;p&gt;Now we understand what issues we might encounter while using REST lets finally get to the GraphQL. I know it was quite a long introduction but I believe there&apos;s no point of showing an alternative without explaining the issue first.&lt;/p&gt;
&lt;h3 id=&quot;few-words-of-a-history&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#few-words-of-a-history&quot; aria-label=&quot;few words of a history permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Few words of a history&lt;/h3&gt;
&lt;p&gt;Well I don&apos;t know if you remember these times but apparently back in 2012 the Facebook mobile app wasn&apos;t as good as it is now. It used to crash a lot and the performance was poor. The reason for that is that mobile apps were simply a wrappers for mobile website and therefore the possibilites for improving them were limited. In that year the Facebook team decided to rebuild their app and improve it. They tried different options for API including REST but eventually they started work on a thing that now is known as GraphQL.&lt;/p&gt;
&lt;p&gt;For details of the history you can check the &lt;a href=&quot;https://engineering.fb.com/2015/09/14/core-data/graphql-a-data-query-language/&quot; target=&quot;_self&quot; rel=&quot;nofollow&quot;&gt;post on Facebook blog&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;so-what-is-graphql&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#so-what-is-graphql&quot; aria-label=&quot;so what is graphql permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;So what is GraphQL?&lt;/h3&gt;
&lt;p&gt;GraphQL is simply a query and data manipulation language for APIs. Like REST it&apos;s not tied to any database, storage, language or framework. Instead it&apos;s more like an approach for your API development. You could say that GraphQL is a presentation layer for your backend, exactly like REST. GraphQL is traditionally served over HTTP but it&apos;s actually agnostic to the transport layer - you could also use different method of connection like WebSockets. The HTTP is the most important though and I will only focus on it.&lt;/p&gt;
&lt;p&gt;There are features of GraphQL which makes it different than REST though.&lt;/p&gt;
&lt;h3 id=&quot;declarative-data-fetching&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#declarative-data-fetching&quot; aria-label=&quot;declarative data fetching permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Declarative data fetching&lt;/h3&gt;
&lt;p&gt;Declarative data fetching is probably the most important feature of GraphQL. Remember when we talked about issues with REST? They all came from the fact that our API had defined endpoints with defined data these endpoints return. Because of these we had our overfetching, underfetching and N+1 issues. Because of these we had a choice either to go with poor performance or high coupling between frontend and backend.&lt;/p&gt;
&lt;p&gt;Declarative data fetching is a response to that issue. When I talked about underfetching I presented an example with simple part of page with name, followers and blog posts where we needed to make three REST requests to get the data we need. We also got more data we needed for each of these endpoints. With GraphQL our API call could look much simpler:&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: 917px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 59.46843853820598%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAYAAABiDJ37AAAACXBIWXMAAA7DAAAOwwHHb6hkAAABc0lEQVR42pWSQW7CMBBFcyluRa/BGRDds2VRiQUnQIIdS0S7QFBI2pLEdmI7dn7zHVFFkIp2JMuRMvP8589E6IsauOQphJKwxkAIgaIo4JzDo4jquob3Hry7UdgSsbrAVhbWWlRVFfIeAoOgGxiDxZ86R64ljDYB2P3nKhcUm6YDpRS01oETxXGM7XaL8/mMLpzJpTNwtb/roGyKja8C7KqaXfBEq9UK4/EYy+WyTS7L8CKT6R+9C4qaB6jSeQedF7BvWcifz+d4Gg4xnU7blqWUmM1myLIsFMZJjI8kCaCkuff7ffhmnsgFdGVQHnOIl9emd2DyPMFgMMBoNGqBVLJer3884p2KDNqaXm8Z9NSbNn+322GxWGCz2bQeUhWVdCfIQZzkF9IsRdG0TxuokjeLKIJD4LnWUQhP1KeA5mZGQZqiAbagW0B34t2V6l0brsRBJThdErwfjkHRX+NOIeFUyHXhRPn9l4X+Fcj1+A/gFvgND/+jS+XrCGIAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Single request for GraphQL instead of three in REST&apos; title=&apos;https://www.howtographql.com/basics/1-graphql-is-the-better-rest/&apos; src=&apos;/static/69931104269a4905d478468249e0e780/59000/GraphQL-single-request.png&apos; srcset=&apos;/static/69931104269a4905d478468249e0e780/fb933/GraphQL-single-request.png 301w,
/static/69931104269a4905d478468249e0e780/32056/GraphQL-single-request.png 602w,
/static/69931104269a4905d478468249e0e780/59000/GraphQL-single-request.png 917w&apos; sizes=&apos;(max-width: 917px) 100vw, 917px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;https://www.howtographql.com/basics/1-graphql-is-the-better-rest/&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;If you take a closer look on the request and response you might notice that we basically defined data we need and our API returned exactly what we wanted. This is declarative data fetching - it gives us a possibility to define what data (fields) we need. We also don&apos;t need to call different URLs for different kind of resources, instead we can call for user and in the same request we ask for different resources connected to the user like posts and followers.&lt;/p&gt;
&lt;p&gt;It&apos;s a powerful feature and actually it solves all of our issues with underfetching, overfetching, N+1 issue and high coupling.&lt;/p&gt;
&lt;h4 id=&quot;coupling&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#coupling&quot; aria-label=&quot;coupling permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Coupling&lt;/h4&gt;
&lt;p&gt;Remember how I said that you might encounter an issue for example with government data? Or with data from another company? In such scenarios it can be really costful or even impossible to change the data returned. If such public APIs used the GraphQL these issues would not exist as all clients could simply ask for data they need.&lt;/p&gt;
&lt;h3 id=&quot;there-is-only-a-single-endpoint&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#there-is-only-a-single-endpoint&quot; aria-label=&quot;there is only a single endpoint permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;There is only a single endpoint&lt;/h3&gt;
&lt;p&gt;One of the thing about GraphQL is that there is only a single endpoint available which is like a proxy for all the data. Instead of calling different endpoints for different data you always call the same one and just describe what you want to query or modify. You can say that GraphQL acts as a proxy not only because you call single URL but also because you can ask for data from its database or from other microservices, systems or APIs&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: 742px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 97.34219269102991%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAATCAYAAACQjC21AAAACXBIWXMAAA7DAAAOwwHHb6hkAAAB6UlEQVR42p1Uy4oaURDtf5qA4i/4D25dieDOPxDcunInLrJQFHQpCQHRhbgws9EEVIJP8NE++mW3etKnhiujhMnYBcW9/ahzz6mqWxoC2OFwwGq1wnq9xn6/x3a7ha7rMAwDWhBAz/Pgui6u16s8c3UcR9ZAgAwmo0wmg1gshnK5fDtICwJGaZvNBolEApFIBPl8Xr6dz+fnAE+nk+TMNE1ZK5UKcrkc2u22gF0uF2jUrfx/tlwuMRqN5F/KU0Yw5pT2KYYEoExWl+wUAF0xU6aNx2P0+33M5/Nb8CNby7Jg2pa0x/tg27bvngWwWq0im82i2WzKC57Kn967Y/stYbxJXCwWqNVqqNfrsn80bTqdolgs4ng8StLpZM18Eci5uLD+6Dh8/e2fBnz78R1fXl4QDofR6XQE5E4y26DVar0x8fdkyDxRjke28Jt2Z8L9pUvAz9dXJJNJpFIpDAaDW5ruAHu93odVPpoG3LN3KwJVcP/PHH6mymS89RuZaSGQunoEfiSiPebgoxvCvBJUxZCt8qcYqjxNJhO5crTdbofZbCaVpqtG156dMuxJDgbK5coRxndK/lOA6sZQPlml02lEo1GUSqXg00YVhPLj8ThCoRAKhUJwQHWbOG263S4ajQaGw6GAcdr8BZPzsOohz46AAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;GraphQL can act as a proxy for different data&apos; title=&apos;https://www.howtographql.com/basics/3-big-picture/&apos; src=&apos;/static/50c7b9ec2c0515debf780cc5acdf20f5/0f2bc/GraphQL-data-from-different-resources.png&apos; srcset=&apos;/static/50c7b9ec2c0515debf780cc5acdf20f5/fb933/GraphQL-data-from-different-resources.png 301w,
/static/50c7b9ec2c0515debf780cc5acdf20f5/32056/GraphQL-data-from-different-resources.png 602w,
/static/50c7b9ec2c0515debf780cc5acdf20f5/0f2bc/GraphQL-data-from-different-resources.png 742w&apos; sizes=&apos;(max-width: 742px) 100vw, 742px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;https://www.howtographql.com/basics/3-big-picture/&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;We can define how the data should be retrieved by the server using &quot;Resolver functions&quot;. These functions job will be to retrieve the data user asked for. In many cases it will simply call your database (or more likely, use the service layer) but just keep in mind this is not always the case.&lt;/p&gt;
&lt;h3 id=&quot;strongly-typed&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#strongly-typed&quot; aria-label=&quot;strongly typed permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Strongly typed&lt;/h3&gt;
&lt;p&gt;GraphQL is strongly typed. The types for each field are defined used Schema Definition Language (SDL). Of course if you come from languages that are not strongly typed you might think that it is something you won&apos;t need but remember the APIs are meant to be used by another people. It&apos;s not just some simple function only you will write, change and use.&lt;/p&gt;
&lt;p&gt;The API might be used by different developers, perhaps not from the same company. The fact that the API is strongly typed is helpful for making a contract between clients of the API and the developers who create that API.&lt;/p&gt;
&lt;h3 id=&quot;response-mirros-the-request&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#response-mirros-the-request&quot; aria-label=&quot;response mirros the request permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Response mirros the request&lt;/h3&gt;
&lt;p&gt;Like you could see above - the response from the API looks very similiar to the request. This is really useful if you think about it as the returned data is really easy to predict.&lt;/p&gt;
&lt;h3 id=&quot;introspection&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#introspection&quot; aria-label=&quot;introspection permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Introspection&lt;/h3&gt;
&lt;p&gt;In GraphQL you can use a feature called &quot;Introspection&quot; for describing the API. This means you can simply ask the GraphQL API for defined resources and types:&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: 730px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 26.910299003322258%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAFCAYAAABFA8wzAAAACXBIWXMAAA7DAAAOwwHHb6hkAAAAeElEQVR42o2PwQrDQAhE97P3j3voJaFk66rjVEvpIaRNBgYRxsfYVLWTPLF3d7+QY28RwaeCkv6nABjqROZz+5lrcMeyblgfo4iHLtUcm+B2XzCGAGqo23225cs8U0R8p0ylWjYVJQwHDYFLwPi8WsCpxpj2hu6BL+o8ghU91KGJAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Introspection in GraphQL&apos; title=&apos;https://graphql.org/learn/introspection/&apos; src=&apos;/static/e5379d7bd61271b6af21a4ffd603f551/e9beb/GraphQL-introspection.png&apos; srcset=&apos;/static/e5379d7bd61271b6af21a4ffd603f551/fb933/GraphQL-introspection.png 301w,
/static/e5379d7bd61271b6af21a4ffd603f551/32056/GraphQL-introspection.png 602w,
/static/e5379d7bd61271b6af21a4ffd603f551/e9beb/GraphQL-introspection.png 730w&apos; sizes=&apos;(max-width: 730px) 100vw, 730px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;https://graphql.org/learn/introspection/&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;This feature can be used by the GraphQL client you use so it will automatically generate a documentation for you - without any additional work from the developer! Check the GIF below for an example of such generated docs:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/static/GraphQL-introspection-example-2ae9aa0be7f7b26a98ec5bde8a1104c8.gif&quot; alt=&quot;Example of auto generated docs using introspective by GraphQL client&quot; title=&quot;Browsing the docs generated by Altair GraphQL client (Chrome extension version)&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;mutations-for-data-manipulation&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#mutations-for-data-manipulation&quot; aria-label=&quot;mutations for data manipulation permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Mutations for data manipulation&lt;/h3&gt;
&lt;p&gt;Usually API not only returns data but can also be used for inserting, modifying or deleting the data. In REST you do that by using different HTTP methods: POST, PUT, DELETE and sometimes PATCH.&lt;/p&gt;
&lt;p&gt;In GraphQL on the other hand you use a thing called mutations. You can think of mutations as defined methods you can call. In REST you have different URLs combined with HTTP method for different thing you want to do. Here you have different methods under different names you can call. Below you can see an example of calling such method&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: 858px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 18.272425249169437%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAECAYAAACOXx+WAAAACXBIWXMAAA7DAAAOwwHHb6hkAAAAYklEQVR42pWPOw6AMAxDe/9rIqBIQAr5dTNtWBjp8KREsp04rUVQboaYQdRgohD32L3WIaz50jQvyFvG0ULpEuhZsLOCmKEttIv+0vWJCsUQF6oH7i82SASG+fu61+Gq38oP/V42UC2+Oy0AAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Example of mutation in GraphQL&apos; title=&apos;https://www.howtographql.com/basics/2-core-concepts/&apos; src=&apos;/static/0888a6ed07200ca37ab3a8d624eee760/42d54/GraphQL-mutations.png&apos; srcset=&apos;/static/0888a6ed07200ca37ab3a8d624eee760/fb933/GraphQL-mutations.png 301w,
/static/0888a6ed07200ca37ab3a8d624eee760/32056/GraphQL-mutations.png 602w,
/static/0888a6ed07200ca37ab3a8d624eee760/42d54/GraphQL-mutations.png 858w&apos; sizes=&apos;(max-width: 858px) 100vw, 858px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;https://www.howtographql.com/basics/2-core-concepts/&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;You can also notice that not only you call method with some parameters (name: &quot;Bob&quot;, age: 36) but you can also define what data you want to be returned when the record is created or modified - just like in queries.&lt;/p&gt;
&lt;p&gt;Of course available mutations are also available through introspection and therefore in automatically generated docs.&lt;/p&gt;
&lt;h3 id=&quot;downsides&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#downsides&quot; aria-label=&quot;downsides permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Downsides&lt;/h3&gt;
&lt;p&gt;GraphQL like anything else had its pros and cons. If we talk about downsides there are few most important.&lt;/p&gt;
&lt;h4 id=&quot;cant-we-do-the-same-with-rest&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#cant-we-do-the-same-with-rest&quot; aria-label=&quot;cant we do the same with rest permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Can&apos;t we do the same with REST?&lt;/h4&gt;
&lt;p&gt;Someone after reading all of these could ask - well that&apos;s cool, but can&apos;t we do the same with REST? For example use HTTP parameters to ask for the fields we need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;GET /books?fields=name,publishedDate&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And indeed we could. This is not a standard though and we might not find that in APIs created by another people. Also it could work well with simple fields but it would be much harder to ask for complex data types - for example ask for User and for his followers (which are also User type) and to fetch only name from the followers.&lt;/p&gt;
&lt;p&gt;But indeed in some cases we might simply not need that at all - for example if we have simple microservice used only by other microservices in our internal architecture.&lt;/p&gt;
&lt;h4 id=&quot;adds-complexity&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#adds-complexity&quot; aria-label=&quot;adds complexity permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Adds complexity&lt;/h4&gt;
&lt;p&gt;For simple APIs the GraphQL might be just too much. It can add complexity which is unnecessary in many cases.&lt;/p&gt;
&lt;h4 id=&quot;caching-might-be-harder-with-graphql&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#caching-might-be-harder-with-graphql&quot; aria-label=&quot;caching might be harder with graphql permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Caching might be harder with GraphQL&lt;/h4&gt;
&lt;p&gt;In REST we have endpoints. Each endpoint represents some data. In GraphQL on the other hand we have one endpoint and the returned data is calculated based on what request we make to that endpoint.&lt;/p&gt;
&lt;p&gt;If we have some cache layer between our server and the client then it might be an issue as caching whole URLs is just easier.&lt;/p&gt;
&lt;p&gt;It doesn&apos;t mean we can&apos;t have a good performance using GraphQL - we simply need to think a little bit differently about the way we use cache and the connections between layers in our architecture.&lt;/p&gt;
&lt;h4 id=&quot;performance-for-multi-level-queries&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#performance-for-multi-level-queries&quot; aria-label=&quot;performance for multi level queries permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Performance for multi level queries&lt;/h4&gt;
&lt;p&gt;Since GraphQL gives you possibility to connect data the way you want you might be tempted to ask for data too &quot;deeply&quot;. For example ask for author of blog post, then ask for all of his posts then ask for all reviews and for user of these reviews etc. Such use cases might result in poor performance on the backend side as querying for the data could become an issue.&lt;/p&gt;
&lt;h4 id=&quot;less-popular&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#less-popular&quot; aria-label=&quot;less popular permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Less popular&lt;/h4&gt;
&lt;p&gt;REST Api is way more popular than GraphQL. This means more resources, more frameworks, most questions on StackOverflow. Your job colleagues also might feel more secure to use something they already know than learning something new.&lt;/p&gt;
&lt;p&gt;Of course GraphQL isn&apos;t that new anymore but still there is a huge gap between REST and GraphQL when it comes to popularity. On the other hand you will already find quite a lot of resources on the topic and frameworks which supports it. In Java for example you can easily create it using Spring - it&apos;s not that different from creating REST Api.&lt;/p&gt;
&lt;h3 id=&quot;anyway---will-it-replace-rest&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#anyway---will-it-replace-rest&quot; aria-label=&quot;anyway   will it replace rest permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Anyway - will it replace REST?&lt;/h3&gt;
&lt;p&gt;Well there is no single answer. GraphQL is not meant to be replacement of REST but an alternative. Anyway in my personal opinion in some cases it&apos;s simply better. Examples of such scenarios can be:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;API for different clients for example Android+IOS+Web app - in such scenario each client can make changes in their own pace or use the API slightly differently - it&apos;s quite common to have slightly different data on Web App and mobile app&lt;/li&gt;
&lt;li&gt;Publicly available APIs - you don&apos;t know your clients. You don&apos;t know what use cases they might have with your data. In such scenario you can&apos;t provide some specific API and instead it must be generic so everyone can use it. If you use GraphQL your clients can fetch the data they need and also avoid issues with underfetching/overfetching&lt;/li&gt;
&lt;li&gt;Using API available in CMS - in some cases you might have an option to pick between REST API or GraphQL provided by CMS you use. This is for example the case if you use Liferay - both REST and GraphQL are available if you plan to use it as headless CMS for your data. In such scenario I believe you should go for GraphQL as even if REST seems fine at first it might not be fine later when the use cases are more complex&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There are also scenarios where REST might work just fine. For example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Simple server with not too many endpoints&lt;/li&gt;
&lt;li&gt;Microservice for internal architecture&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;summary&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#summary&quot; aria-label=&quot;summary permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Summary&lt;/h2&gt;
&lt;p&gt;To summarize - I believe if you ever worked with REST you should also know the basics of GraphQL. This way you will be able to make better decisions about architecture of your application. This will of course result in better app you do, better API you provide to your clients or perhaps reduces time to market and simplifies the maintainance of your app once it is realeased.&lt;/p&gt;
&lt;p&gt;In this article I didn&apos;t touch any code. I did it on purpose as the code will look different for different languages/frameworks but the concept will stay the same. I haven&apos;t also explained too much of the GraphQL code but that&apos;s simply because there is a nice documentation available at &lt;a href=&quot;https://graphql.org/learn/&quot; target=&quot;_self&quot; rel=&quot;nofollow&quot;&gt;https://graphql.org/learn/&lt;/a&gt;. I belive you will find everything you need there once you decide to learn something more about GraphQL or you decide to start using it.&lt;/p&gt;
&lt;p&gt;Like always I hope you learned something. If you had any questions you can always &lt;a href=&quot;http://pydyniak.com/contact/&quot; target=&quot;_self&quot; rel=&quot;nofollow&quot;&gt;contact me&lt;/a&gt; or leave a comment.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Why should you use CMS such as Liferay for your web application]]></title><description><![CDATA[In this short article I would like to answer the question that some of you might have "Why should I use CMS for my website". As an example of such CMS I will use Liferay which is primary system I use. What is CMS anyway? Lets first define what CMS system is. CMS is just an abbreviation for "Content Management System" which is a software that simplifies the process of creating and modifying digital content. The main two areas for which CMS systems are used are: Web content management - creating…]]></description><link>https://pydyniak.com/why-should-you-use-cms-such-as-liferay-for-your-web-application/</link><guid isPermaLink="false">https://pydyniak.com/why-should-you-use-cms-such-as-liferay-for-your-web-application/</guid><pubDate>Fri, 12 Mar 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In this short article I would like to answer the question that some of you might have &quot;Why should I use CMS for my website&quot;. As an example of such CMS I will use Liferay which is primary system I use.&lt;/p&gt;
&lt;h2 id=&quot;what-is-cms-anyway&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#what-is-cms-anyway&quot; aria-label=&quot;what is cms anyway permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;What is CMS anyway?&lt;/h2&gt;
&lt;p&gt;Lets first define what CMS system is. CMS is just an abbreviation for &quot;Content Management System&quot; which is a software that simplifies the process of creating and modifying digital content. The main two areas for which CMS systems are used are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Web content management - creating web content such as pages on your website or articles on a blog&lt;/li&gt;
&lt;li&gt;Enterprise content management - digital documents management in a collaborative software&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Beside these two there are many other things that typical CMS supports such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Managing the digital assets like videos or images&lt;/li&gt;
&lt;li&gt;Users, authentication, roles&lt;/li&gt;
&lt;li&gt;Multiple languages&lt;/li&gt;
&lt;li&gt;Administrator panel for non technical users&lt;/li&gt;
&lt;li&gt;Content hierarchy which means that pages can have subpages, images can be grouped into directories etc.&lt;/li&gt;
&lt;li&gt;Tools for improving SEO of your site&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Of course there might be much more features (or less) depending on the CMS software you use.&lt;/p&gt;
&lt;h3 id=&quot;but-are-the-cms-systems-actually-popular&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#but-are-the-cms-systems-actually-popular&quot; aria-label=&quot;but are the cms systems actually popular permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;But are the CMS systems actually popular?&lt;/h3&gt;
&lt;p&gt;At this point you might wonder are the CMS systems popular at all? Well actually yes. According to W3Techs.com five most popular CMS systems have almost 50% of the market share:&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: 682px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 37.54152823920266%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAICAYAAAD5nd/tAAAACXBIWXMAABJ0AAASdAHeZh94AAABaklEQVR42oVS0XKCMBDk/7+utaNTCCGgCCiQhOqgKLjdi9XXMnOTu02yt3shKssdssygKAo0TYM8rHXAtswPhwOqqg5427b474uu1ysulxET12m6YRzHEILXdQVjDMqyRLWvw/7xeETLkK/rOp6pWbf4OZ2ehN4NsPYE50fc7wu8s1ST4zpN6KhIax3UV1UVCI3JsdvtcLvdIO6USlHkOYkbPB4kTJI9Pj8svlYd7TluGqzXaxxJNjiHNNWI42829OipKEkSYikc96y1IdeZDvVEEVFmKqxWjl17nM+OXffQ7Nr3PRwviGW5JLUnqckyaDaRXLAXoZAHQqX22Gw8o+dhF+zEcfwmFMtxwpq5qJBa64yKnwqVUkiJiQMZQ2SyJhBut5YkHjVnpdnVWhcIZPASlrmoyjlDiWHg7HsbRqBSFf4Aechonme+8IJ5XjhUWWc+zh3LsvzFg/jjXb/2BROLPUlFqR98sPwLsBRZH+s7RbcAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Image showing market share by top 5 CMS&apos; title=&apos;&apos; src=&apos;/static/f90f66a004da48e0de01ee169e53474a/160a3/five-top-CMS-market-share.png&apos; srcset=&apos;/static/f90f66a004da48e0de01ee169e53474a/fb933/five-top-CMS-market-share.png 301w,
/static/f90f66a004da48e0de01ee169e53474a/32056/five-top-CMS-market-share.png 602w,
/static/f90f66a004da48e0de01ee169e53474a/160a3/five-top-CMS-market-share.png 682w&apos; sizes=&apos;(max-width: 682px) 100vw, 682px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Image showing market share by top 5 CMS&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;As you can see the number one is definetelly Wordpress which has around 40% market share.&lt;/p&gt;
&lt;h3 id=&quot;well-should-i-use-wordpress-then&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#well-should-i-use-wordpress-then&quot; aria-label=&quot;well should i use wordpress then permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Well should I use Wordpress then?&lt;/h3&gt;
&lt;p&gt;You might wonder - if wordpress is so popular, should I use it as well? Well it depends. If you need a simple personal blog or a website for small company then... yes! You should definitely consider Wordpress as your CMS system. Although if you need more complicated website with your own modules, advanced permissions system, best performance, clustering etc then wordpress might just be not enough. Wordpress, like everything else, has its pros and cons and the main property of wordpress is its simplicity. While it&apos;s advantage for small sites it also might be a huge disadvantage for bigger ones. For example if you want your site to have a lot of users with different roles and permissions or you need to synchronize data with third party system like SAP or perhaps you want to develop your own application for your employees and you need a lot of custom modules then it might just be too simple for your needs.&lt;/p&gt;
&lt;h3 id=&quot;what-are-my-options-then-for-more-sophisticated-website&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#what-are-my-options-then-for-more-sophisticated-website&quot; aria-label=&quot;what are my options then for more sophisticated website permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;What are my options then for more sophisticated website?&lt;/h3&gt;
&lt;p&gt;Actually there are quite a lot of advanced CMS systems and it would be hard to present any details about all of them (or at least half of them) and that&apos;s why in this post I will only focus on one of your options and perhaps in the future I will also write a blog entry about some others.&lt;/p&gt;
&lt;p&gt;Today I will focus on Liferay though.&lt;/p&gt;
&lt;h2 id=&quot;what-is-liferay-then&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#what-is-liferay-then&quot; aria-label=&quot;what is liferay then permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;What is Liferay then?&lt;/h2&gt;
&lt;p&gt;Well since the previous part of this article was focused on CMS systems then... well you guessed correctly, Liferay is one of them. Unlike Wordpress it&apos;s more complicated though and it also gives you much more possibilites when it comes to creating and managing your website.&lt;/p&gt;
&lt;p&gt;Does it mean you can&apos;t use it for a simple website? Of course you can! You just probably don&apos;t want to :) Let&apos;s then talk about few properties of Liferay or, in other words, reasons why would you want to consider it or not as your web platform. Of course it would be hard to go through all of them and that&apos;s I will just mention few - of course in the end every platform needs to be considered in details for specific use case (in which I can of course help, just write to me and I will try to do my best!).&lt;/p&gt;
&lt;h3 id=&quot;advantages-of-using-liferay&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#advantages-of-using-liferay&quot; aria-label=&quot;advantages of using liferay permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt; Advantages of using Liferay&lt;/h3&gt;
&lt;p&gt;Lets first go through advantages and then I will also talk about disadvantages&lt;/p&gt;
&lt;h4 id=&quot;advanced-pages-hierarchy-and-documents--mediaweb-contents-segregation&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#advanced-pages-hierarchy-and-documents--mediaweb-contents-segregation&quot; aria-label=&quot;advanced pages hierarchy and documents  mediaweb contents segregation permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Advanced pages hierarchy and documents &amp;#x26; media/web contents segregation&lt;/h4&gt;
&lt;p&gt;Liferay is quite great when it comes to configuring your webcontents, pages or your media. You can have folders with different media types or different web contents. You can have pages and subpages. Actually everything can be organized which is great for huge sites with thousands of different kind of assets. Of course you can also categorize your content and use tags, whatever you need.&lt;/p&gt;
&lt;p&gt;Liferay can also handle searching through different kind of assets you might have in one common way. You can easily choose for which kind of assets your search box should look for. You might for example only look for pages or you might want to search your whole system (pages, users, images, blogs, etc) - it&apos;s totally up to you but Liferay gives you a lot of options that you can configure.&lt;/p&gt;
&lt;h4 id=&quot;advanced-users--permissions-system&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#advanced-users--permissions-system&quot; aria-label=&quot;advanced users  permissions system permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Advanced users &amp;#x26; permissions system&lt;/h4&gt;
&lt;p&gt;Liferay has advanced permissioning system. Almost everything in Liferay has its own permission so you can easily decide what exactly you want users to see or be able to do. If you develop your code it&apos;s also easy to create your own permissions so then they can be easily managed from the control panel without any technical knowledge.&lt;/p&gt;
&lt;p&gt;Another important thing is the number of possibilites you have to organize your users. You can create multi level structure of users using organizations or you can simply group your users using a thing called &quot;User groups&quot;. Or you can use them both to fill your needs in a best possible way. After you organize your users you can then easily assign them to different roles or sites to give them access exactly to things you want.&lt;/p&gt;
&lt;p&gt;But what if you already have your users in some system? Well the great thing is that you have out of the box integration with different, most popular systems (LDAP, SSO, CAS, Facebook and many more) so perhaps you don&apos;t need even a line of code to integrate users between systems.&lt;/p&gt;
&lt;h4 id=&quot;multiple-instances-or-sites-on-same-server&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#multiple-instances-or-sites-on-same-server&quot; aria-label=&quot;multiple instances or sites on same server permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Multiple instances or sites on same server&lt;/h4&gt;
&lt;p&gt;Well imagine that you&apos;re creating system you plan to sell to different companies. For example a system in which company&apos;s employees will be able to create holiday requests, organize their worktime and stuff like that. For obvious reasons you don&apos;t want to mix these users on same server, they should not see each other. Company X should not even know that company Y is also using the same system. Of course they also want the urls connected to their company so for example x.com and y.com. On the other hand if you have a lot of clients then creating another server and then managing it for each of them might be a hard and expensive process.&lt;/p&gt;
&lt;p&gt;In such scenario Liferay virtual instances might be handy for you. Each client might be on his separate instance. Each instance has its own pages, users, roles, content and even its own url. Basically everything is separated which decreases the costs and also might cut time to market since you don&apos;t need different server, you just need to create new instance for your new client.&lt;/p&gt;
&lt;p&gt;But hey! That&apos;s not all. The company X might want to have different sites for different kind of employees and you can also easily do that. So each client can has its own virtual instance and on each of these instances you can have as many different sites with different pages as you need.&lt;/p&gt;
&lt;p&gt;Basically whatever you imagine is probably possible.&lt;/p&gt;
&lt;h4 id=&quot;clustering-support&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#clustering-support&quot; aria-label=&quot;clustering support permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Clustering support&lt;/h4&gt;
&lt;p&gt;As the number of clients increase one server might be not enough. That&apos;s fine though as Liferay has &quot;native&quot; support for clustering. I wrote about clustering in Liferay before in &lt;a href=&quot;/step-by-step-guide-for-creating-clustered-liferay-environment/&quot;&gt;Step by step guide for creating clustered Liferay environment&lt;/a&gt; article and therefore I won&apos;t go into too many details but lets just say it&apos;s simple. Because of the out of the box support for clustering it is not only easy but also gives you different configuration possibilites - you can do it almost automatically with multicast but if that&apos;s not an option (for example because servers are not in the same network or because of the cloud limitations) then you have different options like TCP unicast.&lt;/p&gt;
&lt;p&gt;No matter if you need clusters in Docker, Kubernetes or just few machines in the cloud - you can handle that quite easily.&lt;/p&gt;
&lt;h4 id=&quot;custom-reusable-parts-of-the-page-with-portlets-and-custom-modules&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#custom-reusable-parts-of-the-page-with-portlets-and-custom-modules&quot; aria-label=&quot;custom reusable parts of the page with portlets and custom modules permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Custom, reusable parts of the page with portlets and custom modules&lt;/h4&gt;
&lt;p&gt;Of course Liferay won&apos;t handle all of the business needs you might have now or in the future. In such case you can simply create your own modules in Java. Liferay has support for many things you might need like database access using ServiceBuilder, creating the service layer, creating well structured code base with OSGI modules which also makes it possible to deploy modules without the need of restarting anything. You also have different possibilites of creating the presentation layer.&lt;/p&gt;
&lt;p&gt;Liferay uses portlets which are modules that can be put on any page of your site and they act as a frontend which your users will see. Portlets are standarized thing in Java, described by JSR-168, JSR-286, JSR-362 specifications. Because of this it&apos;s also possible to use for example Spring MVC portlets.&lt;/p&gt;
&lt;p&gt;Basically creating modules is a huge topic but lets just say you can do quite a lot.&lt;/p&gt;
&lt;h4 id=&quot;extending-liferay&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#extending-liferay&quot; aria-label=&quot;extending liferay permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Extending Liferay&lt;/h4&gt;
&lt;p&gt;In some cases it also might be crucial to extend your Liferay to your needs. For example you might need to change some translations, modify view just a little bit or add some extra logging when user login or logout. There are tons of different scenarios such as these and actually Liferay supports extending quite well. You don&apos;t need to modify framework code but instead you have different ways of extending or modifying your Liferay in a way that won&apos;t break once Liferay is updated. Because of that it&apos;s not only easy but can be done in a safe way.&lt;/p&gt;
&lt;p&gt;There are also some extension points which are available directly in the control panel - for example you can add a custom field to things like Users or Sites and then you can use these custom fields in your custom code to meet your needs.&lt;/p&gt;
&lt;h3 id=&quot;disadvantages&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#disadvantages&quot; aria-label=&quot;disadvantages permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Disadvantages&lt;/h3&gt;
&lt;h4 id=&quot;complexity&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#complexity&quot; aria-label=&quot;complexity permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Complexity&lt;/h4&gt;
&lt;p&gt;Liferay is great because of number of options it gives to you but because of this it&apos;s also more complicated. Like I mentioned in the beginning - you probably don&apos;t want to use Liferay if you just need a site with few static pages.&lt;/p&gt;
&lt;p&gt;Of course if now you only have few pages but you plan to grow then you might want to start right away with CMS like Liferay, just keep that in mind and if you can consult your decision with someone more experienced.&lt;/p&gt;
&lt;h4 id=&quot;server-resources&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#server-resources&quot; aria-label=&quot;server resources permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Server resources&lt;/h4&gt;
&lt;p&gt;For a simple site (like this blog) you don&apos;t need too much server resources. Even something like 300mb of memory might be sufficient for you needs. You can even just buy a server with already installed Wordpress or has a script for installing it with few clicks. In Liferay you will need much more resources even for an empty portal which means more expensive server. Also you will need to configure it on the server yourself, along with the database or you need to pay someone to do it for you.&lt;/p&gt;
&lt;p&gt;Again, these server costs are not that big but it&apos;s just another reason why for simple site something else might be enough and cheaper.&lt;/p&gt;
&lt;h4 id=&quot;wordpress-like-pluginsthemes&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#wordpress-like-pluginsthemes&quot; aria-label=&quot;wordpress like pluginsthemes permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Wordpress like plugins/themes&lt;/h4&gt;
&lt;p&gt;If you have used Wordpress before (or any other popular CMS) you might know that you can do tons of things with already existing plugins. A lot of these plugins are also free which is also great. That&apos;s sadly not a case with Liferay. There are tons of already available options so for many cases you will be fine but if you wanted something less popular (lets say to add some SEO stuff) then in Wordpress you had a chance to find a plugin which does that for you. The marketplace in Liferay on the other hand is quite small so if something isn&apos;t available in Liferay then you will probably not find an existing plugin for it. In such scenario you need to find someone who will write required module for you.&lt;/p&gt;
&lt;p&gt;The same thing is true for themes. For Wordpress you will find thousands of them, a lot of which are free. In Liferay you will only find few.&lt;/p&gt;
&lt;p&gt;But like I mentioned before, Liferay is not meant for simple pages so custom development is most likely thing that you just need to have on your mind.&lt;/p&gt;
&lt;h2 id=&quot;summary&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#summary&quot; aria-label=&quot;summary permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Summary&lt;/h2&gt;
&lt;p&gt;To sum it up. In many cases you should consider using CMS for your website. Depending on your needs it might be the best to use simple one like Wordpress or Joomla or you might consider something more advanced like Liferay, Kentico or Adobe Experience Manager.&lt;/p&gt;
&lt;p&gt;Each of the solutions has its own advantages and disadvantages. I hope I gave you at least a little understanding why you should also consider Liferay in your project. Of course I mentioned just a few properties but there are much more and there are many uses cases for it. Sometimes you will use as a bank site, sometimes you will just keep content in it and use it as a Headless CMS and sometimes you might want to develop a whole SaaS with it. Because of so many uses cases and options in Liferay it would be hard to list them all and therefore it&apos;s always good to ask someone more experienced to help you find a tool that will work best in your scenario.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Step by step guide for creating clustered Liferay environment]]></title><description><![CDATA[Hello! In this article I would like to show you how to create clustered Liferay environment. In my example environment I will use docker but the steps are similiar if you're installing two (or more) instances on one or many servers. I will try to show you step by step how you can build the environment yourself so you can understand the process better. Of course if you want to you can go to the final version of code right away - it's on my git. I encourage you to go through the steps and learn…]]></description><link>https://pydyniak.com/step-by-step-guide-for-creating-clustered-liferay-environment/</link><guid isPermaLink="false">https://pydyniak.com/step-by-step-guide-for-creating-clustered-liferay-environment/</guid><pubDate>Tue, 26 Jan 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Hello! In this article I would like to show you how to create clustered Liferay environment. In my example environment I will use docker but the steps are similiar if you&apos;re installing two (or more) instances on one or many servers.&lt;/p&gt;
&lt;p&gt;I will try to show you step by step how you can build the environment yourself so you can understand the process better. Of course if you want to you can go to the final version of code right away - it&apos;s on my &lt;a href=&quot;https://github.com/RafalPydyniak/liferay-docker-cluster&quot; target=&quot;_self&quot; rel=&quot;nofollow&quot;&gt;git&lt;/a&gt;. I encourage you to go through the steps and learn something new though!&lt;/p&gt;
&lt;p&gt;Important thing - I will focus on simplicity and on Liferay configuration so I will not create database/Elasticsearch/loadbalancer cluster. If you need those then it&apos;s quite simple to add that to the basic configuration I will show you and of course you will find tons of resources on these topics. Instead I will just show you the basic configuration for database/ElasticSearch and load balancer.&lt;/p&gt;
&lt;h2 id=&quot;prerequisites&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#prerequisites&quot; aria-label=&quot;prerequisites permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Prerequisites&lt;/h2&gt;
&lt;p&gt;To start you will need Docker installed on your local machine (or server or any other place where you want to start your cluster environment).&lt;/p&gt;
&lt;p&gt;I won&apos;t go through the basics of containers or docker so it would be good if you knew the basics but I guess anyone can follow.&lt;/p&gt;
&lt;p&gt;If you plan to create your environment while reading then you can also create a new directory with files/directories like on the screen below (the files can be empty at this stage):&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/Ca1DAAAACXBIWXMAAA7DAAAOwwHHb6hkAAABqklEQVR42p2SW4vaUBSF/fl96Z/oe6F0plAcsGKt09toRxNNNNHc7wlOknE0MX49Ci10ah/qDusENoePtfY+LUQdqoSm+MGvqquK/b7hkmodj6a4p3FeEHoWw7vvfGx3aN90MU3zdOlwOPwncOtQp23hakdd1+yrmjiO2WyeLnRYirjhS2xNpdvr0+/0GMuyAG5+O2ya5vR/rrPA/SZkG/V4KjKSLCUKE7IsoxKzvMjhzLhn9PUVvqajzDV0dY4qFIYR2kJjoapMJxKqoqIvl5iGiTSZUuTFXzM+ASX9C4O715TrHMM08F0HwzAoyhLP9YXjgMVUEUuyMC2b1dLAtV1229154G5bkaZrQgFyHI84ivC9kLk8xbJckjgh8H2yNBVKKIryiPl35LzIcT2PNAqR5ZmIqzKeTFBkBUlS8LyA2/6A8VhGFn15prJvxFJOH3+odXy+wXrDyvZQrYS598AqLtGDQoxC9JwHNDdlrAcsoxI33xFvwS/P6wR0s0dsEXE4XfGu+432pxFXnVuu2wOuP4zoD2e8eT/grejdfJZwH8HODzhiJ8/1E+mkQelILQSfAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Initial files structure&apos; title=&apos;&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; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Initial files structure&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;And in the deploy directory you can create two empty directories called &quot;portal-node-1&quot; and &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,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAYAAABiDJ37AAAACXBIWXMAAA7DAAAOwwHHb6hkAAABgUlEQVR42q2SbUsbQRSF9y+3P0Zq1QrSStSAoUYkbaSBqqBBCSYlFpqoH9SYjdnsJjv7MrPzOLuJUYoRRC9c7jAv555z5lpXN11al9fc9Qfc2nf0nCFSKl4bWussrc5tj8OTBvvVOu2LSw6Oz3BHwfgSrw/rscO4JpNOD4D6PxZP67OAWicoOSRR4ZT2W8LS0kXdfEL1f062TAOVIHyB73kEYYROkuzEcz3jr8RzXHwRzACMbGTrA9gL1I6qbK6tslMq8/XzF3LfVjlttijl85Qrv5ifW6DwfYvz5j865gOfk29pJYh7JRK3Sq/bNSxcojgmFCKr6YPIsBFBQBiGSDWZAD1LsjL+2TnU4ODB+kxiHEVEBiSK5ZRFYGzQiTbAgtFIvCT5o5G8SOOkRnFzgx+7FXLLK+Rz69TP2pQLBSp7v1maX2Z7e4c/jRrNv+ezJPtIu4h29+lcXWMb2b7v4/YdPG+YfdDI2JCuHWdAMJGeMn9xDt8rxoBmFtN86wymgPeNoJvBU7rpxQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Deploy directory initial files structure&apos; title=&apos;&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; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Deploy directory initial files structure&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;Or you can check the GIT repository instead if you prefer to see the end result from the beginning.&lt;/p&gt;
&lt;h3 id=&quot;docker-storage---mounting-volumes&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#docker-storage---mounting-volumes&quot; aria-label=&quot;docker storage   mounting volumes permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Docker storage - mounting, volumes&lt;/h3&gt;
&lt;p&gt;After publishing the original post I thought about one more thing - in below instructions I use Docker volumes for holding some data (elasticsearch and Liferay&apos;s documents repository) and I mount some other (deploy directories) and I also copy some (configs, jars, portal-ext.properties) in Dockerfile. Also for the MySQL I don&apos;t save the data anywhere actually.&lt;/p&gt;
&lt;p&gt;This approach gives you an overview of different methods you can use but there&apos;s a catch. If you plan to create production environment based on the instructions below then you should understand data storage in Docker first! I believe the best place for that is &lt;a href=&quot;https://docs.docker.com/storage/&quot; target=&quot;_self&quot; rel=&quot;nofollow&quot;&gt;Docker documentation&lt;/a&gt;. It&apos;s really crucial as all types of data storage methods in Docker have their pros and cons.&lt;/p&gt;
&lt;h2 id=&quot;process-of-building-the-environment&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#process-of-building-the-environment&quot; aria-label=&quot;process of building the environment permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Process of building the environment&lt;/h2&gt;
&lt;h3 id=&quot;liferay-containers&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#liferay-containers&quot; aria-label=&quot;liferay containers permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Liferay containers&lt;/h3&gt;
&lt;p&gt;Since we want to have clustered Liferay environment we need two or more Liferay instances. I will go for two but it shouldn&apos;t be a problem for you to extend that number.&lt;/p&gt;
&lt;p&gt;So how do we start? Well since we want to use docker compose we could create a simple docker-compose.yml file with description of two Liferay containers but I won&apos;t do that. Instead I will start with &quot;Dockerfile-liferay&quot; with following content:&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;So basically we create our own image from existing one (official Liferay 7.0.6 GA7 image). In our version of image we copy portal-ext.properties to LIFERAY_HOME directory and also config files and some jars. We will need that later. Of course we could map our local disc to docker disc instead but since these files are not going to change we will do it this way. Perhaps the portal-ext.properties is an exception as it changes more often but I will leave it this way for now.&lt;/p&gt;
&lt;p&gt;If you want you can test it now. To do that build that image, start it and then connect to the container to see if the files were indeed copied and the directory was created. Following commands will help us with that:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker build -t my-liferay-7-tutorial -f Dockerfile-liferay . # builds the &quot;Dockerfile-liferay&quot;. The created image will be &quot;my-liferay-7-tutorial&quot;. The dot at the end is for pointing the context directory
docker run -d my-liferay-7-tutorial # we run our image
docker exec -it CONTAINER_NAME /bin/bash # we enter the container so we can test if everything is there. You can find the CONTAINER_NAME using docker container ls command
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;use-our-images-in-docker-composeyml&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#use-our-images-in-docker-composeyml&quot; aria-label=&quot;use our images in docker composeyml permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Use our images in docker-compose.yml&lt;/h3&gt;
&lt;p&gt;The next step is to create our Liferay containers (instances) definition in docker-compose.yml. The first scratch of the docker-compose.yml is below:&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;So what we did there? Well we defined two instances of Liferay &quot;liferay-portal-node-1&quot; and &quot;liferay-portal-node-2&quot;. We use our dockerfile we created before as the image and use current directory as a context path. We also expose ports from our container so they can be accessed outside the container. So we expose port 8080 to 6080 (node-1) and 7080 (node-2) and we also expose the gogo shell port.&lt;/p&gt;
&lt;p&gt;Then we set the hostname so we can distinguish our instances and then we use volumes to do two things:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;persist the documents &amp;#x26; media data to docker volume (defined at the end of the docker-compose). This way we have shared data directory between two instances as we use the same volume and that is what we need according to the documentation - all instances should share the same document library.&lt;/li&gt;
&lt;li&gt;map our local deploy/portal-node-X directory to the deploy directory of the container. This way we can easily deploy our modules&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;At the end we also define our volumes - for now it will only be &quot;lfr-dl-volume&quot;.&lt;/p&gt;
&lt;p&gt;You can also notice the &quot;depends_on&quot; fragment. This is just a declaration on what containers our container depends on. We define that so for example Liferay instance is not started before Elasticsearch or our Database which would result in errors. Of course we don&apos;t have Elasticsearch or mysql container defined yet but we will get to it soon.&lt;/p&gt;
&lt;h4 id=&quot;documents--data&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#documents--data&quot; aria-label=&quot;documents  data permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Documents &amp;#x26; data&lt;/h4&gt;
&lt;p&gt;It&apos;s important to add that for production purposes you might want to use &quot;Advanced File System Store&quot; data repository instead of the simple (default) one. In this case I used the default one and the default location to keep it simpler as changing that is not requirement for clustering (which is the main topic of this article). For more details about data repositories you can check this &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;article&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;database-configuration&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#database-configuration&quot; aria-label=&quot;database configuration permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Database configuration&lt;/h3&gt;
&lt;p&gt;For any Liferay environment we need a database (we don&apos;t want to use embedded H2). I won&apos;t go into too many details - the block for docker-compose.yml looks like that:&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;As you can see I&apos;m using mysql but you can use any other database. You will easily find images on docker hub for example for postgres.&lt;/p&gt;
&lt;p&gt;We also need a defined connection to our database. To do that we will use portal-ext.properties. You might remember that while creating our Liferay dockerfile we included a statement that copies our portal-ext.properties from configs directory to the server. This is the place we can add our database connection declaration. It will look like below:&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;Of course you can also add your own properties like you would do in any other case. We will also add some more properties later when we get to cluster configuration.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; I actually don&apos;t store the mysql database data anywhere so it won&apos;t be persistent - if you remove the container (or use docker-compose down) the database data will be lost. To avoid that you can simply use volume (see elasticsearch config for reference) or create another directory and map it there like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;volumes:  
- ./data/mysql:/var/lib/mysql
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;elasticsearch-configuration&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#elasticsearch-configuration&quot; aria-label=&quot;elasticsearch configuration permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Elasticsearch configuration&lt;/h3&gt;
&lt;p&gt;One of the steps for clustering described in Liferay documentation is installing an instance of Elasticsearch. Since we&apos;re clustering we can&apos;t use the embedded one as all of our Liferay instances need to communicate with same Elasticsearch. In our case we want to use 2.4.6 version of Elasticsearch but in your case you might need another version - it all depends on the Liferay version you choose to use. You can refer to &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;this&lt;/a&gt; article for informations how to find the correct Elasticsearch version for your Liferay.&lt;/p&gt;
&lt;p&gt;After you find the Elasticsearch version that will work for you, you need to add it to your docker-compose.yml. In my case it looks like below:&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;So again - first we define image we want to use. Then we have some additional configuration options which are either self explanatory (like memory args) or you can find their meaning in google (especially for ulimits). The only important setting connected directly to Liferay clustering is the &quot;cluster.name&quot; - this has to be something unique for your cluster environment but it can be any string. In our case it will be unique as our environment is quite simple but sometimes you might have for example DEV and TEST environment on same server and then you need unique cluster.name property to distinguish them.&lt;/p&gt;
&lt;p&gt;You can also notice that we use &quot;esdata1&quot; docker volume so we need add its definition to the end of docker-compose. Our volumes definition will look like below:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;volumes:
  lfr-dl-volume:
  esdata1:
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;using-remote-elasticsearch-in-liferay-instances&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#using-remote-elasticsearch-in-liferay-instances&quot; aria-label=&quot;using remote elasticsearch in liferay instances permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Using remote elasticsearch in Liferay instances&lt;/h3&gt;
&lt;p&gt;Now that we configured our Elasticsearch server we need to actually use it in Liferay. To do that we will usage OSGI&apos;s configuration file. Lets  create a file called &quot;&lt;em&gt;com.liferay.portal.search.elasticsearch.configuration.ElasticsearchConfiguration.config&lt;/em&gt;&quot; in the &quot;configs&quot; directory. Then put the following content inside:&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;The code above just sets the Elasticsearch mode to REMOTE and sets the address and clusterName. You might wonder why we use &quot;es-node-1&quot; as host instead of something like &quot;127.0.0.1&quot; or &quot;localhost&quot;. We do that because the services we define in docker-compose are in the same docker network and they&apos;re reachable by other containers by their container name. The binding is handled by the docker itself - it basically adds required entries in /etc/hosts and the container name is just an alias there.&lt;/p&gt;
&lt;p&gt;Also note that the &quot;clusterName&quot; must correspond to the name you defined in Elasticsearch service definition.&lt;/p&gt;
&lt;p&gt;We don&apos;t need to do anything else because in the Dockerfile we declared before we already copy all &lt;em&gt;.config&lt;/em&gt; files to LIFERAY_HOME/osgi/configs directory.&lt;/p&gt;
&lt;h3 id=&quot;using-multiple-version-of-bundles&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#using-multiple-version-of-bundles&quot; aria-label=&quot;using multiple version of bundles permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Using &quot;multiple&quot; version of bundles&lt;/h3&gt;
&lt;p&gt;In Liferay 7.0 CE which we use in this article we need to do another step described &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;there&lt;/a&gt;. Basically this version by default is not ready to be run in clustered mode because it only contains &quot;non-clustered&quot; version of three modules:&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;We need to disable them and instead use the &quot;multiple&quot; versions of these. The article linked above describes the steps for obtaining (compiling) them so I won&apos;t go into too many details here. I will just describe what to do with them once you get them.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; if you use the 7.0.6 version of Liferay you can just get these from the git repository for this article.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note2&lt;/strong&gt;: you might not need that step at all if your version already supports multiple module versions. This is true if you use DXP version or if you use newer version of Liferay - just run the gogo shell command in your Liferay &quot;lb portal.scheduler&quot; and see if there is enabled module called &quot;Liferay Portal Scheduler Multiple&quot; - if yes then you don&apos;t need to do that step&lt;/p&gt;
&lt;p&gt;Once you have your multiple modules you need to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;deploy them - just put them in &quot;configs&quot; directory&lt;/li&gt;
&lt;li&gt;ignore the .single version modules - to do that create new file called &quot;com.liferay.portal.bundle.blacklist.internal.BundleBlacklistConfiguration.config&quot; in configs directory with following content:&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;blacklistBundleSymbolicNames=&quot;com.liferay.portal.cache.single,com.liferay.portal.cluster.single,com.liferay.portal.scheduler.single&quot;&lt;/p&gt;
&lt;p&gt;The Dockerfile-Liferay we defined before will do the rest i.e. copy them to approriate directories.&lt;/p&gt;
&lt;h3 id=&quot;enabling-the-cluster-mode&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#enabling-the-cluster-mode&quot; aria-label=&quot;enabling the cluster mode permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Enabling the cluster mode&lt;/h3&gt;
&lt;p&gt;We are almost done but we still didn&apos;t tell Liferay that we want to use clustered environment. How do we do that? Well it&apos;s quite simple actually - all we need is to add following line into our 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;But hey - is that that simple? Well actually it is - at least in some cases. Basically this property makes Liferay to use JGroups channel to communicate with each other on default ips and ports using UDP multicast connection. Therefore it will only work if the Liferay instances are able to communicate with each other through multicast - this is not always the case. Even if you have servers on the same subnet this still might not work as for example Azure doesn&apos;t support multicast in virtual networks and they&apos;re not planning to add (last time I checked). In our case it will work though as we use docker-compose on one machine and docker will provide multicast connection. If you need other solutions then you can check Liferay documentation where other method is described (unicast over TCP).&lt;/p&gt;
&lt;p&gt;For our environment this is enough though. We don&apos;t need to do anything else but you might also want to add one more property:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;web.server.display.node=true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will add displaying a message with current Liferay&apos;s node. This is useful for testing purposes so I encourage you to do that. Of course you might want to add other properties you need.&lt;/p&gt;
&lt;p&gt;You might want to check the &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;Liferay docs&lt;/a&gt; to read how exactly the JGroup communication works. The idea is quite simple and clever and it should take you just a couple of minutes to read that and understand the basics.&lt;/p&gt;
&lt;h3 id=&quot;load-balancing&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#load-balancing&quot; aria-label=&quot;load balancing permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Load balancing&lt;/h3&gt;
&lt;h4 id=&quot;initial-note&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#initial-note&quot; aria-label=&quot;initial note permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Initial note&lt;/h4&gt;
&lt;p&gt;Please note that this step is totally optional. You might already have a load balancer or HTTP server (like Apache or Nginx) installed on your server and then you can&apos;t declare another one here and instead you should do the load balancing in your current application or ask your server admistrator to do that for you. If you want (or need) to do it by yourself you can always google it like &quot;Load balancing Apache2&quot; or &quot;Load balancing HAProxy&quot; etc. You can also follow steps below for HAProxy configuration on docker but the steps are similiar if you have it installed as a regular service.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Important&lt;/strong&gt; you need to have sticky session in your load balancer. This is important as session is by default not shared between cluster nodes so if the user gets redirected to another node he will be logged out. This might be an issue if you use Nginx (which is really popular) as by default sticky sesions are available only in paid version of the app as far as I&apos;m concerned.&lt;/p&gt;
&lt;h4 id=&quot;docker-config&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#docker-config&quot; aria-label=&quot;docker config permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Docker config&lt;/h4&gt;
&lt;p&gt;So the last thing we need is load balancing between our servers. We obviously don&apos;t want user to pick the server he wants to enter himself. Instead we want to have one url like &lt;em&gt;app.pydyniak.com&lt;/em&gt; (nothing there so you don&apos;t need to check :)) which picks one of the servers automatically. The digram below shows the basic idea:&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/tAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAyUlEQVR42pWSCaqEQAxEvf/9BE/gvuG+1/ACgf8bhJlAUDvVlaTKaN93TdOkZVnsSV7XpTDu+9YwDFrX1bCODyNqmkZZlqmua7VtqzRNlee5tm3TPM92EZKu69T3vWVRFBrH0Wo0+kd4HIcVfTpAnD3PY+kXwFRVZXXeaUbjcJtIXwSkNAun8S1+JvSpv6m9EgI6z9OMQDf/DjGYihSsDvaVkFXQqixLM8wNItDWp8MstIQsSZJ3QjQDxAXSXf1LyBk1/hRqcRzrAzKvcclecX2gAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Digram of the architecture&apos; title=&apos;&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; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Digram of the architecture&lt;/figcaption&gt;
  &lt;/figure&gt;
In the example I will use HAProxy but you can use whatever you want as this step is totally independent from Liferay (see the &quot;Note&quot; above).&lt;/p&gt;
&lt;p&gt;So first we want to build another Dockerfile. Lets call it &quot;Dockerfile-haproxy&quot; and copy the content inside:&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;There is nothing special in this code. We just copy our config file to HAProxy and this is actually the recommended way shown on Docker Hub. Now lets define our service in 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;We point to our dockerfile and use the current directory as context. We also expose required port and set hostname. I believe it&apos;s quite clear.&lt;/p&gt;
&lt;p&gt;We also need to fill the haproxy.cfg file we defined in our Dockerfile so we create &quot;haproxy.cfg&quot; inside configs directory and use following configuration:&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;This is quite a long configuration but what is important is that it does what we wanted. The highlighted lines are the core ones. Basically in these line we do two things:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Set the mode to roundrobin - it&apos;s just one of &quot;ways&quot; of distributing traffic between servers. You will easily find that on Wikipedia for example&lt;/li&gt;
&lt;li&gt;Define the servers available int he cluster and also implementing &quot;sticky session&quot;. Sticky session means that if an user enters the site and for example enters the NODE2 he will stay on the same node for the whole session. This is required as the session is not shared between nodes. To understand better how exactly the sticky session is handled you can check &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;this article&lt;/a&gt; on HAProxy site&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;lets-run-our-liferay-clustered-environment&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#lets-run-our-liferay-clustered-environment&quot; aria-label=&quot;lets run our liferay clustered environment permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Lets run our Liferay clustered environment&lt;/h2&gt;
&lt;p&gt;Finally we&apos;re done. We defined everything we need and I hope you understand why we did what we did. Now we need to start the environment. We can do that by executing following commands:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker-compose build
docker-compose up -d liferay-portal-node-1
# wait until it starts. You can also check docker-compose logs to validate that
docker-compose up -d liferay-portal-node-2
docker-compose up -d haproxy
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;During start of the servers we can see in logs (&lt;em&gt;docker-compose logs&lt;/em&gt;) that indeed the nodes connect to each other. You can look for message like below:&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;If you then enter the server on :80 port you will see that you&apos;ve been connected to one or two nodes (of course if you added property for displaying that message):&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: 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+WAAAACXBIWXMAABJ0AAASdAHeZh94AAAApUlEQVR42l1OXQ+CMBDj//89I6BiINEM2FiIbAzHS7274WJ8aNrr9T4K4yKsT5h+mP1cH3r602luF88cKB7zG/XgcdMr2imgHj3uJqC1AQ3xdVylbnSQTGc38pN3oWzZO9H9EqFetPA5bzgrh1JxY5UA63pwqEhXxN9BRkdHK3qAfX7kpBY5PNDCnheyMG6Hodc1syCSjrnW7sgQxiX5OetTn/cwPpkJLqUaKTBkAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Message which tells us on which node we are&apos; title=&apos;&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; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Message which tells us on which node we are&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;You can also try to connect to the same server from different browser - you will most likely get connected to the second node - if that&apos;s the case then congratulations. You can&apos;t tell right away that everything works because we only know that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;In theory the connection works between nodes looking at the logs&lt;/li&gt;
&lt;li&gt;The HAProxy seems to be working - you can also verify the cookie to see that indeed we have server information there&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But in real case scenario you would need to perform some more tests:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If the elasticsearch works - if you add some content on node1, can you search for it on node2?&lt;/li&gt;
&lt;li&gt;Does the connection indeed works? If you add some content on node2, can you see that content on node1?&lt;/li&gt;
&lt;li&gt;Are the documents &amp;#x26; media shared? If you add some document on one of the nodes, can you see it on another one?&lt;/li&gt;
&lt;li&gt;If you stop one of the nodes, is everything fine?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And of course you might come with your own tests you would like to perform especially if that&apos;s the production server.&lt;/p&gt;
&lt;h3 id=&quot;performance&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#performance&quot; aria-label=&quot;performance permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Performance&lt;/h3&gt;
&lt;p&gt;One thing you might notice is that the servers are not too fast. That&apos;s because we didn&apos;t set memory settings. The simplest way to do that is probably to create configs/setenv.sh. Inside that file you would need to tune the memory settings (override default content). Then you would need to add a COPY in your Dockerfile-liferay. You need to copy that to /opt/liferay/tomcat-8.0.32/bin/ ($LIFERAY_HOME/tomcat-8.0.32/bin/). You can check the git repository where I added that. Please note that you might also need to change the memory settings for your docker so it has enough memory (and CPU).&lt;/p&gt;
&lt;h2 id=&quot;conclusion&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#conclusion&quot; aria-label=&quot;conclusion permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;I hope the article was clear enough. The article was quite long but mostly because I tried to explain a little bit why I did things the way I did. The docker project for that environment is quite small though and you can of course check that on &lt;a href=&quot;https://github.com/RafalPydyniak/liferay-docker-cluster&quot; target=&quot;_self&quot; rel=&quot;nofollow&quot;&gt;git&lt;/a&gt;. In this article I did cluster on 7.0.6 but steps are pretty much the same for newer versions so if you need for example 7.3 then you will probably just do some changes in Dockerfile-Liferay and that should be enough.&lt;/p&gt;
&lt;p&gt;If you have any questions feel free to ask either in comments or by &lt;a href=&quot;https://pydyniak.com/contact/&quot; target=&quot;_self&quot; rel=&quot;nofollow&quot;&gt;contacting me&lt;/a&gt;.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Why migrating Liferay projects to a newer version is not easy v2]]></title><description><![CDATA[Hello! In first part Why migrating Liferay projects to a newer version is not easy I introduced you to the topic of Liferay migrations and I showed you a few issues you might encounter during database migration process. In this part I'd like to show you few issues you might have during migration of the code of your custom modules for example portlets.]]></description><link>https://pydyniak.com/why-migrating-liferay-projects-to-a-newer-version-is-not-easy-v2/</link><guid isPermaLink="false">https://pydyniak.com/why-migrating-liferay-projects-to-a-newer-version-is-not-easy-v2/</guid><pubDate>Sun, 10 Jan 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Hello! In first part &lt;a href=&quot;/why-migrating-liferay-projects-to-a-newer-version-is-not-easy/&quot;&gt;Why migrating Liferay projects to a newer version is not easy&lt;/a&gt; I introduced you to the topic of Liferay migrations and I showed you a few issues you might encounter during database migration process. In this part I&apos;d like to show you few issues you might have during migration of the code of your custom modules for example portlets.&lt;/p&gt;
&lt;h2 id=&quot;how-to-migrate-the-code&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#how-to-migrate-the-code&quot; aria-label=&quot;how to migrate the code permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;How to migrate the code&lt;/h2&gt;
&lt;p&gt;For starters - how do you even start the code migration process? Well first of all if you decided to migrate your project to higher version you should start with the &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;official documentation&lt;/a&gt;. There are few options how do you even start migration - for example you will start differently if you&apos;re migrating from 6.2 (or earlier) and differently if you&apos;re already on version 7.x. Of course the general rule is if you have higher version already then the migration will go easier for you. Also if you use more Liferay&apos;s API then it will be harder. Anyway you should start with documentation because it should give you some idea of how it all goes. Obviously after going through that documentation you might feell a little overwhelmed because there&apos;re a lot of informations but don&apos;t be too stressed about it, it won&apos;t be that bad!&lt;/p&gt;
&lt;h2 id=&quot;issues-that-we-can-encounter&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#issues-that-we-can-encounter&quot; aria-label=&quot;issues that we can encounter permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Issues that we can encounter&lt;/h2&gt;
&lt;p&gt;Lets go back to the main thing I wanted to share with you in this post. What can go wrong while migrating the code? Well the biggest issue is that not everything is documented well enough. You can find so called &quot;Breaking changes&quot; pages for each version but sadly not all informations are there and because of that you might find yourself in a place that something stopped working and you can&apos;t find a clue why. What can we do? Well I think the only thing you can do is to just go through the migration process and fix unexpected and undocumentted issues one by one. Either by searching them on Liferay forum, or in Liferay blog or just by debugging the Liferay code (I hope it won&apos;t be neccessary for you but you never know)&lt;/p&gt;
&lt;p&gt;The good news is that I want to show you four issues you might encounter so you can learn something on my mistakes instead on your owns :) These are not all issues I&apos;ve encountered but I hope that will help you anyway. I also would like to remind you that I wrote about some other issues in previous post.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note!&lt;/strong&gt; I found these issues some time ago. It might happen that when you read that (or even at the publication date) some of them are already in breaking changes already - I haven&apos;t checked that. This could&apos;ve happened though as even I wrote some posts in Liferay forum so perhaps someone added that at some point.&lt;/p&gt;
&lt;h3 id=&quot;web-contents-fetching-images-api-change&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#web-contents-fetching-images-api-change&quot; aria-label=&quot;web contents fetching images api change permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Web Contents fetching images API change&lt;/h3&gt;
&lt;p&gt;I&apos;d like to start by showing you how the XML for image looked like in Web Content in version 7.0:&lt;/p&gt;
&lt;pre&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; [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;/pre&gt;
&lt;p&gt;And then the XML for same (or almost the same) Web Content in version 7.2&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x3C;dynamic-content [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;/pre&gt;
&lt;p&gt;Do you see any difference? Well you could notice that there is no path of the file now. Instead of that we have JSON with few informations about our image. What&apos;s the big deal you might ask? Well the thing is that common practise in 7.0 was to fetch the path of the image in following way:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;final com.liferay.portal.kernel.xml.Document document = SAXReaderUtil.read(article.getContentByLocale(locale.toString()));
final String imageUrl = document.valueOf(&quot;//dynamic-element[@name=&apos;Image&apos;]/dynamic-content&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you used the same in 7.2 you&apos;d end up with fetching JSON instead of the image path so if you use that as your img source it will look like that:&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: 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,iVBORw0KGgoAAAANSUhEUgAAABQAAAAFCAYAAABFA8wzAAAACXBIWXMAABJ0AAASdAHeZh94AAAA/UlEQVR42o1Py0rEQBDM//+JIi7qCqKsWbKrhJj3JJnMe7Jnk/gHZU8E8eihqeqqoqmO5s8Z67pi/Vo3XJZ5m8CDN89hX34y/5ioiiuk1+/I9zlEV6JJHtFlCThj+KgVslLAWg3rJ0IH69wftIQezvtfHqleYCw5hpTDSA49tFBdBWckhmEEFxqTM7ByJE3BKgEfjhF3hEEzwbNmw6g+3KBPX8BOD2iO96gPtxiyIx1uwLMYbbzb9Ca+Q/l8BXZ+gh578PyM+nWHNtkTP0FSCV68IZKsgBEcsi0QuOobeHrBO0sNLFRoTJrTCpozakKvSbE1Cp4euy0fstPlgm8rNmKZv6uLtwAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Screen showing the JSON inside img tag&apos; title=&apos;&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; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Screen showing the JSON inside img tag&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;In order not to let it happen you need to adjust your code for example in following way:&lt;/p&gt;
&lt;pre&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;/pre&gt;
&lt;p&gt;Please note you could have similiar situation in your FTL template but it should be easy for you to translate above Java code to FTL one.&lt;/p&gt;
&lt;p&gt;It&apos;s worth to read &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;that blog&apos;s entry in Liferay forum&lt;/a&gt; where you can read that this change was made because the Liferay developers thought not one uses this path. Unfortunately there&apos;s no better alternative (or wasn&apos;t back then). The Liferay developers didn&apos;t provide neat API you could use to fetch the Web Content&apos;s image so the above code was used - it was the solution you could found on Liferay&apos;s forum for example.&lt;/p&gt;
&lt;p&gt;Anyway just keep that thing in mind as the images will just stop working - no error or anything in your IDE.&lt;/p&gt;
&lt;h3 id=&quot;the-fields-in-search-api-change&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#the-fields-in-search-api-change&quot; aria-label=&quot;the fields in search api change permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;The fields in Search API change&lt;/h3&gt;
&lt;p&gt;In Liferay 7.2 (or 7.1) there was also a change in the naming of fields in Search API. The change was that in 7.0 the field name in&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;com.liferay.portal.kernel.search.Document
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Looked like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ddm__keyword__328630__PublicationDate_en_US_String_sortable
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In 7.2 on the other hand it looks like that:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ddm__keyword__328630__PublicationDate_String_sortable
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;See the difference? Well the _en_US part is gone. Of course it makes sense - we don&apos;t translate the PublicationDate field so why would we want to add the language key. Unfortunately you might&apos;ve had a code that used the Search API and you used the name of the fields directly. You can look for such code by looking for &quot;IndexSearchHelper&quot;, &quot;Document&quot; or &quot;SearchContext&quot; keywords and the example of such code could look like that:&lt;/p&gt;
&lt;pre&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;/pre&gt;
&lt;p&gt;Where our pattern is&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private static final String PUBLICATION_DATE_TERM_FIELD_PATTERN = &quot;ddm__keyword__%s__PublicationDate_%s&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So we can search by the field name (see &lt;em&gt;addRangeTerm&lt;/em&gt;) and pass the field name with the language key part. The code will still compile but the results will be incorrect because we will search based on non existing field name as it was changed.&lt;/p&gt;
&lt;p&gt;I guess this issue might be quite rare as I believe Search API is not that common but if you use that then you might have hard time finding why it stopped working.&lt;/p&gt;
&lt;h3 id=&quot;view-permission-in-web-contents&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#view-permission-in-web-contents&quot; aria-label=&quot;view permission in web contents permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;View permission in Web Contents&lt;/h3&gt;
&lt;p&gt;Another change I couldn&apos;t find in breaking changes.&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: 26.245847176079735%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAFCAYAAABFA8wzAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAmUlEQVR42k1PQQ4EIQjz/68ddTNGENiCYubQFFooWnr/2fsOW0tM1QI0yZgXNADMxLvGzJwz2P2E90QUXFrraPiGmQcuMxGzyYoaAHst8Cb67fmMGq89k34ZY9yrGcqxpNCAw6GFJzcoZg74HCrPU82/7WYGZi2ylzfvg+KHRY/+hftqpdaGF9INyyA313mNc/4gtTz2nXH+AzTfixXhNi4qAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Image showing the permissions for Web Content&apos; title=&apos;&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; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Image showing the permissions for Web Content&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;When you have a Web Content with permissions like on the screen above our Liferay portal will behave differently based on version. This is is connected to one of the Liferay settings available under (in 7.2):&lt;/p&gt;
&lt;p&gt;Control panel → configuration → system settings → web content&lt;/p&gt;
&lt;p&gt;The config is called &quot;Article view permissions check enabled&quot; and with it you can configure either the portal should verify the &quot;View&quot; permission for Web Contents or not. So:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If the the option is off - every one can see our Web Content even though the permissions (look on the screen above) suggest that only Owner should have that access.&lt;/li&gt;
&lt;li&gt;If the option is on - there is a verification and for example Guest doesn&apos;t have access to our Web Content&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The thing is the default value changed between versions. In 7.0 it was &lt;strong&gt;disabled&lt;/strong&gt; by default and in 7.2 it&apos;s &lt;strong&gt;enabled&lt;/strong&gt; by default.&lt;/p&gt;
&lt;p&gt;So it&apos;s a suprise waiting for us - before everyone could see our Web Contents and now no one can. Of course if we defined the permissions then we will be fine but what if we did defined permissions for all web contents but one or two? Well that might lead to some page not working properly and since it&apos;s only one page for example then it can be easily overlooked.&lt;/p&gt;
&lt;h3 id=&quot;changed-way-of-fetching-current-url-in-ftl&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#changed-way-of-fetching-current-url-in-ftl&quot; aria-label=&quot;changed way of fetching current url in ftl permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Changed way of fetching current URL in FTL&lt;/h3&gt;
&lt;p&gt;In 7.0.x we could fetch current URL in FTL files using following code:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;url = request.attributes[&apos;CURRENT_URL&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But after upgrading our Liferay Portal it won&apos;t work any longer. Instead we can use:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;url = portalUtil.getCurrentURL(request)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;it&apos;s a minor thing but undocumented one.&lt;/p&gt;
&lt;h2 id=&quot;is-that-all&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#is-that-all&quot; aria-label=&quot;is that all permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Is that all?&lt;/h2&gt;
&lt;p&gt;Well no. The above issues are just few examples which we might find. But still the issues especially with Search API and fetching the images are the perfect examples showing the unexpected issues that might happen to us during migration process. For contrast I also added two issues that are much easier to find just to show you that not all issues are that hidden (well the Web Content issue might be hard to find in some rare scenarios).&lt;/p&gt;
&lt;p&gt;I hope this post will help you at least a little bit during your migration&lt;/p&gt;
&lt;p&gt;Best regards!&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Why migrating Liferay projects to a newer version is not easy]]></title><description><![CDATA[Hi! In today's post, I would like to talk about the migration of Liferay projects between the versions, and more precisely to present the problems related to this based on my own experience. I personally participated in the migration of several projects - both from version 6.2 to 7.x and from version 7.0 to 7.2 (here additionally from CE version to DXP version). In addition, these migrations covered the whole spectrum of "cases" in terms of size - these were small modules with literally several…]]></description><link>https://pydyniak.com/why-migrating-liferay-projects-to-a-newer-version-is-not-easy/</link><guid isPermaLink="false">https://pydyniak.com/why-migrating-liferay-projects-to-a-newer-version-is-not-easy/</guid><pubDate>Wed, 21 Oct 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Hi!&lt;/p&gt;
&lt;p&gt;In today&apos;s post, I would like to talk about the migration of Liferay projects between the versions, and more precisely to present the problems related to this based on my own experience. I personally participated in the migration of several projects - both from version 6.2 to 7.x and from version 7.0 to 7.2 (here additionally from CE version to DXP version). In addition, these migrations covered the whole spectrum of &quot;cases&quot; in terms of size - these were small modules with literally several portlets, medium projects with several database entities and several dozen portlets, but there was also one project that contained far more than 100 portlets and database entities.&lt;/p&gt;
&lt;p&gt;It is on the basis of these experiences that I would like to show some examples of what problems we may encounter during migration. By doing so, I would like to make you aware of the fact not to believe that you will automatically manage to migrate the project, because that will most probably not be the case. I will say more - I had to debug the Liferay code myself in some cases because unfortunately, in many cases neither the documentation, nor the existing topics in the forum, nor even asking my own question, helped. Of course, if you have DXP versions, there is a chance that you will receive official support, although I know from experience that a small part of portals is based on a paid version which, due to the cost, is quite understandable.&lt;/p&gt;
&lt;p&gt;I warn you straight away that there will not be too much code or real examples in this post because it would just take too much time to recreate them, and the production code at which my problems occurred for obvious reasons I cannot show. Nevertheless, I hope you will be able to get something useful out of the few minutes you spend reading this text.&lt;/p&gt;
&lt;p&gt;Attention: During the writing process, it turned out to be so much material that I decided to split it into two parts. The second part will be coming soon - I simply thought, when writing this post, which was supposed to be a whole, that there was too much of it, and I did not even get to the &apos;main&apos; part that I wanted to write about, namely the migration of modules.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note!&lt;/strong&gt; There is also second part of the article available showing some issues in code migration process. Check this out &lt;a href=&quot;/why-migrating-liferay-projects-to-a-newer-version-is-not-easy-v2/&quot;&gt;Why migrating Liferay projects to a newer version is not easy v2&lt;/a&gt; &lt;/p&gt;
&lt;h2 id=&quot;why-update-liferay-versions-at-all&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#why-update-liferay-versions-at-all&quot; aria-label=&quot;why update liferay versions at all permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;strong&gt;Why update Liferay versions at all?&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;It is worth asking ourselves first - why do we migrate at all? There are a few answers - first and foremost safety. Just look at the so-called &quot;known vulnerabilities&quot; to see that old versions of Liferay contain a lot of shortcomings.&lt;/p&gt;
&lt;p&gt;Another issue may be the novelties that offer newer versions of the technology used. In the case of Liferay, it is perfectly visible on the example of version 6.2 and 7.x (even 7.0) - the leap was huge due to the transition to OSGI, for example.&lt;/p&gt;
&lt;h3 id=&quot;how-does-migration-in-liferay-look-like&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#how-does-migration-in-liferay-look-like&quot; aria-label=&quot;how does migration in liferay look like permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;strong&gt;How does migration in Liferay look like&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;As I wrote earlier - this entry will not be an instruction to carry out the migration because for it is the &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;official documentation&lt;/a&gt;. I&apos;m not hiding the fact that it could probably be presented much better than in the official documentation, but it would be a topic for the whole course rather than a blog post.&lt;/p&gt;
&lt;p&gt;However, if at this point you are not interested in going through the whole documentation, you can still assume that migration comes down to a few points in a nutshell:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Preparation of our portal - all backups, removal of duplicates or unused articles etc.&lt;/li&gt;
&lt;li&gt;Updating the database - using a script&lt;/li&gt;
&lt;li&gt;Migration of our modules to new versions - in case of migration from 6.2 you will most probably also need to break down the application into any modules&lt;/li&gt;
&lt;li&gt;Tests, tests and more tests&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;And I&apos;ll focus on the first three to describe what can happen to us.&lt;/p&gt;
&lt;h3 id=&quot;preparation-of-a-portal&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#preparation-of-a-portal&quot; aria-label=&quot;preparation of a portal permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;strong&gt;Preparation of a portal&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;The preparation of a portal is a step that is recommended in the official documentation, although, does it seems to be possible to skip it? Well, unfortunately, as it turns out, that not always it is possible. As a standard, this preparation of a portal is aimed at saving space and speeding up the migration process, although there is also a second bottom - Liferay does not always deal properly with old references. What is the old reference? For example, it could be some old webcontent that we no longer use and have long forgotten about. At one point, someone deleted the photo that this WebContent was using. It seems that nothing bad can happen because this is standard behavior - after all, nobody analyzes the content of all articles before removing the photo and really nothing bad happens in this case.&lt;/p&gt;
&lt;p&gt;However, we move on to the migration of our portal to a higher version, we have migrated everything, the portal works until one time we download the whole LAR from the server in order to locally restore the portal and... we finish with the so-called &quot;infinite loop&quot;. Sounds incredible? And yet, that&apos;s what happened to me. I&apos;ll add that the portal was originally 7.0.6 CE and migrated to 7.2.1 DXP. You can see the whole story (along with the advice I received) on the &lt;a href=&quot;https://liferay.dev/forums/-/message_boards/message/11966917&quot; target=&quot;_self&quot; rel=&quot;nofollow&quot;&gt;Liferay forum&lt;/a&gt;. I&apos;ll just add that in addition to the solutions proposed there with logs (which helped a lot!) I had to debug the Liferay code.&lt;/p&gt;
&lt;p&gt;And it was only about the removed photo... In the defense of Liferay, I&apos;ll only admit that this case was specific - not always the removed photo will spoil anything (rather never), but from what I&apos;ve noticed only when we upload the photo directly to the content from the editor, skipping documents &amp;#x26; media. Maybe there must be some additional cases because I had a lot of pictures in this migrated portal (thousands), and only one of them turned out to be problematic.&lt;/p&gt;
&lt;p&gt;How to look for such “bad” pictures? Generally, I didn&apos;t know that such a problem exists and so, as I mentioned, I ended up debugging the Liferay code along with reading logs on the DEBUG level (a lot of it) and so I found my broken WebContent. However, if I had more of such cases, most probably I would try some tool for searching &quot;Broken references&quot; - generally it would be to search our site for places where we have broken photos (you know - such a small icon of a photo that didn&apos;t load because of a bad source). Unfortunately, it won&apos;t work if our webcontent is not embedded anywhere - then you could probably write some small program to look for it.&lt;/p&gt;
&lt;h3 id=&quot;updating-the-database&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#updating-the-database&quot; aria-label=&quot;updating the database permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;strong&gt;Updating the database&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;As far as updating the database itself is concerned, I admit it&apos;s not bad. The creators of the so-called &quot;upgrade tool&quot; did a good job because I didn&apos;t encounter any major problems, despite migrating really large databases (one with about 8 virtual instances and the other even with about 20). In fact, I encountered only two problems strictly related to the migration of the database, but they were not tragic - considering how much it could have broken down in WebContents for example&lt;/p&gt;
&lt;h3 id=&quot;jax-rs-configuration-entries&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#jax-rs-configuration-entries&quot; aria-label=&quot;jax rs configuration entries permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;strong&gt;JAX-RS configuration entries&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;In one of the migrated applications (7.0.6 -&gt; 7.2.1 DXP), where the JAX-RS was used (about which in the context of Liferay you can read &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;here&lt;/a&gt;) there was an error at the start of the application and it looked like this:&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: 35.880398671096344%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAHCAYAAAAIy204AAAACXBIWXMAAAsTAAALEwEAmpwYAAAA+klEQVR42o1R2W7DMAzz//9husZnEucyEjtohxwIaynYS1FsE0DQsCFKNMXQ97DWZRhoraHVBWMs6rqGb1v4xmMcRwzDwBxCwDzP2LYN+74zb9vKZ7GkxAKVq+BY2Ga2KMuSB5Do9WbzEAPnHLxvEVPEeZ54L5Hygzaam5VUmQ1sbiShrusQxoAlLTx9XVcGbXQcBz6VSDGymDXXBnT+YaUUM9kkkf+UiDFxo9E2s4YsJW9aZibQoGmaeTOySC5/gxiHCfe7ydAoihK3QkJKg6+b5PvKNng8VpBDWvIviJjTauomf7S/Us1M/9b3lGjgZCnV5/P7Ywjvll+KERRNMgLGDgAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Screenshot of an error when launching a portal with an incorrect entry in the Configuration_ table related to JAX-RS&apos; title=&apos;&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; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Screenshot of an error when launching a portal with an incorrect entry in the Configuration_ table related to JAX-RS&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;It was associated with incorrect entries in the Configuration_ table. They looked more or less like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;liferay.portal.remote.rest.extender.configuration.RestExtenderConfiguration.e7c762dd-9fed-4956-8bbf-a89ec9e3a1b8&lt;/li&gt;
&lt;li&gt;liferay.portal.remote.rest.extender.configuration.RestExtenderConfiguration.factory&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There were more of them (one for &quot;Factory&quot; and one for each registered endpoint). Numbers will also be different (in case of the first one), so the easiest way to find them is with a query:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;select * from Configuration_ WHERE configurationId like ‘%com.liferay.portal.remote.rest.extender.configuration.RestExtenderConfiguratio%’
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Solution? Just delete these entries and use them during the database migration. When we migrate our JAX-RS related modules and run them all will work properly.&lt;/p&gt;
&lt;h3 id=&quot;release_-table&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#release_-table&quot; aria-label=&quot;release_ table permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;strong&gt;Release_ table&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;The second problem I encountered was incorrect entries in the Release_ table, which stores information about application versions or modules. The problem arose when migrating from version 6.2 GA2 to version 7.3 GA1 - the release table did not migrate correctly, although it was enough to compare its contents with the &quot;fresh&quot; database created in 7.3.1 and &quot;swap&quot; the entries and everything worked correctly.&lt;/p&gt;
&lt;h3 id=&quot;in-conclusion&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#in-conclusion&quot; aria-label=&quot;in conclusion permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;strong&gt;In conclusion&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;As I mentioned in the introduction, this is the first of two articles on migration problems. In the second post I&apos;ll describe the topic of module migration where there are many more problems - there I&apos;m sure I won&apos;t even describe everyone I&apos;ve met because there are a lot of them, although many of them are rather due to poor documentation (and deficiencies in the so-called &quot;Breaking changes&quot;), but most of them are not so difficult to fix.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;And finally, good luck!&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PS&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If you are looking for help with such migrations in your company, do not hesitate to contact me. I can help with small migrations as well as large ones.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Continuous integration and quality inspection for Liferay 7]]></title><description><![CDATA[Hello! Today I would like to show you how to use Jenkins for continuous integration (CI) and continuous code quality inspection using SonarQube. I decided to write this post because some time ago I had to make such process for project I work on and I couldn't find any help regarding Liferay and I had some problems with that, especially with setting SonarQube properly for Liferay workspace Prerequisites First we need few things: Liferay Project - for this case I created a simple project which you…]]></description><link>https://pydyniak.com/continuous-integration-and-quality-inspection-for-liferay-7/</link><guid isPermaLink="false">https://pydyniak.com/continuous-integration-and-quality-inspection-for-liferay-7/</guid><pubDate>Fri, 26 Jun 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Hello! Today I would like to show you how to use Jenkins for continuous integration (CI) and continuous code quality inspection using SonarQube.&lt;/p&gt;
&lt;p&gt;I decided to write this post because some time ago I had to make such process for project I work on and I couldn&apos;t find any help regarding Liferay and I had some problems with that, especially with setting SonarQube properly for Liferay workspace&lt;/p&gt;
&lt;h2 id=&quot;prerequisites&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#prerequisites&quot; aria-label=&quot;prerequisites permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Prerequisites&lt;/h2&gt;
&lt;p&gt;First we need few things:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Liferay Project - for this case I created a simple project which you can also use. It contains just a few classes and modules but is more than enough to show the basic concept. You can find it &lt;a href=&quot;https://github.com/RafalPydyniak/JenkinsSonarDemo&quot; target=&quot;_self&quot; rel=&quot;nofollow&quot;&gt;on my github&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 id=&quot;jenkins&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#jenkins&quot; aria-label=&quot;jenkins permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Jenkins&lt;/h3&gt;
&lt;p&gt;Lets start with Jenkins. In case you never heard of Jenkins it&apos;s an open source automation server. It&apos;s often used with code-related automation process such as building, running tests and deploying application but you can as well use it for other things such as running some application periodically for example.&lt;/p&gt;
&lt;p&gt;To install Jenkins you should check &lt;a href=&quot;https://www.jenkins.io/doc/book/installing/&quot; target=&quot;_self&quot; rel=&quot;nofollow&quot;&gt;this installing instruction site&lt;/a&gt;. You can install it either on your machine or using Docker. I suggest you the latter as I&apos;m Docker fan myself but the instructions are the same either way so pick yourself :)&lt;/p&gt;
&lt;h3 id=&quot;sonarqube&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#sonarqube&quot; aria-label=&quot;sonarqube permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;SonarQube&lt;/h3&gt;
&lt;p&gt;The second tool we need is SonarQube which is an open source tool for performing automatic code quality checks. It can detect code smells, bugs, duplicated code and things like that. I&apos;ll show you SonarQube later on when we&apos;ll start configuring things. The instructions for installating the SonarQube are on &lt;a href=&quot;https://docs.sonarqube.org/latest/setup/install-server/&quot; target=&quot;_self&quot; rel=&quot;nofollow&quot;&gt;its official site&lt;/a&gt;. Again you can pick between Docker or standard installation way.&lt;/p&gt;
&lt;h2 id=&quot;integration&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#integration&quot; aria-label=&quot;integration permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Integration&lt;/h2&gt;
&lt;p&gt;Now when we have fulfilled requirements we can start the configuration of integration between our Jenkins, SonarQube and our project.&lt;/p&gt;
&lt;p&gt;Basically what we want to do now:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Configure the project in SonarQube&lt;/li&gt;
&lt;li&gt;Add SonarQube Scanner plugin to Jenkins&lt;/li&gt;
&lt;li&gt;Configure the SonarQube scanner plugin&lt;/li&gt;
&lt;li&gt;Create a new Jenkins project for building our project and scanning with SonarQube&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;configure-the-project-in-sonarqube&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#configure-the-project-in-sonarqube&quot; aria-label=&quot;configure the project in sonarqube permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Configure the project in SonarQube&lt;/h3&gt;
&lt;p&gt;Before we start an integration we need to create SonarQube project. To do that we go to our SonarQube server (if installed locally it&apos;s most likely under localhost:9000), we log in (default credentials are admin/admin) and click &quot;Add&quot; button in top right corner &lt;span class=&apos;gatsby-resp-image-wrapper&apos; style=&apos;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 111px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 82.88288288288288%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAARCAYAAADdRIy+AAAACXBIWXMAABJ0AAASdAHeZh94AAACf0lEQVR42q1S32uSURj+/odpM3Wfbk6/zx+jFLwbTEjdIqiLboRuoiCoQTdFFwVOiNrKC6XGCgosgtgIXZuLdKtNpvaDfnkxGOhKL5pC3USLIE2fvvM6bcZqtDrwcN7znvc853nPczin04WBgb1bwu3uh2uPa1O4Xf3NOq6npwdarfaP0Gg06DZ0w7zbAtMucwtYTi/oqYbVcu9XV9Hb24uuri6IoghBEGAwGGhmMJqM0Kh4nL7pxcTHOEIrEdx+NynhPkJvIxj/EMO5uxehVWsgGo3gqtUqHA4H3UAEUpIRs5mtRaOIDoUa56MBxPES4bUFzFSeYqacojiGF/DPXwev7KBzXKVSIUIml5HI5XIMDg7CarWC53lSyAh9YT9mvj9B5EsC117dwo3cFCJr84hW0hiOXQWvWifcqNBkMhFZNBpFIBBAX18fdN06ut0X8eMBniP8aRanxs7g7L0AJr8u0iUjs6M/Ff5KaLfbEQwG4fF4aC2IAjp2qokwJrUcyk7g0twYRuKjuFOK4SGeYTi+gbBcLjdbZglG7HQ6YTabodPp6KEZ4VD4MqkZl4xhbxf+/JjiestXNm+54WxnZyf0ej25XTdFRaY8whtMfUtiWjKEEU2Xk5jDa/gX1k2Rajmv1wubzUaGMFUM7G82YovFAkEvYN+hAzh24SSODB3HUd8JAotZbv/hgxANAtVyMpmM3GQKfwvpOVQKJRRtO6CQtbeirR1KaU+jrddyqVQKiURiSywmF5FMJTcF22vUcfjPg0zZLtio1WotuX9SmMvlUCqVmsSk8G9JGgez2Szy+TyWl5dRLBabe9tSyA6m02kUCgUsLS0hk8lQnrX8A3wBjkttap8yAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Add project button&apos; title=&apos;&apos; src=&apos;/static/9b10e62adb5bd598881423c0d643d5a3/a4766/Add-project-button.png&apos; srcset=&apos;/static/9b10e62adb5bd598881423c0d643d5a3/a4766/Add-project-button.png 111w&apos; sizes=&apos;(max-width: 111px) 100vw, 111px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;Then we have to generate our token - we simply enter some name and click &quot;Generate&quot;. The token is generated for us and we can use it from other application (such as Jenkins) to connect to SonarQube.&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,iVBORw0KGgoAAAANSUhEUgAAABQAAAAHCAYAAAAIy204AAAACXBIWXMAABJ0AAASdAHeZh94AAAA4klEQVR42pWQ2W7DIBBF+f//60tfmsaNFANmtctizM1AFjlVo6pIR1cww52FDccBn4cDhuELH6RykjDGQGvTdY/Wuqv3/iVskhKCcwghO1IKnMcRnJBCgPfYVcczvVH+PM8vYW9HjveTRArfcFQhpoS/Tq31mV2MLTHBLwHOOVhrMU0TlFJdWzfqrjSuUxrF2Lvrr8XYtm1Y1xWeDBttT7KZqLYv+yjketwjNGJECAEpXTXSfbsV6IYprzhxBeNmZBp5CY2MUran0dqnlv+TuuuW5ZwRyVBYWmqIqLvEUsq/uQCXuB5gjQcy3gAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Generated token in SonarQube&apos; title=&apos;&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; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Generated token in SonarQube&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;We save the token and the project name as we need that later. If you click continue you can see the example of how to use SonarQube from Gradle or Maven but we won&apos;t need that.&lt;/p&gt;
&lt;h3 id=&quot;add-sonarqube-scanner-plugin-to-jenkins-and-configure-it&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#add-sonarqube-scanner-plugin-to-jenkins-and-configure-it&quot; aria-label=&quot;add sonarqube scanner plugin to jenkins and configure it permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Add SonarQube scanner plugin to Jenkins and configure it&lt;/h3&gt;
&lt;p&gt;In order to integrate with SonarQube we need a &quot;SonarQube Scanner&quot; plugin. We can install it by going to (from dashboard):&lt;/p&gt;
&lt;p&gt;Manage Jenkins -&gt; Manage plugins -&gt; Available -&gt; Search for &quot;SonarQube Scanner&quot; -&gt; check the first one and click &quot;Install with restart&quot; &lt;/p&gt;
&lt;p&gt;Then, after we install the plugin, we go to (from dashboard):&lt;/p&gt;
&lt;p&gt;Manage Jenkins -&gt; Global tool Configuration -&gt; Under &quot;SonarQube Scanner&quot; we click &quot;Add SonarQube Scanner&quot;, name it somehow and just leave the other, default options. We then click &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: 1204px; &apos;&gt;
      &lt;span class=&apos;gatsby-resp-image-background-image&apos; style=&quot;padding-bottom: 41.19601328903655%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAICAYAAAD5nd/tAAAACXBIWXMAABJ0AAASdAHeZh94AAAA+ElEQVR42o1Sy07DMBDM/x9Bgg+ASvAjPfA49VigpyqNGyd1/Caxh10jJETb0LXGsq3R7O6sK+cchBBQSiGGgJQSjHWw9O5jRM4ZlwZzK97SNBWM41gem6bB2/sHdnUNPQyw1oITz4E5gQqq/mZJBB8+cdhsoLZbGGPgvUekas+BhQKJautPCFKHbacgdjWklIV8adt+yseCHDEGDFqj7/viLVd4xrRvofUa7d09zHJ5LJhpaWPLUPj8zxSQyBK3WqF7eIR9eT0hSCQ1aBxoGJ7bnRHjkIsFmqtriJtbmKdnVPxNfnvE91Z22O9bdF1fPGTj5yv9QcYXnsRvTOF6oaYAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Configuration of SonarQube scanner in Jenkins&apos; title=&apos;&apos; src=&apos;/static/48639724c1ee9e6afd0a41dccc4c0df2/35252/SonarQube-Scanner.png&apos; srcset=&apos;/static/48639724c1ee9e6afd0a41dccc4c0df2/fb933/SonarQube-Scanner.png 301w,
/static/48639724c1ee9e6afd0a41dccc4c0df2/32056/SonarQube-Scanner.png 602w,
/static/48639724c1ee9e6afd0a41dccc4c0df2/35252/SonarQube-Scanner.png 1204w,
/static/48639724c1ee9e6afd0a41dccc4c0df2/e515d/SonarQube-Scanner.png 1430w&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; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Configuration of SonarQube scanner in Jenkins&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;After we add SonarQube Scanner we also need to configure our SonarQube server. To do that we go to (from dashboard):&lt;/p&gt;
&lt;p&gt;Manage Jenkins -&gt; Configure System -&gt; We look for &quot;SonarQube servers&quot; and we click &quot;Add SonarQube&quot;. We name it, provide URL&lt;/p&gt;
&lt;h3 id=&quot;create-jenkins-project-task&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#create-jenkins-project-task&quot; aria-label=&quot;create jenkins project task permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Create Jenkins project (task)&lt;/h3&gt;
&lt;p&gt;In Jenkins we have several types of projects. What we want is the basic &quot;Freestyle project&quot; which basically a type of task where we can put different stuff together (like build, test, deploy etc). We create it by just using &quot;New item&quot; from dashboard and selecting &quot;Freestyle project&quot;, giving it a name and just click 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: 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,iVBORw0KGgoAAAANSUhEUgAAABQAAAAPCAYAAADkmO9VAAAACXBIWXMAABJ0AAASdAHeZh94AAAB/0lEQVR42q2Sz2sTQRTHN78grVSFUIQchHpv8gcYPMVDsNuAm4QckxASQ+LJ3IQuIgjiQei5t57bW6EU8WBtQYgpa1WwVGijLd3WmGRXk012s19nJtlQsbEpdOGz896bmc+bHZbjOA6UK+PjuH51AlZOsdlsjGG5VTudc6xIWHw5h+WFZ4MJv98Pr9fLYpfLBYfDgUAgAI/HA5/PB7fbDafTeZaUw93JSay8mMPS44eYHhtjEw+yWRSLRaRSKeRyOYYoikin00gmkygUCqxGx2AwyPbY7fae8MbENSSyjzAvPsUtIr85NYUsEfI8j3A4DEEQBuJ8Po94PM6kiUQCmUwGoVCICdmJ+1bcDtzBPbKZdfr3XkYnGo0iFothdpYHPzMD4b4AWrOgpzudUyKRyF853W/FHC754VqtFjq6juqPY6xvbGLjXQlrr17jzdtNrBP2Kt9hGAZUVYWiKGi329A0bSg9YaeDRq2G8paEUvkDJGkb0vYnvC9vYZ8IddKw2Wwy/idjQo11bLPOptlFt2thsFHXDbToYrpuBPp3aPZl5kBIP7MnJXMU0xztDq3ANA3y/k0DjLh3uFBTZOysPsHPL8+hKnV8LH1GZXcfh9+OUNnZYxxUZKvz+cJOs4avqyJkaRl0uXwg4+TwGI26ika1jurRCYl/XeyTL+s//AOMUXHqsPufVgAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Jenkins new project button&apos; title=&apos;&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; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Jenkins new project button&lt;/figcaption&gt;
  &lt;/figure&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: 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,iVBORw0KGgoAAAANSUhEUgAAABQAAAAYCAYAAAD6S912AAAACXBIWXMAABJ0AAASdAHeZh94AAADnklEQVR42o1VTY/iRhD1X8ghPyjn/S2JlN+RHJJ7LlF271Gk1SrSHiKRjTIjzQwwAwzLN8Y2xoDB3xjMy6uyIRDNJjk8qqq76e56/apsbLdbxHGMKIpKhCHjCGEVy5ys2Ww2/wvGYrHA77UaHu7v0Xx8RKNex+3tLZ6eHlGvP+Dm5ga2bWO9XmO5XGK1Wr0ImRcYEnS7XfQHA/T7fYyGI43H4xF6vR6en58hh8rpvu9/EucNTwOSVlEUOBwOV1aQ5zl2u90nkabp+ZaG/EynU0wmE4XjOOcTfQH9/+Lt6oZys2DDP/lrnQyCAGEQIuTjiP9vaV5CN5ZHSdIM9nYPL8yx5sR8scRy5WPhkWx/g93+gLxC6RfY5aUtcekXMOIsx4f3r/Hwxy9w5i7anQ4Gw6E+xng8JodH7Pd7hfB6gnAn3Ip/mhcYSbbHdnKLaN7iDbdYuHPKw1Mu546tkhHfti3YlqWxQCiSVIUan77oNyB93HCHwxE48CZxkhIZQop5vQmw2QYIIgo7jBGEEbbkVi2RH4oSkjLtrrJGyg2bIwvd8Qw96rDRbJDDtR4gKeVM4yVkO5ESkVeoYiMlF402hWzaME0TPYpaUvM8j9paql0SruuqwD1CKFmxasSfz+c6J5BxI8syHCrCtYyWK7jkbGbOdJE1m/FPjGkFJw4d8mtSv1Y1ZpoWZpMBN+QNhaeQWK1XV0K90hglJIfL64o94Spmxeij1Nsf0R2MKZUOWq22Subu7g6PbBbtdketjEc8VMosTmIkSaLdSKwiLa3e0HHZRdYbpuaq9hYLTzuL5wlfK+VQYhkXTpVL2jmpUYooNZtcegu35PB4ZDMoDpqalKKcVAq4ODeKyxRfQlZZyibD1HL1lvLK04p8IX02MzHijV2eXOjmx7IDHY/nTnT8h89aTvHTzzXct/rsOhPy12Uf7GM4HGAw6Gssm2rnZgZhSLGzKpI4KRsJfamSsIIh1/RZcmlSER6XZMtkXH0GxBcq/n51Xzc7ta/TJ0LmDOHr/Z9N1PsOJmwKnVaHRX5d8NeNoVDImstmURSlNbaRj1qzh4cBBbxkQ1hYiNINa1kqwasqYKkvPddqcbUAZOxcPTJHhch6A0jw7a/3+PzrN/jim7f47KvX+OG3OrLYw9ScaadxbAcWy1FgmlOFxQc8VY5YeVDxuWGEHz884dV37/Dlmxpeff8ObxsfgSPFy+a7owq0SRCS+un7cmkv5/4CgjcRtbdie20AAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;New Jenkins project options&apos; title=&apos;&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; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;New Jenkins project options&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;After we click ok, a newly created task setting site should appear. We won&apos;t go through all of the settings because there are many of them and most of them are for quite specific use cases. Fortunately we can click on question mark next to each field and a see a help message for the field.&lt;/p&gt;
&lt;p&gt;When you create a new Jenkins task you should first define what exactly you want to do. In our case we want to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Pull code from remote repository - this will be git in my example but you can also other like SVN&lt;/li&gt;
&lt;li&gt;When we have our code we want to build it using gradle&lt;/li&gt;
&lt;li&gt;Then we scan our code with SonarQube&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In this example that&apos;s all. Usually you also want to run tests or go even further and automatically deploy the application to your server (Continious deployment) to have full CI/CD process but for now we&apos;ll just stick with continiuous builds and code check&lt;/p&gt;
&lt;h4 id=&quot;pull-code-from-repository&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#pull-code-from-repository&quot; aria-label=&quot;pull code from repository permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Pull code from repository&lt;/h4&gt;
&lt;p&gt;To automatically pull code from our repository we go to &quot;&lt;strong&gt;Source Code Managemen&lt;/strong&gt;t&quot; section. Then we pick the system we want to use. In my case this will be Git. We also need to provide credentials - we can do that by clicking add (assuming we&apos;re doing that for the first time) and selecting &quot;Jenkins&quot;. In new panel we provide username and password and click add. The credentials will be saved and we&apos;ll be able to reuse them with other tasks. When we finish it should like on the screen below:&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: 1196px; &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,iVBORw0KGgoAAAANSUhEUgAAABQAAAAHCAYAAAAIy204AAAACXBIWXMAABJ0AAASdAHeZh94AAAA8klEQVR42pVQy3KEIBD0/78wt+haurwRVBBz6J2BjVUmh1QOXUM3M00PnTUa8zxjHEc8Hg/0/SeUUiil4DiOf6OzfoG1Dtu2IcZYse97NWSc54lylos3rek/8UV6Z/yKECI1HrehnPMNtyTEU0oXMlfS4p7RxRiglcZCSTmdlBLTNOEpnjDGNGhdv0G9qyWNN+JNUtrh44qPyUD6jQ3bit8rCyEwDAM0DXvnYaylxzzWdYWgh6U28MSXZbmqffeVnJohNzNCCNWIU1lqsDWhrWdHaAn1lZz5ME4QUsE5V4P9MqzDdPkXON0sJPphrIbM2fAFRioW8e+GzfIAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Image showing the filled credentials fields&apos; title=&apos;&apos; src=&apos;/static/7d257be55d9a80481f3cd3959c5a70b3/51800/Credentials.png&apos; srcset=&apos;/static/7d257be55d9a80481f3cd3959c5a70b3/fb933/Credentials.png 301w,
/static/7d257be55d9a80481f3cd3959c5a70b3/32056/Credentials.png 602w,
/static/7d257be55d9a80481f3cd3959c5a70b3/51800/Credentials.png 1196w&apos; sizes=&apos;(max-width: 1196px) 100vw, 1196px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Image showing the filled credentials fields&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;h4 id=&quot;build-our-application&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#build-our-application&quot; aria-label=&quot;build our application permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Build our application&lt;/h4&gt;
&lt;p&gt;Now we go to &quot;Build&quot; section. In this section we can add one or more steps for our task. The step can be for example running Gradle, invoke Ant, run shell (or windows cmd) command etc. In our case we pick &quot;Invoke gradle&quot;. We can either choose a version we want to use (these can be configured in Jenkins settings) or use Gradle Wrapper. Using gradle wrapper is recommended way of using gradle so we&apos;ll pick that option. We check the &quot;Make gradlew executable&quot; (it basically does something like chmod +x ./gradlew) and in task section we put our build task&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: 1177px; &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,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAYAAAAywQxIAAAACXBIWXMAABJ0AAASdAHeZh94AAABAUlEQVR42p2Sy26EMAxF+f/PaxfdsJppCXFIyIsEMki3dkZVl0O7OOIhfLi2MyzGQCmFx/lAaw3HcfyLdp69fihlg3MW1goLUkrYtu06OaPUCvP+BqMNhpQy7p9fGMcRt9sdIQTknLr4ClEC8PdKW3xMK4bIwokf9DxDaw3rPNaQkK8KI19jhOWayUVumWMbQ1CTArFwJoslVBx7RSnlEju3HPMGWkXILyQZEWH1vs+l1sLUPuRXyEIqO4xPGHXAIILIkTMPV5A2nHPwLH+VTH4auHbWBFocp8yylNQ37NzKC/kV/8hf4X2AJvMMwOF6QuKzaBaLxJJ93/9Ma8+zKPffQFO0h6rL5t0AAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Running gradle in Jenkins configuration&apos; title=&apos;&apos; src=&apos;/static/c7114550e3a658fab96089beb30ff31f/c0ebd/Build-gradle-task.png&apos; srcset=&apos;/static/c7114550e3a658fab96089beb30ff31f/fb933/Build-gradle-task.png 301w,
/static/c7114550e3a658fab96089beb30ff31f/32056/Build-gradle-task.png 602w,
/static/c7114550e3a658fab96089beb30ff31f/c0ebd/Build-gradle-task.png 1177w&apos; sizes=&apos;(max-width: 1177px) 100vw, 1177px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Running gradle in Jenkins configuration&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;h4 id=&quot;scan-our-code-with-sonarqube&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#scan-our-code-with-sonarqube&quot; aria-label=&quot;scan our code with sonarqube permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Scan our code with SonarQube&lt;/h4&gt;
&lt;p&gt;We have our code built. Assuming it builds properly we want to call the SonarQube now. To do that we add another build step by clicking &quot;Add build step&quot; just under our gradle step. We then pick &quot;Execute SonarQube Scanner&quot;. In &quot;Analysis properties&quot; we need to provide informations for SonarQube scanner so it knows how to scan our project. There are many settings we can use and you can find them all in Sonar documentation but the properties we&apos;re going to need are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;sonar.login - token you have generated in SonarQube (see &quot;Configure the project in SonarQube&quot; section)&lt;/li&gt;
&lt;li&gt;sonar.projectKey - projectKey you entered when you created your project&lt;/li&gt;
&lt;li&gt;sonar.java.binaries - you specifiy location of your project binaries&lt;/li&gt;
&lt;li&gt;sonar.java.libraries - if they are extra libraries you should put their location so Sonar can use them while scanning&lt;/li&gt;
&lt;li&gt;sonar.java.source - source version of your project&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Check the screen below for the default values - they should work also for you unless you have some specific paths or requirements&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: 1142px; &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,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAABJ0AAASdAHeZh94AAABUElEQVR42o2R6W6DMBCEef8nq9R3iAQEwumLyxzOdHcdmrTKjyB9wjbsemY2ybIUaRrJ8xzWWozThHEcP2OaMage1fcX3DAiKcsb2raF1hrGGCzzjGXx8KvHvu+/bNv2lnXdcD92XJ3HtXdIDCmaqcn0UNV1HV3QQSktZ8MwCLz23r9lo8sbZdHZgRqSKlbHKKUQ9yfxrO97iWJZFmIWAa94OuNmPTesbhWKokBZllDUgG/kwnle5Oe4joXswNP+TxRE2FekrYuWRR0rIwVsK89yXC4XGVBVVaLY+1Uu4ry4yf9npW/aWAzjhEQm9ciJ4QHVdQVFNlkVNwgh4H4POI5D1vw+CeEQF85FQUlTN9KA1Zw5nbaf9p9D4wxfsdahJ4dZo1BrylDsku1TYbS2fsRGOKrRFEveajSGGubXAhlhKANWEAfxOVxjqVYbJxn+AJFOAdRSGB8MAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;SonarQube scanner configuration in Jenkins project&apos; title=&apos;&apos; src=&apos;/static/a602be810b0ccdb2677dcf4f5dc9340c/b490f/Jenkins-sonar-config.png&apos; srcset=&apos;/static/a602be810b0ccdb2677dcf4f5dc9340c/fb933/Jenkins-sonar-config.png 301w,
/static/a602be810b0ccdb2677dcf4f5dc9340c/32056/Jenkins-sonar-config.png 602w,
/static/a602be810b0ccdb2677dcf4f5dc9340c/b490f/Jenkins-sonar-config.png 1142w&apos; sizes=&apos;(max-width: 1142px) 100vw, 1142px&apos; style=&apos;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&apos; loading=&apos;lazy&apos; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;SonarQube scanner configuration in Jenkins project&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;h2 id=&quot;lets-test-it&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#lets-test-it&quot; aria-label=&quot;lets test it permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Lets test it!&lt;/h2&gt;
&lt;h3 id=&quot;run-jenkins-build-we-created&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#run-jenkins-build-we-created&quot; aria-label=&quot;run jenkins build we created permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Run Jenkins build we created&lt;/h3&gt;
&lt;p&gt;Finally we have it all to run our task for the first time. After we save our Jenkins task we can run it by clicking &quot;Build now&quot; (if we&apos;re on dashboard we have to enter the task by clicking on its name first).&lt;/p&gt;
&lt;p&gt;We wait for a little bit. After all we should see a task number with blue dot under build history indicating that everything went 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,iVBORw0KGgoAAAANSUhEUgAAABQAAAAHCAYAAAAIy204AAAACXBIWXMAABJ0AAASdAHeZh94AAABMElEQVR42m2QzU7DMBCE8/7vw62HRkVColURB5QCairIfxzHae3YdRxpsJcWFcTh0+zau/bORsYYjEoRSo0YxxFSSWijcT6fKVd09xvpMXrEZA20Dn3f55GUJzxun5G8viFeLrFYLHC/WpHGcYwsy8E5B2MMXdcRIR5Ej4dtgrvlE+L1DmlW+7oOkdYaZdODiyOEENQseq8dR9M0GIaBppFS/kJ5Fw3r8VkytJ3Aic78hM7NOA6CGqdpIuxkL7GDtT63luwHQnzFuQmzc6TGr0hrg2ieZ/Sc+cn6f5vo4at6rjV/CbsOu4yC1SQtsNslZDcQrJZlibquSQN5nhNt2/7U3cL8ioqWIypqhtXmBZv1GmVVofJkWYY0TXE4HLDf70lDHgiPVpe6W3L/6ftHgS81kA9VuZo8pgAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Build history overview&apos; title=&apos;&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; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Build history overview&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;In build history you can check see your previous builds (you can see mine is already #4 - previous 3 failed but hopefully if you follow the tutorial it will work for you at the first time! ;) ). You can also click on the number to go into specific build history entry where you can for example check console output (really useful if your task fails!).&lt;/p&gt;
&lt;h3 id=&quot;check-the-result-in-sonarqube&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#check-the-result-in-sonarqube&quot; aria-label=&quot;check the result in sonarqube permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Check the result in SonarQube&lt;/h3&gt;
&lt;p&gt;Lets go to our SonarQube project and see the result. If you&apos;re on SonarQube main page you can go to &quot;Projects&quot; and enter the project you&apos;ve created by clicking on its name.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;**:** Sometimes you might need to wait a while. Jenkins task sends the data to SonarQube and finishes but SonarQube need a while to process the data. For big projects there is usally a small delay between Jenkins task finishes and the results are available.&lt;/p&gt;
&lt;p&gt;When you enter your project you can see summary for your project:&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: 53.156146179401986%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAABJ0AAASdAHeZh94AAABnElEQVR42oVSO05DMRC0lEuRJg1SBFVKKJA4CeImVCDuACJQUcMZEAXkff3/PQ9rw4OEBLHSvGfZ6/Hs7LL39wqiExCtgpMORhoIKSClhNYGyhhIpWm9DSEEqqpCVTd4W9Womxbs5PEUx49HOHg4xOxuhvPnc/RNDy44JBGuWg7tIoyxBLOBTCqJVCkFoehxymfsimFyPcHkcgJ2wbBYLpAcSJXCMMSClBJijDsxDANiGGCsgLYN2N7NHvaX+5jfzzG9neLs6QxIgA8BZZG/6fP/V+TjECzdUWBBBazDaw/jPDSVFArp/5GKAA3nycPs1Yhe9KUZLkRY6wrhurpSHpX5O2Kkkj01yfVguVMjOM8GS1IYCqH3foMwFb+2VUcSoJxCa7sdhKTQ+lBIR/9G0k5ZOLJjbNS4n5Ubb8At3yTMyCV7ejHjx6PPi9I4InSFYD0GKjkr7O2vkgXnZVyS0WjrDq8N/x6N0iDijSmiNg1eVl15IG9KmtFAAgbK3SYkhcFaWCJWlDg2J/uZzQ8xQDpZpkB/neUhd3Se1x/pmELB9l9AMgAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Summary of errors on SonarQube&apos; title=&apos;&apos; src=&apos;/static/29e74e9eac4dab6205b77e76bdc53fa4/35252/Sonar-summary.png&apos; srcset=&apos;/static/29e74e9eac4dab6205b77e76bdc53fa4/fb933/Sonar-summary.png 301w,
/static/29e74e9eac4dab6205b77e76bdc53fa4/32056/Sonar-summary.png 602w,
/static/29e74e9eac4dab6205b77e76bdc53fa4/35252/Sonar-summary.png 1204w,
/static/29e74e9eac4dab6205b77e76bdc53fa4/d7050/Sonar-summary.png 1647w&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; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Summary of errors on SonarQube&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;There are tabs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;New code - it shows summary only for new code&lt;/li&gt;
&lt;li&gt;Overall code - summary for whole codebase&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The difference is that if you have an error that exists for a year then it will only be counted in &quot;Overall code&quot; but not in new code. That helps especially when you start scanning an existing code - usually you have a lot of bugs already and you don&apos;t want to fix them right away so you can just start by making sure that new code is good enough.&lt;/p&gt;
&lt;p&gt;Another useful tab is &quot;Issues&quot;. It shows the issues in the code - probably the most important tab. You can check every bug Sonar finds in your code. You can also filter by severity, type, date and many other things. What is also cool about this is that you can assign an issue to someone so from Sonar you can manage the people assigned to certain errors. You might also change type, severity or add a comment. What is also important is that you can also mark issue as fixed, false positive or won&apos;t fix - this is really helpful in some scenarios since SonarQube is just a tool and makes mistakes.&lt;/p&gt;
&lt;h3 id=&quot;problem-with-liferay-service-builder-code&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#problem-with-liferay-service-builder-code&quot; aria-label=&quot;problem with liferay service builder code permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Problem with Liferay service builder code&lt;/h3&gt;
&lt;p&gt;If you looked through your SonarQube issues you probably noticed one problem - there are a lot of bugs. Is your code that bad? Well maybe but there is also one more explanation - auto generated code from service builder. In my case my test project has only three classes where I put code any yet I have almost 100 errors which is quite a lot (even though I made some mistake on purpose for this demo). And that&apos;s just from one entity in my Service Builder.&lt;/p&gt;
&lt;p&gt;We could of course mark these bug as &quot;false positive&quot; or &quot;won&apos;t fix&quot; but lets imagine a real project - number of these bugs would be just too high. I myself worked on a project which had around 150 entities in service builder and probably your project might have much more.&lt;/p&gt;
&lt;p&gt;Fortunately there is another way. We can tell Jenkins to ignore some paths from scanning with sonar.exclusions property. With this property you can select the paths you want to ignore. This seems easy but there are actually two problems:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If you use Liferay 7 you probably have well modularized project (or you should have!) with OSGI. That means you can&apos;t simply put one path for your services because you might have as well have 15 modules with services&lt;/li&gt;
&lt;li&gt;We have auto generated code in *-api but also in *-service. You probably could ignore whole *-api modules in most cases but in *-service there is some of the auto generated code and some of your code (in *impl classes)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And I can think of two solutions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;For small projects, perhaps you don&apos;t have that much modules - you could simply point to proper modules and for -service to directories in that module that should be ignored&lt;/li&gt;
&lt;li&gt;Think about some &quot;general&quot; rule which would apply in your case&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;general-exclusion-rule&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#general-exclusion-rule&quot; aria-label=&quot;general exclusion rule permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;General exclusion rule&lt;/h4&gt;
&lt;p&gt;I picked the second option for this blog post but also in my real life project so I can say it works quite well. The property is:&lt;/p&gt;
&lt;pre&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;/pre&gt;
&lt;p&gt;As you can see you can separate the entries with comma. You can also use:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;* - to ignore on or more character in file name&lt;/li&gt;
&lt;li&gt;** - to ignore one or more directories&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So this property basically ignores auto generated classes in both -service and -api modules of service builder. There is one problem though. This property makes some assumptions about naming in your project. For example it ignores &lt;code&gt;**/*-service/**/service/util/**/*&lt;/code&gt; but what if you had your own module called for example blogs-service and inside you had a directory service/util? Well then this would be ignored then.&lt;/p&gt;
&lt;p&gt;Unfortunately in this case you have to tune the general rule or just pick the option &quot;a&quot; and point to directories you want to exclude yourself. In most cases though that should work for you &quot;as is&quot;.&lt;/p&gt;
&lt;h3 id=&quot;another-test&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#another-test&quot; aria-label=&quot;another test permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Another test&lt;/h3&gt;
&lt;p&gt;When you put this extra property it should look much better for you. In my case out of 100 errors I have only 10 left which is great. And these ten are the real bugs or rather code smells I have in my code. But what is also important - the one error in *LocalServiceImpl is still there so we didn&apos;t accidentally ignored that class with property we added.&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,iVBORw0KGgoAAAANSUhEUgAAABQAAAAgCAYAAAASYli2AAAACXBIWXMAABJ0AAASdAHeZh94AAAEjUlEQVR42o1W13LrOAz1//9TXnaSu5vqXmS5RLIsyZZkNfeSszhQ6HVmsjeXMzBJEAIBHAB0DTLK/Rmbwxm73Rbb7Ran0wm7/V7mI46XD+xPF5yORznf4Xg4YHe84HC+4LtR488sOyDIt7AHffT6FvwggO/7SOII5fEDq80Ry0UI27YRzGcYRlt42V4VfHx8yA+u6xp+M1QYV/k/GqowTVPMxaIwCOF5Hi7i5rosEIWBkK+CWbpSq0PhLZaRrl3HgTeboSjXOJ/PGI9s1BiboiiwWq1UcRzHuEjs3hcp/uq6eLQ8rHeH6oIoQpIkSLNMZaPlQnnr9QZH0RMtl6hR2Xq9VjA2m43OpfDSvMSqWCMrNyjL6nwvQJEoZ2RJ1JHnuVKNB/1+X13luixLJV6SitW+N4NlDdHtdjEajzEcDtU6nlMRv6GVg8FAZWo8WIqpFDLKDPGDVGJHNxeLxZXyvPgiR8vIp2E1Yw1v4nxroeFXtL26aM4M8WKGYiweqMKyyMU9CbZYQjR1n8TKy8TtipKrjJHLPs+LPMNeimI8GonLojCIVrAdDz17iuG7BzeMMRg7yrMmLobTmZI1ddEbTfWM1B+/YzBx4PhLLNM1LFsUJlmBppug7wQYhSuMgxWmiwzDeQzLW2ISpnCiArYfYyKpNPITjIJE1hmmy6zai6yTHfGrZaEWZyX6XoTW6zPazQYGvQ6sflfWdTTeXtBu1GFbgmCriX6nrftOq4GR8Fr1N7REbu6+Y3sG/laFaSGWZWiK4OubCLRa6PZ6miLNpqy7PQ2264q7giL58/lc6nqEeqMBW+J2lkL4otAOxJWhBWtoYzKZ6oeWPURH8oqVw3ExdS0zieX5X71/YHMC/mkP6XKl0Oq2Bf5cEc6zFLmguN2sf2wGpnHQQlVIUFoCyv3jK9xFAtv14QSS+YKqF6UoDhdku9P/UipU7E9YbM741bSqWp4lBcLyiHm6hZ/t4K020h/3OjtxCVfIzG7yOd/wmAVOvEZnYFelV4iLvudi5kylgXpIpbF6ghybKffzmatz4HPt6J4Uyn7mVHLH3QaT8aiqlCyv2tFS+lwcJxLLErFUQ5KsFBRtWXKeSFVEsq/OEqw+edzTMHb0a/uyLEubJlNiJk1zMpkgkKeARc/1SNKDM89I/JidhylFGQ5VSAvZXF9eXvD09ITHx0fNxfv7ezQkz9iSms2mWkqipZk0WFrItsU9uw0HL71aaCyjUBiGeivXJJOLPw1VqI1Ubrm7u9NbqZgKWRnk3ybz7+iLhaZJsqfx7b2lg7zD5PPNMGR4nLk3T8M1hqbjmkfIdGhexrVB2sSRMec3VWYsdU0rVSFdpiICQjB60hhIXBPBN2kY9XpdgSFAr6+v2urJ4zvy8PCg/KvLxsJOp6MMChBp3vb8/KzpxDUvoWUEjulCAyjP83a7ra7rE2BQduTR5iFvpYDJOf7PuQXmyz+K70DRSpG8ojW0lGtawjDwcbpVcPsX5TuUr6BQCWPEmWAQAFaJCfblcvmR+FeEgNXMs8kU4BPJVDHz7cP/J8Tw/QvDdWnah0ffMwAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&apos;gatsby-resp-image-image&apos; alt=&apos;Errors after applying the general rule&apos; title=&apos;&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; decoding=&apos;async&apos;&gt;
    &lt;/span&gt;
    &lt;figcaption class=&apos;gatsby-resp-image-figcaption&apos;&gt;Errors after applying the general rule&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;h2 id=&quot;summary&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#summary&quot; aria-label=&quot;summary permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Summary&lt;/h2&gt;
&lt;p&gt;To sum up - we have created a Jenkins job that runs the build of our project and sends the result to SonarQube. On SonarQube page we can see the result of our code quality check i.e. issues in our codebase such as security hotspots, vulnerabilities, code smells and others.&lt;/p&gt;
&lt;p&gt;In this tutorial I didn&apos;t touch the topic of build triggers (see &quot;Build Triggers&quot; section in Jenkins task configuration) but you probably want one of these. This allows you to automatically run the task whenever something change in your GIT (or other) repository. There are different ways of doing that but you can read a lot regarding this topic on other sites or blogs so I left. You can also check other options in task configuration but like I said before - most of them have some specific use cases and you&apos;ll probably not need most of them at least for now.&lt;/p&gt;
&lt;p&gt;The next step in our automation process could be automatic unit tests running or automatic deployment of your application to the server. You could also send an email for failed builds (see post-build actions in task configuration) or perhaps do something else specific to your company process.&lt;/p&gt;
&lt;p&gt;With Jenkins you can do quite a lot so I encourage you to try it out and see what options it can give you. Keep in mind that there are also plugins that can help you integrate with different stuff you might want to use.&lt;/p&gt;
&lt;p&gt;Hope you found that useful and see you. And remember - if you had any questions you can always write a comment or &lt;a href=&quot;https://pydyniak.com/contact/&quot; target=&quot;_self&quot; rel=&quot;nofollow&quot;&gt;contact me&lt;/a&gt;.&lt;/p&gt;</content:encoded></item></channel></rss>