LiferayLiferay/es/c/blogs/rss2024-03-29T15:57:39Z2024-03-29T15:57:39ZLeveraging the Elasticsearch Diagnostics UtilityChris Mount/es/c/blogs/rss&entryId=1225074202024-03-28T17:16:21Z2024-03-28T16:33:00Z<p data-renderer-start-pos="3">The first thing we always examine when
troubleshooting any issue is the log files and the same is true with
an Elasticsearch cluster. However, Elasticsearch also provides an
excellent robust set of Rest APIs as well. These APIs are extremely
helpful in understanding details about the cluster that are not
present in the logs, such as number of shards, size of indexes,
unassigned shards and more.</p>
<p data-renderer-start-pos="402">Elasticsearch administration is
not my specialty, but I have learned to depend on a number of the <a
data-renderer-mark="true" data-testid="link-with-safety"
href="https://www.elastic.co/guide/en/elasticsearch/reference/7.17/cat.html"
title="https://www.elastic.co/guide/en/elasticsearch/reference/7.17/cat.html">CAT
APIs</a> that I usually <em>curl</em> one-by-one when troubleshooting
a cluster. However, would it not be great to automatically execute a
fuller set of the APIs all at once when looking into an issue and save
them to files? I think so, especially when referring back and forth
from the results of the different APIs.</p>
<p data-renderer-start-pos="806">Well, I recently ran across the
following article, <a data-renderer-mark="true"
data-testid="link-with-safety"
href="https://www.elastic.co/blog/why-does-elastic-support-keep-asking-for-diagnostic-files"
title="https://www.elastic.co/blog/why-does-elastic-support-keep-asking-for-diagnostic-files">https://www.elastic.co/blog/why-does-elastic-support-keep-asking-for-diagnostic-files</a>
on Elastics’s Support Diagnostic Utility. This tool does exactly that,
it automates the execution of a complete list of APIs in order to help
Elastic's Support troubleshoot and advise on cluster issues. The
results are stored in json and text files and then nicely zipped up at
the end of the run. You can read the article for more details about
the tool, but I wanted to demonstrate how easy it is to setup and use
and more specifically in Liferay’s <a data-renderer-mark="true"
data-testid="link-with-safety"
href="https://www.liferay.com/offerings#liferay-paas">PaaS</a> environment.</p>
<p data-renderer-start-pos="806"> </p>
<h3>Setup & Execution</h3>
<ol> <li> <p data-renderer-start-pos="1434">In the Liferay Cloud
Console, navigate to the shell of a Liferay service container.</p>
</li> <li> <p data-renderer-start-pos="1521">From the Liferay Home
directory(<em> /opt/liferay</em>), make a directory for the
diagnostics tool and diagnostic files, e.g. <em>mkdir
es-diagnostics</em> </p> </li> <li> <p
data-renderer-start-pos="1662">Next, from the tool’s github
repository, find and copy the link to the most recent
release, <a data-renderer-mark="true"
data-testid="link-with-safety"
href="https://github.com/elastic/support-diagnostics"
title="https://github.com/elastic/support-diagnostics">https://github.com/elastic/support-diagnostics</a></p>
</li> <li> <p data-renderer-start-pos="1805">Use the <em>curl</em>
command to download the diagnostics zip file using the copied
link.</p> <ol> <li> <p data-renderer-start-pos="1889">
<em>curl -LJO </em> <a data-renderer-mark="true"
data-testid="link-with-safety"
href="https://github.com/elastic/support-diagnostics/releases/download/v8.5.0/diagnostics-8.5.0-dist.zip"
title="https://github.com/elastic/support-diagnostics/releases/download/v8.5.0/diagnostics-8.5.0-dist.zip">https://github.com/elastic/support-diagnostics/releases/download/v8.5.0/diagnostics-8.5.0-dist.zip</a>
</p> </li> </ol> </li> <li> <p
data-renderer-start-pos="2004">Unzip the zip file and change
directory inside the resulting folder (e.g.
<em>diagnostics-8.5.0</em>)</p> </li> <li> <p
data-renderer-start-pos="2100">Lastly, execute the tool. There are
several parameters that can be passed on the command line or it
can be ran interactively. More details can be found in the
project’s README, but it should be possible to execute the tool
with the following basic parameters.</p> <ol> <li> <p
data-renderer-start-pos="2363"> <em>./diagnostics.sh
--type api -h search</em> </p> </li> </ol> </li> </ol>
<p data-renderer-start-pos="2363"> </p>
<p data-renderer-start-pos="2363"> <em> <img
data-fileentryid="122507426"
src="https://liferay.dev/documents/portlet_file_entry/14/Screenshot+2024-03-27+at+5.18.57+PM.png/31078b6e-42d8-283f-ed80-ba712fe93033">
</em> <br> </p>
<p data-renderer-start-pos="2363"> </p>
<p data-renderer-start-pos="2415">After execution a new date
stamped archive will be created in the current directory, e.g.
<em>api-diagnostics-20240327-191750.zip. </em>This archive will
contain several json and text files with the results of the
Elasticsearch APIs that were invoked.</p>
<h3>Downloading the Results</h3>
<p data-renderer-start-pos="2684">To download results there
basically two options. The first is to move the archive to the
<em>/opt/liferay/data </em>directory, which is persisted and gets
backed up by the Backup Service. This would allow the archive files to
be downloaded as part of a backup. This might be helpful if you were
running the tool multiple times, creating several diagnostic archives.
However, the simplest is to move it to the tomcat webapps folder.</p>
<ol> <li> <p data-renderer-start-pos="3110">Create a directory in
the Tomcat webapps directory to contain the diagnostic archives,
e.g. <em>mkdir tomcat/webapps/esdiag/</em> </p> </li> <li>
<p data-renderer-start-pos="3234">Move the archive, e.g.
<em>mv api-diagnostics-20240327-191750.zip
/opt/liferay/tomcat/webapps/esdiag/</em> </p> </li> <li> <p
data-renderer-start-pos="3335">Lastly, open a browser tab
and navigate to the Liferay site, adding a
/esdiag/<em>ARCHIVE_NAME</em> on the end of the url. For example,
<a data-renderer-mark="true" data-testid="link-with-safety"
href="https://webserver-lfrtest-dev.lfr.cloud/esdiag/api-diagnostics-20240327-191750.zip"
title="https://webserver-lfrtest-dev.lfr.cloud/esdiag/api-diagnostics-20240327-191750.zip"><em>https://webserver-lfrtest-dev.lfr.cloud/esdiag/api-diagnostics-20240327-191750.zip</em>
</a></p> </li> </ol>
<p data-renderer-start-pos="3548">After the file is downloaded, the
diagnostic results can be viewed more easily in an IDE or tool of your
choice or even shared with other team mates.</p>
<p data-renderer-start-pos="3548"> </p>
<p data-renderer-start-pos="3548"> <img
data-fileentryid="122507435"
src="https://liferay.dev/documents/portlet_file_entry/14/Screenshot+2024-03-27+at+9.31.21+PM.png/2a399b33-e652-fc2e-a5bf-ec043060bd57">
<br> </p>
<p data-renderer-start-pos="3548">One final note, this installs the
diagnostic tool in the Liferay container’s ephemeral storage.
Therefore, the installation steps would need to be repeated if the
container is deleted, which is in a way self cleanup.</p>Chris Mount2024-03-28T16:33:00ZTargeting Quarterly ReleasesDavid H Nebinger/es/c/blogs/rss&entryId=1224792992024-03-26T18:06:41Z2024-03-26T15:41:00Z<div class="overflow-auto portlet-msg-error">I have to start with an
apology. I'm publishing this blog early because parts are ready to go
now but some parts are not yet ready. I felt it was too important to
hold off waiting for the final parts to be ready before
publishing...<br> <br> This will be a living blog post, it will get
updates on it as soon as I hear something different, but in the mean
time the content that is here is worth sharing sooner rather than later...</div>
<p>So Liferay has been doing quarterly releases of DXP for some
time now. 2023 Q3 was the first quarterly release, followed by 2023
Q4, and recently we released 2024 Q1.</p>
<p>However, there hasn't been a way to target those releases in a
Liferay workspace. You had to target U92 instead of 2023.Q3, U102
instead of 2023.Q4, and U112 instead of 2024.Q1.</p>
<p>We have finally finished the tooling updates to support
targeting the Q releases.</p>
<p>To target them in your own workspaces will involve the following...</p>
<h1> <a name="blade">Update Blade</a></h1>
<p>There is a new update to Blade that will allow it to list the
quarterly releases and allow you to pick a release when initializing a
new workspace.</p>
<p>To update blade, from the command line issue the <code>blade
update</code> command and it will take care of the rest.</p>
<div class="overflow-auto portlet-msg-alert">Actually this update
is not ready yet. My understanding is the team is looking for the
week of April 8th, 2024 as the release date. As soon as I know it is
available, I'll update the blog post...</div>
<h1> <a name="ides">Update IDE Plugins</a></h1>
<p>Whether using Intellij, Eclipse, Liferay IDE, or Liferay
Developer Studio, you'll need to update the Liferay plugins to make
them aware of the new Blade and new quarterly releases.</p>
<p>I'm not going to go into instructions here, refer to your tool
of choice to update the plugins.</p>
<div class="overflow-auto portlet-msg-alert">Actually these updates
are not yet ready and, if I have my way, might never be ready. See <a
href="https://liferay.dev/blogs/-/blogs/ides-to-be-or-not-to-be">https://liferay.dev/blogs/-/blogs/ides-to-be-or-not-to-be</a>
why the updates might not happen.<br> <br> I don't know when/if they
will get updated, but I didn't want to hold the blog post waiting to see.</div>
<h1> <a name="workspace-plugin">Update Liferay Workspace Plugin Version</a></h1>
<p>In your Gradle workspaces, you need to update the version of the
Liferay Workspace plugin.</p>
<p>Open the <code>settings.gradle</code> file and set the version
of the <code>com.liferay.gradle.plugins.workspace</code> line item
to <code>10.0.2</code> (or newer, if available).</p>
<div class="overflow-auto portlet-msg-info">To find the latest
available version, check here: <a
href="https://repository-cdn.liferay.com/nexus/content/repositories/liferay-public-releases/com/liferay/com.liferay.gradle.plugins.workspace/">https://repository-cdn.liferay.com/nexus/content/repositories/liferay-public-releases/com/liferay/com.liferay.gradle.plugins.workspace/</a>.
Note that the list is sorted alphabetically, not numerically, so be
careful looking for the 10.x versions.</div>
<p>Note that if you have an older Gradle 6 (or earlier) workspace,
you will need to update it to use Gradle 7 instead. You can find
instructions for upgrading to Gradle 7 here: <a href="https://liferay.dev/blogs/-/blogs/gradle-7-is-here">https://liferay.dev/blogs/-/blogs/gradle-7-is-here</a></p>
<div class="overflow-auto portlet-msg-error">Developers sometimes
report missing artifacts from <a
href="http://mvnrepository.com">mvnrepository.com</a>. We try to keep
<a href="http://mvnrepository.com">mvnrepository.com</a> up to date,
but have faced issues in the past that left <a
href="http://mvnrepository.com">mvnrepository.com</a> out of sync. The
official position is that developers should use and rely upon <a
href="http://repository-cdn.liferay.com">repository-cdn.liferay.com</a>
for all Liferay artifacts and we do not guarantee <a
href="http://mvnrepository.com">mvnrepository.com</a> will be
maintained in a timely fashion or up to date.</div>
<h1> <a name="workspace-product">Update Workspace Product</a></h1>
<p>The final requirement is to modify the
<code>gradle.properties</code> file so you can target a quarterly release.</p>
<p>In the file, you're going to be setting a line like:</p>
<pre>
liferay.workspace.product=dxp-2024.q1.1
</pre>
<p>The other quarterly releases from 2023 Q3 and 2023 Q4 are also
available. If a specific version you need is not available, open a
support ticket to request it.</p>
<div class="overflow-auto portlet-msg-info">To find the available
workspace product values, check here: <a href="https://releases.liferay.com/releases.json">https://releases.liferay.com/releases.json</a></div>
<h1> <a name="early-adoption">What to do before Blade/IDE Updates</a></h1>
<p>As I stated at the top of the blog, I've posted this early...</p>
<p>At this point in time Blade has not been updated, nor have the
IDEs/plugins been updated.</p>
<p>However, you can still target the quarterly releases... Here's
the steps:</p>
<ol> <li>Use blade to create a new workspace, target the latest
available U-bundle.</li> <li>Edit the <code>settings.gradle</code>
file, change the workspace plugin to <code>10.0.2</code> (or
newer).</li> <li>Edit the <code>gradle.properties</code> file and set
the <code>liferay.workspace.product</code> key as outlined in the
previous section.</li> </ol>
<p>The current blade templates should all work unchanged, and using
the gradle wrapper and the updated workspace plugin should leverage
the right quarterly versions.</p>
<p>This is a short-term workaround until Blade and the IDEs/plugins
have been updated. When that happens, these instructions will no
longer be necessary.</p>David H Nebinger2024-03-26T15:41:00ZHow to Update Email Address and Screen Name with Groovy Scriptssaravanan muniraj/es/c/blogs/rss&entryId=1224971922024-03-25T13:06:54Z2024-03-24T20:54:00Z<p>If you, as an admin user, wish to modify the screen name or email
address of site members, you will need to input the password. It is
possible that the admin user may not be aware of the password.<img
data-fileentryid="122497207"
src="https://liferay.dev/documents/portlet_file_entry/14/Liferay-comfirm-password+%281%29.jpg/63f1f662-4ff2-49f2-7ec3-40b72ab821ac"
style=""> </p>
<p> </p>
<p> </p>
<p> <a>Groovy</a> is a scripting language with Java-like syntax
for the Java platform. check this page how to run the <a>groovy
scripts in Liferay</a></p>
<p> </p>
<p>Groovy is a scripting language that resembles Java in terms of
syntax and is designed for use with the Java platform. You can find
instructions on how to execute Groovy scripts in Liferay on this page.</p>
<p> </p>
<p> </p>
<p>Using Groovy scripts, admin users can update the screen name.
Here is a simple script that uses the updateScreenName method
of <code>UserLocalServiceUtil</code>. <code>****</code> needs to be
replaced with the <em>long</em> <code>userID</code> and replace ####
with Screen Name</p>
<p>By utilizing Groovy scripts, administrator users have the
ability to update the screen name. Below is a straightforward script
that employs the UserLocalServiceUtil's updateScreenName method.
Replace **** with the lengthy userID and #### with the desired
screen name.</p>
<p>import com.liferay.portal.kernel.service.UserLocalServiceUtil;</p>
<p>import com.liferay.portal.kernel.model.User;</p>
<p> </p>
<p>try</p>
<p>{</p>
<p>UserLocalServiceUtil.updateScreenName(****,"####");</p>
<p>}</p>
<p>catch(Exception e)</p>
<p>{</p>
<p>out.println("ERROR is"+e.getMessage());</p>
<p>}</p>
<p> </p>
<p>Using Groovy scripts, admin users can update the Email address.
Here is a simple script that uses the updateEmailAddress method
of <code>UserLocalServiceUtil</code>. <code>****</code> needs to be
replaced with the <em>long</em> <code>userID</code> .</p>
<p>Through the use of Groovy scripts, administrator users can also
update the email address. Take a look at this simple script that
utilizes the UserLocalServiceUtil's updateEmailAddress method. Replace
**** with the lengthy userID.</p>
<p> </p>
<p>import com.liferay.portal.kernel.service.UserLocalServiceUtil;</p>
<p>import com.liferay.portal.kernel.model.User;</p>
<p> </p>
<p>try</p>
<p>{</p>
<p> </p>
<p>User existingUser = UserLocalServiceUtil.getUser(****);</p>
<p>UserLocalServiceUtil.updateEmailAddress(existingUser.getUserId(),existingUser.getPassword(),"test@testmail.com","test@testmail.com");</p>
<p> </p>
<p>}</p>
<p>catch(Exception e)</p>
<p>{</p>
<p>out.println("ERROR is"+e.getMessage());</p>
<p>}</p>
<p> </p>saravanan muniraj2024-03-24T20:54:00ZIDEs: To Be or Not To BeDavid H Nebinger/es/c/blogs/rss&entryId=1224865192024-03-22T22:02:29Z2024-03-22T15:46:00Z<h1> <a name="history">History of the Liferay IDEs</a></h1>
<p>I first started developing for Liferay version 4.2.3. Back then,
we used what was called the "extension environment" to
handle Liferay customization. This was an Ant-based environment with
fairly complicated build scripts that was responsible for building
the deployable Liferay.</p>
<p>And we were building portlets per the portlet specification,
creating portlet war files and ended up being many different XML files
to edit and maintain.</p>
<p>And of course there was Service Builder, and it had its own XML
files to define entities and spring beans.</p>
<p>With Liferay 5, we switch to the Liferay SDK, another Ant-based
environment. The builds of artifacts like themes, hooks and portlets
happened outside of the Liferay tree, but there was still a lot of XML
files to manage.</p>
<p>Going into Liferay 6 we had an updated SDK, but all of the XML
files were still in the picture.</p>
<p>Along the way we finally got Maven archetypes for building
Liferay assets so we could get out of the SDK, but we still had all
of the XML files that we had to deal with.</p>
<p>Throughout these different Liferay version changes, Liferay
realized that they could improve developer productivity by creating a
custom IDE with friendly UIs to replace the direct XML editing.</p>
<p>First we were all using the Liferay IDE (a regular Eclipse
plugin) or we were using Liferay Developer Studio or LDS (an
enhanced version of the IDE for Liferay EE developers). Once
Intellij came out and developers started to upgrade (that comment is
for you, Olaf!), Liferay created a plugin for Intellij.</p>
<h1> <a name="osgi">OSGi Changes Everything</a></h1>
<p>In Liferay 7, OSGi was fully embraced. Liferay even changed how
portlets were packaged, dropping the portlet war files with lots of
XML and moving to OSGi jars and classes with <code>@Component</code>
annotation properties.</p>
<p>At the same time, we got the Blade tool and the Liferay
Workspace. Blade is a CLI for creating new workspaces and
scaffolding new modules from templates, and the Liferay Workspace is
a Gradle-based (and for some time a Maven-based) area to organize
Liferay customizations and builds.</p>
<p>The only real XML file that was left over from the old days was
the Service Builder service.xml file; the rest were not necessary
unless you were still building legacy portlet wars.</p>
<p>But Liferay kept the IDEs... Now, however, instead of doing
anything useful, the UIs would present an interface but would invoke
the Blade CLI tool to do all of the work. Basically a lot of effort to
build and maintain the IDEs with <em>some</em> benefit, but very little.</p>
<h1> <a name="nail-in-the-coffin">Client Extensions are the Nail
in the Coffin</a></h1>
<p>The days of a custom Liferay IDE/plugin are numbered, and Client
Extensions are the nail in the coffin.</p>
<p>With Client Extensions, the custom UIs in the IDEs have little
value. Since you should create CX instead of OSGi modules, creating a
new module in the IDE through invoking Blade is not necessary.</p>
<p>Because we recommend using Objects in lieu of Service Builder,
the service.xml file is no longer necessary.</p>
<p>And for times where you might need an OSGi module or a Service
Builder API, since the Blade CLI is still available and the
service.xml file can be used to generate services, just because there
wouldn't be a custom UI for these things wouldn't mean the
capabilities they provided were gone.</p>
<h1> <a name="conclusion">Conclusion</a></h1>
<p>I predict that, some time soon, Liferay will announce that the
Liferay IDE, LDS and the Liferay Intellij plugin are all going to be
deprecated and no longer supported or updated.</p>
<p>For what it's worth, I'm one of the voices pushing for this. Our
dev tools team is small and has limited resources, and there are
things that we really need them to focus on (Blade CLI updates to
handle the quarterly releases, workspace plugin updates for current
and new CX types, tooling updates for JDK 21, etc.).</p>
<p>The team just doesn't have the capacity to get the things done
that we must have as well as maintain custom IDEs for things we're
not recommending anymore.</p>
<p>So yes, [I hope] it is the end of days for the custom Liferay IDEs.</p>
<p>I will miss them, of course, but their time has come and gone.</p>
<p>Share your thoughts on whether we need the Liferay IDEs below...</p>David H Nebinger2024-03-22T15:46:00ZSession Auto-Extend Properties are now in ConfigurationDavid H Nebinger/es/c/blogs/rss&entryId=1224767082024-03-18T19:16:47Z2024-03-18T18:52:00Z<p>Hey, so in <a
href="https://liferay.dev/blogs/-/blogs/introduction-to-liferay-objects">my
blog last week</a>, I indicated that I had the following in my
<code>portal-ext.properties</code> file:</p>
<pre>
session.timeout=15
session.timeout.auto.extend=true
session.timeout.warning=0
session.timeout.auto.extend.offset=300
</pre>
<p>As I was writing the blog and flipping back and forth from the
post to the browser where I was building stuff, some times I was
still being logged out.</p>
<p>I didn't really pay much attention to it, I mean it's the
typical annoyance we all deal with most of the time, so I just
logged in and then continued working on the blog...</p>
<p>Well, today my friend and coworker <strong>Jorge Diaz</strong>
reached out to me and let me know that those properties, they don't
work anymore...</p>
<p>Instead, as of Liferay Portal GA104 and Liferay DXP 2024.Q1,
session auto-extend properties have been moved into Configuration.</p>
<p>You can set the auto-extend from either Site Configuration (on a
per-site basis), Instance Settings (on an all-sites-per-instance
basis), or even at System Settings (to apply to everything).</p>
<p>In fact, here's my screen cap after enabling the Auto Extend in
the Instance Settings:</p>
<p> <img data-fileentryid="122476717"
src="https://liferay.dev/documents/portlet_file_entry/14/session-timeout.png/d6d33239-53a3-5d67-5c97-3cb10771ef63"> </p>
<p>I can confirm that the session properties from
<code>portal-ext.properties</code> are <em>
<strong>completely</strong> </em> ignored. This is unfortunate, I
would have preferred if the properties were used to at least set the
initial configuration, but that doesn't seem to be the case.</p>
<p>If you need auto-extend on the session, you'll have to go in and
<strong>manually</strong> enable it (or you could use a .config file
to push around to all environments).</p>
<div class="overflow-auto portlet-msg-alert">To change the session
timeout from the default 15 minutes still requires editing the web.xml
file on each node and restarting the app server.</div>David H Nebinger2024-03-18T18:52:00ZIntroduction to Liferay ObjectsDavid H Nebinger/es/c/blogs/rss&entryId=1224704092024-03-16T18:41:56Z2024-03-16T17:42:00Z<h1> <a name="introduction">Introduction</a></h1>
<p>Wow, I've blogged about Liferay Objects a lot lately. Talked
about them too. Also am working on developing materials for
learn.liferay.com about Objects.</p>
<p>But one thing I haven't seen is an introduction to objects
suitable for long-time Liferay users or new Liferay users, so I
thought I'd take time to write one (since my blog ideas well seems
to be running dry again).</p>
<p>So that's what this blog post is going to be, a step-by-step
guide towards building an application using Liferay Objects.</p>
<p>It's also going to highlight Liferay's low code/no code
proposition, so you'll be able to put those things to the test also.</p>
<p>So let's go ahead and dive in!</p>
<h1> <a name="preparation">Preparation</a></h1>
<p>I didn't want any baggage, so I started by going <a
href="https://www.liferay.com/downloads-community">here</a> and
downloading Liferay Portal 7.4 GA112:</p>
<p> <img data-fileentryid="122470417"
src="https://liferay.dev/documents/portlet_file_entry/14/objects-01.png/5cfb2b97-1f7f-dded-687a-9e6d26701e6c"> </p>
<p>I'm going to do this post using Liferay CE 7.4, but you could
just as easily do this in Liferay DXP, on Liferay PaaS and also
Liferay SaaS.</p>
<p>And you don't have to be on the latest version, although I do
recommend it. If you were to go through this step-by-step guide using
GA55, for example, it might work or you might fail or you might find
something missing. I know that this guide will work for GA112,
anything else will be up to you.</p>
<p>Also, as you can see I downloaded the bundle with Tomcat. You
can also do this using the Docker image, although you have to have
the persistent store set up so your data is retained across
container restarts (if you do it all in one sitting, you don't have
to worry about restarts).</p>
<p>Before starting the bundle for the first time, I did set up my
portal-ext.properties file to contain the following:</p>
<pre>
setup.wizard.enabled=false
layout.show.portlet.access.denied=false
layout.user.private.layouts.enabled=false
layout.user.public.layouts.enabled=false
session.timeout=15
session.timeout.auto.extend=true
session.timeout.warning=0
session.timeout.auto.extend.offset=300
terms.of.use.required=false
users.reminder.queries.enabled=false
field.enable.com.liferay.portal.kernel.model.Contact.male=false
field.enable.com.liferay.portal.kernel.model.Contact.birthday=false
include-and-override=portal-developer.properties
dl.file.indexing.max.size=419430400
</pre>
<p>Nothing really special here, although I am including
portal-developer.properties as well as setting the session auto-extend
settings (so I can log in and walk away but still remained logged in;
I wouldn't do this in proper environments, but it works great on my
local workstation).</p>
<p>Then I started up Tomcat, logged in using the admin credentials,
changed my password, and I was good to go!</p>
<p>To make sure we're on the same page, here's the screenshot of
the home page, and I've highlighted the Waffle menu (I'll be
referring to that often) on the right side as well as the Site menu
(also referring to that a lot) on the left side.</p>
<p> <img data-fileentryid="122470437"
src="https://liferay.dev/documents/portlet_file_entry/14/objects-02.png/9f5b7da0-9ab3-24cc-0402-593ebddaf853"> </p>
<p>Additionally, sometimes I refer to the Peapod menu: <img
data-fileentryid="122471965"
src="https://liferay.dev/documents/portlet_file_entry/14/objects-43.png/de6159c4-3732-04da-1130-877ee1f85087">
Normally you'll find this in the upper right corner, sometimes by
itself where the Waffle menu normally is, sometimes it will be amongst
other menu icons.</p>
<h1> <a name="step-1">Step 1: Create Masterclass Site</a></h1>
<p>Now we could just as easily do this in the default Liferay site,
but it will be a bit more impressive if we start from a site that is a
bit prettier to look at.</p>
<p>Go to the Waffle menu, then to the Control Panel, and then to Sites.</p>
<p>Click the Add button and pick the Masterclass site template.
Give your new site whatever name you want, I was original and called
mine Masterclass.</p>
<p>After the site is created, you'll end up on the Site Settings
control panel. Click on the Site menu, then on the Home button to
bring up the main site page.</p>
<p> <img data-fileentryid="122470450"
src="https://liferay.dev/documents/portlet_file_entry/14/objects-03.png/81439425-9735-a78d-a167-cc861504bf47"> </p>
<p>If you're unfamiliar with the Masterclass site template, take a
few moments to look around. Basically you'll find it is basically a
brochure site for an online learning platform. Check out the home
page, the blog page, and also go to the user menu (upper-right corner
of the content area, below the bar w/ the Waffle menu) and then to the
My Learning page.</p>
<p>The My Learning page is part of the authenticated user space.
It's less flashy and more business like.</p>
<p>Next, go to the Site menu and specifically check out the
<b>Design</b> area (Style Books, Fragments, and Page Templates) as
well as the <strong>Site Builder</strong> area (Pages, Navigation
Menus, and Collections) to see how the site is constructed.</p>
<p>Note the use of Liferay modern site building tools to build a
good looking site that doesn't have a custom theme (yet looks
nothing like Classic that it uses), custom Fragments for the
presentation, etc.</p>
<p>When you're ready, move on to the next step...</p>
<h1> <a name="step-2">Step 2: Review Application Requirements</a></h1>
<p>Okay, so we're not really going to do much in this step besides
introducing what our application is going to be.</p>
<p>Our goal is to allow an authenticated user to register for an
online course as well as view the courses they have signed up for.
Registrations must go through a custom business process where a Course
Administrator (custom role) must accept the registration. Users that
are Course Administrators can see the full list of course
registrations and can approve or deny registrations.</p>
<p>So that's a pretty basic explanation of what we're going to
build, let's think for a minute about how we might have built this
using legacy Liferay techniques...</p>
<p>We know that we're going to be working with an entity that we'll
call a CourseRegistration. Three ways we might consider handling this
entity would have been:</p>
<p>Use ServiceBuilder to create the entity. Would require a Java
developer, a Liferay workspace for the modules, a developer to build
the UI either as a regular JSP portlet or maybe we go all in with a
RESTBuilder implementation and then a React portlet application.
Either way it is yet more development resources required to bring this
thing to life. Oh and we'll need a second UI for the Course
Administrator to see the list and approve/reject the registrations.</p>
<p>Structured Web Content. Yes we could build a web content
structure and then permission it so users can create new articles,
then leverage FreeMarker templates to display single articles and
another FreeMarker template for the Asset Publisher to handle the
listing. Would get kind of hacky though handling the business
process approval, but maybe we attempt to overlay it into Liferay Workflows?</p>
<p>Liferay Forms. Forms seems kind of obvious since we need to
collect registration details, that's what forms are for, right? We'd
still need to figure out how to handle the list for Course
Administrators and the approval process (Liferay Workflows again?).</p>
<p>Ultimately there are reasons why we wouldn't want to choose any
of these approaches. They'll cost us some treasure (money, time,
resources), we'd be using Liferay in ways it's not meant to be used
(Liferay Workflow as our business process solution), we would probably
need to customize a part of Liferay that would be difficult to
maintain (i.e. overriding something in Forms or Web Content), ...</p>
<p>So let's discard these old ways and build our application using
Liferay Objects. But first, some more pre-work now that we know parts
of what our application is going to need...</p>
<h1> <a name="step-3">Step 3: Complete Additional Setup</a></h1>
<p>We now have some more setup work to complete for our application.</p>
<p>First, we know that we need a Course Administrator role that
will be assigned to some users and will give them additional privileges.</p>
<p> <img data-fileentryid="122470814"
src="https://liferay.dev/documents/portlet_file_entry/14/objects-04.png/b2edcc01-60ff-dc76-50e1-c929e0e15d04"
style="height: auto;width: 359.0px;display: block;margin-left: auto;margin-right: auto;"
width="359"> </p>
<p>Additionally we should probably create some user accounts so
we'll be able to test the different personas.</p>
<p> <img data-fileentryid="122470825"
src="https://liferay.dev/documents/portlet_file_entry/14/objects-05.png/0b9ba6c1-b99b-122c-5593-50ec432589e1"
style="height: auto;width: 396.0px;display: block;margin-left: auto;margin-right: auto;"
width="396"> </p>
<p>There are two student accounts that are regular users and are
members of the Masterclass site. There is one course admin account
which is also a member of the Masterclass site, but they have been
assigned the Course Administrator role.</p>
<p>We're also going to create a couple of Picklists. To add a
Picklist, go to the Waffle menu, then to Control Panel, and under the
Object category select Picklists.</p>
<p>The first Picklist is going to be the Upcoming Course picklist.</p>
<p> <img data-fileentryid="122470845"
src="https://liferay.dev/documents/portlet_file_entry/14/objects-06.png/6b9f47f0-f472-e12e-275f-4dc00c410ac8"
style="height: auto;width: 460.0px;display: block;margin-left: auto;margin-right: auto;"
width="460"> </p>
<p>After creating the Picklist, click on it to open the right-side
flyout and then add some upcoming courses by clicking on the Add
button. I added six to mine, but you can add any number you want.</p>
<p> <img data-fileentryid="122470856"
src="https://liferay.dev/documents/portlet_file_entry/14/objects-10.png/7e4c75e5-ccad-5442-bd84-4b9193b18d3f"
style="height: auto;width: 405.0px;display: block;margin-left: auto;margin-right: auto;"
width="405"> </p>
<p> <img data-fileentryid="122470978"
src="https://liferay.dev/documents/portlet_file_entry/14/objects-11.png/e2188fbc-3f07-e8d3-a95c-dccf033877b7"> </p>
<p>The next Picklist is the Registration Status.</p>
<p> <img data-fileentryid="122470989"
src="https://liferay.dev/documents/portlet_file_entry/14/objects-08.png/b6ae7906-e00d-86e4-c783-15ffb29a05fb"
style="height: auto;width: 432.0px;display: block;margin-left: auto;margin-right: auto;"
width="432"> </p>
<p>Here you should add three items, Pending, Registered, and Denied.</p>
<p> <img data-fileentryid="122471000"
src="https://liferay.dev/documents/portlet_file_entry/14/objects-09.png/47e52fcc-d720-8e49-3ee8-52def6b36931"
style="height: auto;width: 612.0px;display: block;margin-left: auto;margin-right: auto;"
width="612"> </p>
<p>With these things ready, we can now move on to the next step,
defining the custom Object.</p>
<h1> <a name="step-4">Step 4: Defining the Course Registration Object</a></h1>
<p>To create the new Object, go to the Waffle menu, then to Control
Panel, then under the Object category choose Objects. This is the
Object Admin control panel.</p>
<div class="overflow-auto portlet-msg-info">In GA112 or DXP
2024.Q1, the Object Folders and Model Builder feature flags are
enabled, so you'll have an advanced view that prior versions won't
have without enabling feature flags.</div>
<p>Start by creating a new Object Folder called Masterclass. This
will organize our custom objects created for the Masterclass site in
its own folder.</p>
<p> <img data-fileentryid="122471014"
src="https://liferay.dev/documents/portlet_file_entry/14/objects-12.png/5d6da125-49b4-43c0-6582-12062dcde8a2"
style="height: auto;width: 413.0px;display: block;margin-left: auto;margin-right: auto;"
width="413"> </p>
<p>Click on the Add button to create a Course Registration object.</p>
<p> <img data-fileentryid="122471025"
src="https://liferay.dev/documents/portlet_file_entry/14/objects-13.png/7f1063b7-8beb-d5fa-bc3a-80cff5e3a657"
style="height: auto;width: 393.0px;display: block;margin-left: auto;margin-right: auto;"
width="393"> </p>
<p> <img data-fileentryid="122471036"
src="https://liferay.dev/documents/portlet_file_entry/14/objects-14.png/74e0134c-fc85-d6e2-e088-f6afe7148178"> </p>
<p>Click on the <strong>Course Registration</strong> object to open
the Object Definition.</p>
<p>On the <strong>Details</strong> tab, set the
<strong>Scope</strong> to <strong>Site</strong>, set the
<strong>Panel Link</strong> to <strong>Content &
Data</strong>, and check the <strong>Enable Entry History in Audit
Framework</strong> under Configuration.</p>
<p> <img data-fileentryid="122471048"
src="https://liferay.dev/documents/portlet_file_entry/14/objects-15.png/8dcf7d4e-45cc-dfa9-8b5d-48cb77bff287"> </p>
<p>This will store all of the course registration object entries at
the site level, it will create a control panel under <strong>Content
& Data</strong> for easy reviewing, and it will also enable an
audit trail (often handy to see who changed what). You can click on
<strong>Save</strong> to save the draft, but you can't yet
<strong>Publish</strong> the object since we haven't added any fields.</p>
<p>Click on the <strong>Fields</strong> tab to add our custom
fields. We need to add three custom fields by clicking on the
<strong>Add</strong> button:</p>
<p> <img data-fileentryid="122471060"
src="https://liferay.dev/documents/portlet_file_entry/14/objects-16.png/00ce58e1-bf9e-886e-1fb1-14d6a45ca46a"
style="height: auto;width: 416.0px;display: block;margin-left: auto;margin-right: auto;"
width="416"> </p>
<p> <img data-fileentryid="122471072"
src="https://liferay.dev/documents/portlet_file_entry/14/objects-17.png/71dda2f8-fe79-8e3f-0449-7bba1952f18b"> </p>
<p> <strong>Course</strong> is a <strong>Picklist</strong> using
the <strong>Upcoming Course</strong> Picklist we created earlier.</p>
<p> <strong>Notes</strong> is just a <strong>LongText</strong>
field, nothing special there.</p>
<p> <strong>Registration Status</strong> is a
<strong>Picklist</strong> using the <strong>Registration
Status</strong> Picklist we also created earlier. Check the
<strong>Mark as State</strong> option, then for the
<strong>Default Value</strong> select the <strong>Pending</strong>
item. This is the field that we will be using the for the business
process handling for the course registration process.</p>
<p>We need to define a <strong>One to Many</strong> relationship
from <strong>User</strong> to the <strong>Course
Registration</strong>, as a user can register for many courses.</p>
<p>Click on the <strong>Relationships</strong> tab, then click on
the <strong>Add</strong> button to add a new relationship. Set the
fields as shown below, then click on the button to swap the
<strong>Course Registration</strong> and <strong>User</strong>
records so it becomes <strong>One Record of User</strong>,
<strong>Many Records Of Course Registration</strong>:</p>
<p> <img data-fileentryid="122471088"
src="https://liferay.dev/documents/portlet_file_entry/14/objects-18.png/65413e90-aa79-e366-bae3-f7bd141348a9"
style="height: auto;width: 467.0px;display: block;margin-left: auto;margin-right: auto;"
width="467"> </p>
<p>After clicking on <strong>Save</strong> you'll return to the
<strong>Relationships</strong> tab, but the new relationship will not
be shown. Click on the <strong>Fields</strong> tab to find the
new<strong> Course Attendee</strong> relationship listed:</p>
<p> <img data-fileentryid="122471100"
src="https://liferay.dev/documents/portlet_file_entry/14/objects-19.png/633757c1-c152-d838-c69c-7c7349bd4dfd"> </p>
<p>The last part in defining the Course Registration object is to
define some actions. Click on the <strong>Actions</strong> tab, then
click on the <strong>Add</strong> button to add a new action. Define
the action as below:</p>
<p> <img data-fileentryid="122471112"
src="https://liferay.dev/documents/portlet_file_entry/14/objects-20.png/fb4e0717-d7ca-f395-1295-feaf7cb5b973"
style="height: auto;width: 424.0px;display: block;margin-left: auto;margin-right: auto;"
width="424">On the <strong>Action Builder</strong> tab, set as follows:</p>
<p> <img data-fileentryid="122471123"
src="https://liferay.dev/documents/portlet_file_entry/14/objects-21.png/d8f2b65b-c36f-b9dc-c745-dbf2bc9b2a56"
style="height: auto;width: 563.664px;display: block;margin-left: auto;margin-right: auto;"> </p>
<p>This is setting up an action that will execute after the Course
Registration is added. The action itself is going to set the
<strong>Course Attendee</strong> to be the current user. This way
the current user will not have to pick themselves when entering a new
course registration, it will automatically be assigned to themselves.</p>
<p>We then add another action as below:</p>
<p> <img data-fileentryid="122471134"
src="https://liferay.dev/documents/portlet_file_entry/14/objects-22.png/767bdce9-b8d4-cbf8-8b4f-c36259855e61"
style="height: auto;width: 339.0px;display: block;margin-left: auto;margin-right: auto;"
width="339"> </p>
<p>And set the <strong>Action Builder</strong> tab as follows:</p>
<p> <img data-fileentryid="122471145"
src="https://liferay.dev/documents/portlet_file_entry/14/objects-23.png/38bbe17e-f432-5c55-754a-941dbd77c1cd"
style="height: auto;width: 541.664px;display: block;margin-left: auto;margin-right: auto;"> </p>
<p>This defines a <strong>Standalone</strong> action (an action
that can be invoked any time, anywhere) and will update the Course
Registration's <strong>Registration Status</strong>, assigning it the
value of registered.</p>
<p>Repeat this to create a <strong>Deny Registration</strong>
action which sets the <strong>Registration Status</strong> to <code>denied</code>.</p>
<p>Now our Course Registration object definition is ready. Click on
the <strong>Details</strong> tab, then click on the
<strong>Publish</strong> button to complete the object and enable it
for use. And since the object is done, we can move on to creating the
course registration page...</p>
<h1> <a name="step-5">Step 5: Creating the Course Registration Page</a></h1>
<p>The Course Registration object is ready, now the users need a
way to submit registrations for the courses.</p>
<p>Use the Waffle menu to get back to your Masterclass site, then
open the Site Menu.</p>
<p>Take a moment to go to the Content & Data section and find
your Course Registrations control panel. Initially it will be empty.
Go ahead and click on the Add button to add a course registration.</p>
<p> <img data-fileentryid="122471172"
src="https://liferay.dev/documents/portlet_file_entry/14/objects-25.png/c9d62e3f-6a18-3df0-8c60-d945c6492843"
style="height: auto;width: 513.664px;display: block;margin-left: auto;margin-right: auto;"> </p>
<p>When filling in the form, I only have to do the course dropdown
and the notes. The Course Attendee will be set to my login because
of the Action that we added, and the Registration Status defaults to Pending.</p>
<p>After adding two course registrations and then returning to the
list, I find:</p>
<p> <img data-fileentryid="122471183"
src="https://liferay.dev/documents/portlet_file_entry/14/objects-26.png/976e9d08-91bb-63c9-9de7-1b09fb4be233"> </p>
<div class="overflow-auto portlet-msg-info">Note the action
dropdown on each line item includes the Approve Registration and
Deny Registration standalone actions that we added to the object. We
could use these to approve or deny registrations.</div>
<p>Before moving on, note that the control panel works, but it's
not very practical. We don't want average users having access to the
control panel, and heck we probably don't want the Course
Administrators coming in here either. It is convenient for us as
Liferay Administrators to get to the full list, but that's about as
useful as it gets.</p>
<p>So let's create a course registration page. Go to the
<strong>Site Builder -> Pages</strong> panel, then create a
new page. Use <strong>Main-1</strong> as the page template, and call
the page <strong>Course Registration</strong>. Click the peapod
menu (found where the Waffle normally is) and choose Permissions,
then uncheck the Guest View permission since only authenticated
users can register for courses.</p>
<p>Next we'll start adding fragments to build out the presentation...</p>
<p> <img data-fileentryid="122471196"
src="https://liferay.dev/documents/portlet_file_entry/14/objects-27.png/681240b9-fac9-9998-a3c1-f36801899ee4"> </p>
<p>First I added a Container fragment so basically I could control
the padding around the contained elements.</p>
<p>Next I added a Masterclass Heading fragment (one of the custom
fragments from Masterclass) and set it to Course Registration.</p>
<p>Next I added a Form Container fragment and set it to use the
Course Registration object. This auto-created four child fragments
for each of the fields in the object plus a Submit button.</p>
<p>Since the Course Attendee would be assigned when I created the
instance, I deleted the form field for that Object field.</p>
<p>And since the default Registration Status would be Pending and
the user cannot change their own status, I removed that form field also.</p>
<p>After playing around some more with margins and padding, I came
up with the page you see above.</p>
<p>Since these are just fragments, you could take this a lot
farther, affecting the styling or replacing the regular form field
fragments with custom form field fragments, add other fragments,
basically do whatever you need to in order to get the presentation
that you're looking for.</p>
<p>I was happy with this layout, so I published the page.</p>
<div class="overflow-auto portlet-msg-error">If you see a message
like this one, it is safe to ignore.<img
data-fileentryid="122471209"
src="https://liferay.dev/documents/portlet_file_entry/14/objects-28.png/b1e23df3-e63b-6f93-b7a6-e725754296de"
style="display: block;margin-left: auto;margin-right: auto;"> <br>
The Registration Status field, since it is a State Manager field, is
required, but we deleted it from the form. You can ignore this
warning because the field does have an assigned default value, so the
warning itself is incorrect, just go ahead and click the Publish button.</div>
<p>Finally we have to go to <strong>Site Builder -> Navigation
Menus</strong> to the <strong>Main Navigation</strong> menu and add
the new Course Registration page. Place it where you feel it should go
in the navigation, there's no wrong answer here.</p>
<p>After the page is added, go ahead and go to the site, find your
page in the navigation, go there and submit a Course Registration.</p>
<p> <img data-fileentryid="122471224"
src="https://liferay.dev/documents/portlet_file_entry/14/objects-29.png/09097914-4850-ea03-655a-6eaf79be88da"> </p>
<p>When you click the <strong>Submit</strong> button, the new
Course Registration object will be added, and you can verify if you
go to the Course Registrations control panel.</p>
<p>Note, however, that this works because we are still logged in as
the Administrator. We have some work to do to enable this for regular users.</p>
<h1> <a name="step-6">Step 6: Update User Role Permissions</a></h1>
<p>If you did log out as an administrator and back in as one of
your test student accounts, the page would display and you could
populate the form, but when you clicked Submit you'd encounter an error.</p>
<p>Let's fix that now.</p>
<p>Log in as the administrator, then go to the
<strong>Waffle</strong> menu, then <strong>Control Panel</strong>,
then to <strong>Roles</strong>. Find the User role, then click on it.</p>
<p>Once in the role, click on the <strong>Define
Permissions</strong> tab.</p>
<p>In the search bar, enter "course". We're interested in
the <strong>Course Registrations</strong> item under the
<strong>Applications</strong>. We don't need to worry about the one
under <strong>Content & Data</strong> because a User is never
going to be in the control panel.</p>
<p>Check the permissions as follows, then click on
<strong>Save</strong>. Basically the only thing you're changing here
is adding the <strong>Add Object Entry</strong> permission.</p>
<p> <img data-fileentryid="122471268"
src="https://liferay.dev/documents/portlet_file_entry/14/objects-33.png/5dbf0e97-146d-cec5-8ed3-144c1cdf3b1d"> </p>
<p>Now, if you log in as one of your students, they can add course
registrations without failures.</p>
<h1> <a name="step-7">Step 7: Add My Course Registrations Page</a></h1>
<p>So now students can register for courses, but they probably also
want to see which courses they have registered for and what their
registration status is.</p>
<p>So now we will create a new My Course Registrations page. Go to
the Site menu, to <strong>Site Builder -> Pages</strong>, then
add a new page using the Private Area template, give it the name My
Course Registrations.</p>
<p>This page we will configure to show the list of course registrations.</p>
<p> <img data-fileentryid="122471252"
src="https://liferay.dev/documents/portlet_file_entry/14/objects-31.png/10a91ecb-c16c-25ae-5bab-e9e696029752"> </p>
<p>First I added a container (I always like to start with a
container), and inside of it I added a Collection Display fragment,
and I configured it to use the Course Registrations Collection
Provider (you get one of those for each Object that you create).</p>
<p>For the Collection Item, I added a 2-column Grid, and then I
added a Masterclass Text Block fragment to each column. For the left
column, I mapped it to use the Course, and the right column was
mapped to the Registration Status.</p>
<p>The page was ready, so I published it. I then went to the Site
menu, to <strong>Site Builder -> Navigation Menus</strong> and
clicked on the <strong>Private Area Navigation</strong>. I added the
My Course Registrations page.</p>
<p>Then when I navigated to the site, then to the My Learning
private area, My Course Registrations is there and when I'm on it, I
see the list of courses. Here's the view logged in as one of my students:</p>
<p> <img data-fileentryid="122471279"
src="https://liferay.dev/documents/portlet_file_entry/14/objects-32.png/8d78b30d-d813-196e-b2a7-3f11a6f485c7"> </p>
<p>I only see the courses that this student has signed up for.</p>
<p>So far, so good, but we haven't dealt with the Course Admin view yet...</p>
<h1> <a name="step-8">Step 8: Add Course Administrator View</a></h1>
<p>The course administrator gets to see all of the course
registrations and can approve or reject registrations. Let's take care
of that now.</p>
<p>First we need to update the Course Administrator role. Like we
did for the User role, go to the Define Permissions tab for the
Course Administrator role.</p>
<p>For this role, add the <strong>Add Object Entry</strong>,
<strong>action.approveRegistration</strong>,
<strong>action.denyRegistration</strong>, <strong>Update</strong>, and
<strong>View</strong> permissions, then save the role.</p>
<p>Go back to the Masterclass site, then to the Site menu, then
<strong>Design -> Page Templates</strong>, then click on the
Private Area template to edit the template. When it opens, click on
the Configure Allowed Fragments button in the drop zone, then check
the Button fragment under the Basic Components category, then click
Save and then Publish Master. This change will allow us to use a
regular button on a Private Area page, and we'll be needing that
button shortly.</p>
<p>Next, go to the Site menu, to <strong>People ->
Segments</strong>. You may not have any defined, which is fine. Click
the New button to create a new Segment, call it Course Administrators,
and give it a condition that the User has a Regular Role and select
the Course Administrators role. This will define a segment for all
users that have this particular role. If you created the sample users,
you should see the conditions map at least 1 member.</p>
<p> <img data-fileentryid="122471650"
src="https://liferay.dev/documents/portlet_file_entry/14/objects-37.png/25740682-731b-3b5b-5103-9b8d83b062f7"> </p>
<p>Go to the Site menu, to <strong>Site Builder ->
Pages</strong>, then edit the My Course Registrations page.</p>
<p>Near the top left there is the Experience dropdown. Click on it,
then click on the New Experience button.</p>
<p> <img data-fileentryid="122471639"
src="https://liferay.dev/documents/portlet_file_entry/14/objects-38.png/ee649786-241b-d3e1-5a2d-cee0c2b08f48"> </p>
<p>Create a new experience called Course Administrators and choose
the Course Administrators segment in the dropdown.</p>
<p> <img data-fileentryid="122471661"
src="https://liferay.dev/documents/portlet_file_entry/14/objects-39.png/64a35d44-80b5-9c87-4f34-a3703e0eaaa8"
style="height: auto;width: 456.0px;display: block;margin-left: auto;margin-right: auto;"
width="456"> </p>
<p>In the experiences list, select the Course Administrators
experience, then click the Up arrow to move the Course Administrators
above the Default experience.</p>
<p> <img data-fileentryid="122471672"
src="https://liferay.dev/documents/portlet_file_entry/14/objects-40.png/bf194c89-e456-fb68-3b16-56edab392091"> </p>
<p>The Course Administrators experience is now active (being edited).</p>
<p>Click outside of the Experience dialog to return to editing the page.</p>
<p>Change the grid from 2 column to 4 column. Drop a Masterclass
Text Block fragment in the new column 3, and a 2-column Grid
fragment into column 4.</p>
<p> <img data-fileentryid="122471684"
src="https://liferay.dev/documents/portlet_file_entry/14/objects-41.png/9dba85e9-0f53-3d79-e48a-d0349bbc6dfa"> </p>
<p>Next I dropped Button fragments into each column in the grid in
column 4.</p>
<p>I mapped the text block in column 1 to the Course Attendee, I
mapped the one in column 2 to the Course, and I mapped the one in
column 3 to the Registration Status so I would see what the current
status was before clicking the buttons.</p>
<p>For each button, on the Button Options I changed the Type to be
an Action, then for each of the child action elements, I set the
Mapping to the action to perform, and the Action tab was also set to
the action to perform and I also checked the "Reload Page After
Success" item. This way, whether you clicked on Approve or Deny,
the page should refresh and show the new status right away.</p>
<p>At this point the page was ready, so I published it.</p>
<p>And when I go to the site and navigate to the page when I have
the Course Administrator role, I see the list and the buttons and,
after clicking some, I can see that the Registration Status has changed.</p>
<p> <img data-fileentryid="122471695"
src="https://liferay.dev/documents/portlet_file_entry/14/objects-42.png/c51d77e7-d45d-9b06-d629-87a3004d437a"> </p>
<p>So, what exactly is going on here?</p>
<p>First, if you have the Course Administrator role, the
permissions that we added to the role allow them to see all course
registrations, not just their own.</p>
<p>Second, when you land on this page, you have a specific
experience that you get, the one with the buttons, but if you're not
part of this segment you are in the Default segment and you see the
view we created before.</p>
<h1> <a name="step-9">Step 9: Party Party, We're Done</a></h1>
<p>Wait, what?</p>
<p>Yes, that's right, we're done with our application, so now it is
time to party!</p>
<p>Well, instead of partying, let's review some of the choices that
we made and see if we couldn't have made some different choices...</p>
<p>In <a href="#step-1">Step 1</a>, we created a new site using the
Masterclass template. Other than some of the custom fragments that we
used, there was nothing in this blog post that couldn't be done with
any other site. Everything we did, from Objects to the different
Fragments, Content Pages, heck even the roles and Segments, all of
those are OOTB features that can be used on new or existing sites.</p>
<p>In <a href="#step-3">Step 3</a>, we created a custom Course
Administrator role, but we probably should have also created a Student
role and added permissions to it, rather than to the User role. Why?
Well every authenticated user has the User role, but not every User
should be able to register for courses. If we used a special Student
role for that, we would have been better able to control who could add
new Course Registrations and who couldn't. Plus, using the technique
from <a href="#step-8">Step 8</a>, we could have isolated the Student
experience so only they could get to the form, and a non-Student
experience could suggest signing up or whatever is necessary to become
a Student.</p>
<p>Also in <a href="#step-3">Step 3</a>, we used a Picklist for
upcoming courses, but this isn't really practical, yeah? I mean, we
actually would have been better served creating a Course object which
would allow the Course Admin to easily create new courses, then have
the one to many relationship from Course to Course Registration. It
would have changed our implementation and obviously the fragments
would have needed to change for the form (maybe we'd even want to use
a custom fragment for an enhanced course selection). </p>
<p>In <a href="#step-4">Step 4</a>, we did not define any
<strong>Layouts</strong> or <strong>Views</strong> for the object.
Defining these will control how the control panel looks, both for the
list as well as for the form, and it can also affect the generated
Course Registration widget if you enabled that item on the
<strong>Details</strong> tab. I tend to create a
<strong>Layout</strong> and a <strong>View</strong>, both marked as
default, to ensure the control panel has what I need and nothing more,
but it is optional, especially if you have no plans of using the
control panel at all.</p>
<p>Also in <a href="#step-4">Step 4</a> we didn't really look at
the State Manager tab. It's this tab that controls the transition
allowed between the states for the Registration Status item. When we
added the field, a new State transition was defined for us allowing
transition from any state to any other state. In a true BPM, we
probably wouldn't want to allow for example a Registered course to
transition back to a Pending, so the transition list would need to
be edited to define the approved transitions. If we were to do this,
the actions we defined for approving or denying registrations would
need to be updated so they would only transition if the current
state was in an approved state for the transition.</p>
<p>In <a href="#step-5">Step 5</a>, we created a form using a Form
Container fragment and mapped fields for the Course Registration
object. Another alternative would have been to create a Form from the
Site menu, Content & Data -> Forms, but these forms tend to
have problems with the State fields. Remember I got a warning trying
to save the content page with the Form Container that didn't have a
field mapped to the Registration Status guy, and it was okay for me to
ignore the warning because the default Pending would be used. When I
tried creating a Form using this object, I could not save the Form if
the field wasn't in there. No warning this time, an error preventing
the save. I could put the field on the form, but it is marked as
required and I found no way to hide it or disable it. If you don't
have states in your Object, you can probably use a Form. Personally,
I'd recommend using a Form Container fragment unless, for some reason,
you can't. Maybe you want a multi-page form, maybe you want something
else that Forms provide that Form Containers do not yet support. In
any case, try going with a Form Container unless you can't.</p>
<p>And finally, note that this was not the only way to have
implemented this. I could just as well created a custom React element
using a Client Extension and let it use the headless APIs to list
course registrations, handle the form/data entry, approve/reject the
registrations, ... It gets away from the no code approach that we
achieved here, but if the requirements call for it? And since I based
it on Objects, the headless APIs mean my React custom element is still
going to be fairly simple as it just needs to handle the presentation,
the APIs handle the rest.</p>
<h1> <a name="conclusion">Conclusion</a></h1>
<p>So, just what did we do here?</p>
<p>We accomplished the following:</p>
<ul> <li>We defined an object to hold the course registration
data.</li> <li>We created a page with a Form fragment on it so users
could submit new course registrations.</li> <li>We created a page
with a Collection Display fragment using a Collection Provider
created automatically for the Course Registration object, allowing
us to show a list of Course Registrations and use fragments with
field mappings to the Course Registration for the display.</li>
<li>We set up different permissions on the Course Registration
object based upon roles.</li> <li>We created a view for regular
users to see the courses they registered for and the status of those
registrations.</li> <li>We are using the Object State Manager feature
to define a business process associated with the Course Registration
object, a business process which is not part of Liferay Workflow and
can conform to whatever business process we want to define.</li>
<li>We created a Course Administrator role, a corresponding segment,
and a custom experience for this segment which provided the full
list of course registrations and buttons to change the registration
status.</li> </ul>
<div class="overflow-auto portlet-msg-info">And how much code did
we write to do all of this?<br> <br> Not a single line of Java, not
a single line of FreeMarker, not a single line of Javascript...</div>
<p>This was all done using low code/no code features of Liferay,
specifically Liferay Objects and Fragments.</p>
<p>It just couldn't get any easier!</p>
<p>Hopefully you've followed along as we went through these steps.
I know it's a long blog post, but I hope you found it worthwhile to
do all of these things and have learned something that you can take
back and use in your day to day job with Liferay.</p>David H Nebinger2024-03-16T17:42:00ZDown with Asset Publisher, Long Live Collections!David H Nebinger/es/c/blogs/rss&entryId=1224702222024-03-15T19:07:22Z2024-03-15T19:02:00Z<h1> <a name="introduction">Introduction</a></h1>
<p>In a recent blog, Down with Web Contents, Long Live Objects! I
shared how Objects and Fragments could be used in lieu of structured
web contents.</p>
<p>That's great if you don't already have structured web contents though.</p>
<p>If you do have structured web contents, you likely also have
Asset Publishers and are rendering your contents using FreeMarker templates.</p>
<p>You don't really need to do this, though, because the power of
the Collection Display and other fragments are available for you, too.</p>
<h1> <a name="carousel">Returning to the Carousel</a></h1>
<p>So the blog focused on building a Carousel, and it had an Object
for the Carousel Entry and then used a pair of custom fragments for
the presentation.</p>
<p>We're going to continue that here, but we're going to use a
structured web content. I'm going to define mine here, but if you
already have a structured web content, you can use it instead.</p>
<h2> <a name="carousel-web-content-structure">Carousel Entry Web
Content Structure</a></h2>
<p>So the structure that I went with mirrored the former Object and
has a title, a summary, an image selector, and instead of a URL I used
a Link to Page for the Call To Action URL:</p>
<p> <img data-fileentryid="122469967"
src="https://liferay.dev/documents/portlet_file_entry/14/structured-content-11.png/d752a5d2-f858-2d6e-6281-a9c92bd4fffa"> </p>
<p>With the structure ready, I created a web content folder and
then three web contents based on the structure:</p>
<p> <img data-fileentryid="122470078"
src="https://liferay.dev/documents/portlet_file_entry/14/structured-content-12.png/209a1388-602c-d98a-a3cd-db9619fb154d"> </p>
<p>Here's one of the entries, but the others are similar:</p>
<p> <img data-fileentryid="122470089"
src="https://liferay.dev/documents/portlet_file_entry/14/structured-content-13.png/5f79e702-ec5d-1a5c-67f1-7928ed0b4591"> </p>
<h1> <a name="preparation">Preparing for the Carousel Display</a></h1>
<p>So the classic way that we would handle the carousel would be to
create some templates.</p>
<p>First, a web content template would be used to create the
individual cards, rendering the DOM with the background image, the
title, summary and button.</p>
<p>Next, we'd build an Application Display Template, or ADT, for
the Asset Publisher which would handle the rendering of the Carousel
itself using SwiperJS.</p>
<p>But we don't really have to do this now, yeah? I mean, we
already created the fragments in the last blog, and we can use those
fragments again in this blog...</p>
<p>Since we used a Collection Display fragment and that requires a
collection, we need to define a new collection for our web contents.</p>
<p>We can go to the Site menu, to Site Builder -> Collections,
then add a new Dynamic Collection:</p>
<p> <img data-fileentryid="122470101"
src="https://liferay.dev/documents/portlet_file_entry/14/structured-content-14.png/aeaa88f3-c4ec-bc54-cdc9-10e10fe39c86"> </p>
<p>We then need to name the new collection, I went with Carousel
Entry Web Contents:</p>
<p> <img data-fileentryid="122470113"
src="https://liferay.dev/documents/portlet_file_entry/14/structured-content-15.png/0082032b-03d3-e678-b27d-535a91e100f0"
style="height: auto;width: 470.0px;display: block;margin-left: auto;margin-right: auto;"
width="470"> </p>
<p>In the configuration for the dynamic collection, choose Web
Content Article as the Item Type and then choose the Carousel Entry
from the Item Subtype dropdown.</p>
<p> <img data-fileentryid="122470124"
src="https://liferay.dev/documents/portlet_file_entry/14/structured-content-16.png/2919762e-415e-3954-7238-c5f3ef5b1984"> </p>
<div class="overflow-auto portlet-msg-info">Interestingly enough,
the dialog for the dynamic collection is very similar to the dialog
for the Asset Publisher configuration:<br> <br> <img
data-fileentryid="122470135"
src="https://liferay.dev/documents/portlet_file_entry/14/structured-content-17.png/affaef76-d5ea-ee86-7ab5-bae28c43bb44"> </div>
<h1> <a name="reconstructing-the-carousel">Putting the Pieces
Back Together</a></h1>
<p>Now that we have a collection defined, we're now able to finish
using the Carousel and Carousel Entry fragments from the last blog,
except now we're going to pick the Carousel Entry Web Contents for the
collection, and when we use the field mapping, we are going to pick
the fields from the structure.</p>
<p>So, as in the last blog, I created a page and then dropped the
fragments in. When I added the Collection Display fragment, I picked
my Carousel Entry Web Contents. As I dropped the other fragments in, I
took care of all of the mappings. When I was finished, I had my
structure all set:</p>
<p> <img data-fileentryid="122470147"
src="https://liferay.dev/documents/portlet_file_entry/14/structured-content-18.png/a554673a-fdfd-35fc-c8d5-071df5057760"> </p>
<p>When I published the page, it worked right away:</p>
<p> <img data-fileentryid="122470158"
src="https://liferay.dev/documents/portlet_file_entry/14/structured-content-19.png/b3f1c56d-3385-0aa4-19d7-c92271a8d8ff"> </p>
<h1> <a name="conclusion">Conclusion</a></h1>
<p>Collections and Collection Display fragments and, heck,
Fragments with mapping in general are super-easy ways to get your
presentation out of FreeMarker templates, putting the responsibility
for rendering in the hands of the page designers.</p>
<p>Sometimes that will also be you, so as a developer you might be
thinking "Oh I'll just stick with FreeMarker, thanks."</p>
<p>But note how in both blogs the only skills I needed to get the
proper presentation was some basic HTML, CSS and JS skills. I didn't
have to write a single line of FreeMarker code, I didn't have to worry
about FreeMarker errors, and I didn't have to worry about how I'm
going to write FreeMarker templates in lower lanes and figure out how
to promote them all the way to production.</p>
<p>Collections and Fragments are super-powerful and effective means
for handling your presentation requirements without the need to know
or understand FreeMarker and building templates.</p>
<p>As you go forward, stop using Asset Publishers and FreeMarker
ADTs. Consider instead leveraging Collections, Collection Display
fragments and Fragments with field mappings for your presentation.</p>
<p>Of course I'd still rather do structured data as Objects instead
of Web Contents, if only for the headless APIs, lack of parsing of
web content data and the batch import/export support, but if I had
existing web contents that I needed to create a new presentation for,
I'd reach for Collections and Fragments to handle the presentation.</p>David H Nebinger2024-03-15T19:02:00ZDevCon 2024 Call For PapersDavid H Nebinger/es/c/blogs/rss&entryId=1224694672024-03-15T15:47:44Z2024-03-15T15:01:00Z<h1>DEVCON 2024 is happening!</h1>
<p> <b>Dates:</b> 11-14 November</p>
<ul> <li> <b>11 Nov:</b> Welcome Evening Reception - 7:00 pm -
10:00pm</li> <li> <b>12 Nov:</b> 9:00am - 5:30pm</li> <li>
<b>13 Nov:</b> 9:00am - 5:30pm, Party Night starting at
7.30pm</li> <li> <b>14 Nov:</b> Unconference - 9:30am -
5:00pm</li> </ul>
<p> <b>Venue:</b> Akvarium Klub - Budapest, Hungary</p>
<p> <b>Get a chance to come to DEVCON by submitting your
proposal!</b> </p>
<p>As always, there is a chance to come and present during DEVCON
by contributing your insights, learnings, and innovations related to
Liferay projects. All selected speakers will be invited to come join
us in Hungary!</p>
<p> <b> <a data-sk="tooltip_parent"
data-stringify-link="https://forms.gle/mPsssCEftZQcdd6X9"
href="https://forms.gle/mPsssCEftZQcdd6X9" target="_blank"
rel="noopener noreferrer">SUBMIT YOUR PROPOSAL HERE</a></b> </p>
<p>Submissions deadline: <b>Friday, June 28th, 2024</b>.</p>
<h2>Unconference Baby!</h2>
<p>Yes, that's right, the last day of DevCon, November 14th, is a
full day Unconference. Our good friend Olaf Kock will be leading an
exciting and informative Unconference! Personally this is my favorite
part of DevCon!</p>
<p> <strong>I plan on attending DevCon this year and look forward
to meeting you there!</strong> </p>David H Nebinger2024-03-15T15:01:00ZStaging vs PublicationsDavid H Nebinger/es/c/blogs/rss&entryId=1223887942024-03-15T15:47:15Z2024-03-15T13:42:00Z<h1> <a name="introduction">Introduction</a></h1>
<p>Recently my friend and coworker Andrew Jardine and I were
together in Rome for a Liferay Partner event where we were part of a
round table discussion about all things Liferay.</p>
<p>Even before the session, Andrew and I had already picked up our
friendly argument about Staging (me) versus Publications (Andrew) and
which one is better. For us it has always been more of a friendly jab
at each other, but it is based upon our histories with each solution.</p>
<p>For me, I ran into trouble using Publications (issues that have
long since been fixed but I think I'm still holding the issues against
Publications), and most requirements that I've been dealing with have
been easily satisfied by Staging.</p>
<p>Andrew, like many others, have been frustrated at times using
Staging and found the new Publications model a breath of fresh air.</p>
<p>Is one better than the other? Let's explore them and find out together...</p>
<p>I know this will seem more like rambling for a while, but these
background details that I'm sharing will help to explain my
conclusions and why I might suggest one vs the other.</p>
<h1> <a name="staging">Staging</a></h1>
<p>Staging has been a key part of Liferay since version 6.0, and
was actually a client-sponsored enhancement.</p>
<p>The client in question wanted a way to preview how the site
would appear before it went live. Workflow approval of assets was
not enough, it was necessary for them to review how each page would
work before it went live.</p>
<p>And staging was born. It was designed after a dead tree
publication process, wherein there would be someone responsible for
laying out the complete page, selecting the colors, fonts, content,
ads, etc, and the publisher who would review this work and either
send it back for rework or would publish it.</p>
<p>The implementation of Staging uses a duplicate of a live site.
Changes are made on the staging site, they can be reviewed and (the
way it used to work) marked as "ready for publication". When
the site is published, approved pages are copied from the staging site
to the live site. Now the "ready for publication" has been
removed, the assumption being that any page (not in draft state) is
actually ready for publication.</p>
<p>There are two forms of Staging, Local and Remote. In Local
staging, the staging site copy is in the same Liferay cluster as the
live site, and in Remote staging, the staging site copy is in a
completely different Liferay cluster. Remote staging can isolate the
cluster where changes can be made and offer heightened security and
protection, but Local staging tends to be a bit faster and not subject
to network issues during publication.</p>
<p>The internals of both forms of staging are effectively the same
- a LAR export from the staging site is made, and the import of
the LAR happens into the live site. The LAR export/import is
automatic, whether local or remote.</p>
<p>And in every version of Liferay since it was introduced, there
have been bugs in the process, and those bugs have been frustrating
for users and implementors and folks often ask why, after all of
this time, are there still bugs in staging? Shouldn't they have been
fully addressed by now?</p>
<p>To answer this, I have to pull back the curtain and share what
goes on behind the scenes...</p>
<p>The perception is that staging is pretty simple... I select one
(or more) pages from staging, I select the Publish button, those
pages are copied over to live, and now the changes are all on the
live site for real users to see.</p>
<p>Internally, though, this is a <em>really</em> complex problem.
If I select a widget page to publish, I'm not really selecting
only a page. A Liferay page is a container of portlets, each
portlet has configuration for the instance, so those things get
selected. If one or more portlets are Web Content Displays, well
they point at web contents and those may need to be published. The
web content might be based on a structure and a template, so those
would need to be published so the web content would work. The web
content can have related assets, ratings, categories, and tags, and
these items would also need to be published to live. The web content
itself may have embedded links to images or documents, so those
items would need to be published. Those files are in folders in
D&M, so those may need to be published. Documents could be based
on a custom document type, so that would need to be published...</p>
<p>What complicates things further, for all of those things that
are possibly selected to publish to a page, we really only want to
publish the things that have changed on the staging side. If the web
contents haven't changed, there's no reason to republish those again.</p>
<p>But then there's also knowing what needs to be published because
it hasn't before; the documents, for example, are in a folder
hierarchy, does the hierarchy exist on the Live side, either full or
in part? Which pieces have not been published because they need to
exist for the document to be published...</p>
<p>So there's a lot going on here, and I only selected a single
widget page... What happens if I select multiple pages? What happens
when I select content pages which are composed of fragments with
configuration, the fragments themselves, the related data the
fragments reference, ...</p>
<p>This really does blossom into quite a complex problem and the
implementation then tends to be on the fragile side.</p>
<p>And what makes it worse is if (when) Liferay is building a new
feature (such as Objects), if supporting staging is not included as a
non-functional requirement, there may be no staging support for the
feature at all, or maybe it is added in as an afterthought...</p>
<h2> <a name="staging-use-cases">Staging Use Cases</a></h2>
<p>There are a number of use cases that Staging solves:</p>
<ul> <li>You want a complete replica of the Live site to make and
approve changes.</li> <li>You want the security and protection
offered by the Remote staging setup.</li> <li>Your using local
staging or your changes happen at the Site level only and are not
dependent upon shared assets from either the Global site or Asset
Libraries.</li> </ul>
<p>That last one is important. If you are using remote staging,
assets outside of the site, i.e. those from the Global site or from
an Asset Library, will not be included in the remote staging
publication. If those assets don't exist on the remote live site,
the publication (import) will fail. This isn't an issue with local
staging because the Global site assets and the Asset Libraries will
of course be there.</p>
<h1> <a name="publications">Publications</a></h1>
<p>So the Staging mess had been around for a while and it was quite
obvious that the complexities involved in selecting and publishing all
relevant assets was not going to go away, so Liferay started to
rethink the whole Staging thing...</p>
<p>And, what started as a way to track row-level changes across all
Liferay database tables grew into what is now called Publications.</p>
<p>With Publications enabled, when a new Publication is started, as
existing records would otherwise be changed, instead they are added as
a new unique row, but they include a foreign key reference to the
current Publication.</p>
<p>This tends to be quite useful, yeah? When you want to publish a
Publication, you're basically finding every record with the
Publication reference and making that record "live".</p>
<p>This is a super-simple solution for the complexity involved in
Staging trying to identify records that need to be published, since
we're tracking individual changes, there is no discovery process necessary.</p>
<p>In fact, Publications ends up being very git-like in that you
can have multiple Publications (branches) going at the same time,
each one tracks its own changes and doesn't change "live"
(main branch) directly, each can be published (merged) separately or
even discarded...</p>
<p>But, of course, therein lies the rub - just like git, you have
to worry about <em>collisions</em> and <em>collision
resolution</em>. And not the simple case of "I'm publishing the
only Publication I have to live" kind of thing, the more
complicated "I have multiple publications and, in each
publication, I have made different changes to the same content page,
now I want to publish them all" sorts of things.</p>
<p>In the initial releases of Publications, this was one of the
things that hit me almost every time... Try to publish a
Publication, get the collision dialog, and wham, I couldn't go
further because there was no real merge process.</p>
<p>Additionally, Publications is scoped at the instance (company)
level. So if you start a publication and then start moving around to
different sites and different asset libraries making changes, those
changes would be tied to the current Publication, whether that's what
you wanted or not.</p>
<p>This was another thing that tripped me up with Publications...
I'd start a Publication for changes I was working on for a specific
site. I get an IM asking me to make some change to instance settings
or another site or an asset library... I'd drop what I was doing, go
make the change, then return to what I was working on... However,
since I was in a publication, those on the fly changes I made were
also tied to the the publication, and I had no way of doing a
"partial publish", so I basically had to exit my
Publication, re-apply the changes that were asked for, then I could
go back to my Publication to finish the work on the site, but now I
have changes from two branches and, when I am finally ready to
publish, I have the collision dialog to look forward to. And of
course this is if I actually realized that I did the changes in the
Publication, more often than not I would say "Hey what you
asked for is done" only to hear back that the requestor
couldn't find the changes, then I'd realize they were in the publication...</p>
<p>So yeah, I held those issues against Publications because I
didn't face any of those issues with Staging...</p>
<p>But, I have to admit that Publications has been getting better,
feedback like mine has been used by the team to make necessary
changes, ... The merge process has gotten much better, fewer
collisions as Publications is better able to automate the merges.
Changes that are part of a Publication can now be moved to another
Publication, so I can solve my last issue by creating a new
Publication, moving the changes I didn't want part of my current
Publication there, then I can publish that Publication on its own...</p>
<p>So yeah, things are getting better with Publications...</p>
<p>And the being instance scoped rather than site scoped? That's
actually a good thing, especially if you are following my advice to
stop using the Global site and even the regular sites for the content,
instead putting the content into Asset Libraries... Since the
Publication spans all of those, if I'm making changes across multiple
Asset Libraries related to a Publication, when I publish it all of
those changes will be published too.</p>
<h2> <a name="publications-use-cases">Publications Use Cases</a></h2>
<p>Publications has the following set of use cases:</p>
<ul> <li>You have multiple changes going on at the same time but
need to be published separately (i.e. campaigns).</li>
<li>You're following best practices by leveraging Asset
Libraries heavily but want to track and publish changes
together.</li> </ul>
<p>Publications also comes with a restriction - if you need the
security and protection by using separate clusters for editing
content, Publications is not for you. Only remote staging has this
capability, Publications does not.</p>
<h1> <a name="switching">Switching Between Staging and Publications</a></h1>
<p>So maybe after reading this blog you're thinking, "I'm
currently using Staging, but I want to give Publications a try"
or vice-versa...</p>
<p>Well, here's how you're going to switch...</p>
<h2> <a name="from-staging-to-publications">From Staging to Publications</a></h2>
<p>First thing you need to do is publish everything. When you
disable Staging, you're throwing out the whole Staging site, so
anything that is not published is going to be lost.</p>
<p>Next, you'll go to the Site menu, then <strong>Publishing ->
Staging</strong> to get to the <strong>Staging Configuration</strong>
settings. Change from Local or Remote (whichever you're on) to None
and hit Save.</p>
<p>Once you do this, your Staging site is going to be deleted and,
depending upon the size of your site, this could take a while...</p>
<p>Once it is done, go to the Waffle menu, to Applications, then to
Publications. On this dialog you can choose to Enable Publications.</p>
<p>From here, just follow the <a
href="https://learn.liferay.com/w/dxp/site-building/publishing-tools/publications">instructions
for Publications</a> and you can learn how to handle them.</p>
<h2> <a name="from-publications-to-staging">From Publications to Staging</a></h2>
<p>First thing you need to do is publish and/or revert (as
necessary). When you disable Publications, expect that your
publications (and publication history) will be discarded.</p>
<p>To disable Publications, go to the Waffle menu, to Applications,
then to Publications and uncheck the Enable Publications option.</p>
<p>This should delete your publications and history, so this might
take a while.</p>
<p>For each site or Asset Library to enable staging for, go to the
Site menu, to Publishing -> Staging to get to the Staging
Configuration settings. Change from None to Local for local staging,
or None to Remote for remote staging. If you're just wanting to test
out Staging, I'd recommend going with Local, save Remote Staging setup
for the production environments because it has some <a
href="https://learn.liferay.com/w/dxp/site-building/publishing-tools/staging/configuring-remote-live-staging">specific
configuration requirements</a>.</p>
<p>When you enable staging, the site or Asset Library is the live
version, so a copy must be made for the Staging version. This can take
some time to complete, so be prepared.</p>
<p>From here, just <a
href="https://learn.liferay.com/w/dxp/site-building/publishing-tools/staging">follow
the instructions</a> for Staging and you can learn how to handle it.</p>
<h1> <a name="conclusion">Conclusion</a></h1>
<p>So, which is better, Staging or Publications?</p>
<p>Well, it depends really upon your use case, yeah?</p>
<p>If you need remote editing, you only have one choice, Remote Staging.</p>
<p>Otherwise, and don't tell Andrew this,
<strong>Publications</strong> would probably be my go-to solution, but
only if I can be one one of the later releases where they've made lots
of improvements to collision handling, moving changes out of
publications, etc.</p>
<p>Staging is still going to work, it hasn't necessarily been
deprecated or anything, but I'm not really sure that there will ever
be a "bug free staging"... I mean, if you can set up staging
in your environment and you're only using those features that support
Staging and you can publish without problems, then you're probably
going to be okay, and you'll enjoy seeing the staging site entirely
and knowing what you'll have in Live when you publish...</p>
<p>But ultimately I have to admit that Publications is more in line
with how we should be handling assets, namely by adopting and using
Asset Libraries almost exclusively, and therefore Publications stands
out as the best fit for adhering to these kinds of best practices.</p>
<p>Before choosing, I do suggest you do your own <a
href="https://learn.liferay.com/w/dxp/site-building/publishing-tools/comparing-publishing-tools">comparison
research</a>, especially paying attention to the <a
href="https://learn.liferay.com/w/dxp/site-building/publishing-tools/comparing-publishing-tools#feature-comparison">feature
comparison</a> and the <a
href="https://learn.liferay.com/w/dxp/site-building/publishing-tools/comparing-publishing-tools#supported-pages-and-content-types">supported
types matrix</a>.</p>
<p>Finally, note that neither option directly supports Liferay
Commerce or Liferay Objects. Your only option here is going to be
workflows. This shouldn't be a problem though because neither of these
are really presentation, they're just data. The presentation aspects
(display page templates, things like that) are subject (per the
matrix) to the publishing options.</p>David H Nebinger2024-03-15T13:42:00ZLiferay Portal 7.4 GA112 and Liferay Commerce 4.0 GA112 ReleaseJamie Sammons/es/c/blogs/rss&entryId=1224657342024-03-14T22:19:14Z2024-03-14T21:48:00Z<h1>Download options</h1>
<p>Liferay Portal and Liferay Commerce share the same Bundle and
Docker image. To get started using either Liferay Portal or Liferay
Commerce, choose the best download option suited for your environment below.</p>
<h2>Docker image</h2>
<p>To use Liferay Portal 7.4 GA112:</p>
<pre>
docker run -it -p 8080:8080 liferay/portal:7.4.3.112-ga112</pre>
<h2>Bundles and other download options</h2>
<p>If you are used to binary releases, you can find the Liferay
Portal 7.4 GA112 and Liferay Commerce 4.0 GA112 release on the<a
href="https://www.liferay.com/downloads-community" target="_blank"
rel="noopener noreferrer"> download</a> page. If you need
additional files (for example, the source code, or dependency
libraries), visit the<a
href="https://github.com/liferay/liferay-portal/releases/tag/7.4.3.112-ga112"
target="_blank" rel="noopener noreferrer"> release page</a>.</p>
<h2>Dependency Management</h2>
<p>For development using the Liferay Platform, update Liferay
Workspace to use the lat est dependencies, by adding the following
line to the <strong>build.gradle</strong> file:</p>
<pre>
dependencies {
compileOnly group: "com.liferay.portal", name: "release.portal.api"
}</pre>
<p>All portal dependencies are now defined with a single
declaration. When Ausing an IDE such as Eclipse or IntelliJ all apis
are immediately available in autocomplete for immediate use. </p>
<p>By setting a product info key property it will be possible to
update all dependencies to a new version by updating the
<strong>liferay.workspace.product</strong> property in the liferay
workspace projects <strong>gradle.properties</strong> file:</p>
<pre>
liferay.workspace.product = portal-7.4-ga112</pre>
<p>When using an IDE such as Eclipse or IntelliJ all apis are
immediately available in autocomplete for immediate use.</p>
<h1>Features</h1>
<h2>Commerce</h2>
<h3>Refunds</h3>
<p> <strong>Feature Status: Release</strong> </p>
<p>Provides the ability for the Order Manager to make refunds
against on-line payments using the stand alone payment gateway.
While the refund amount can be configurable, the refund is always
made against the payment method of the original payment.</p>
<p>Documentation: <strong></strong> <a
href="https://learn.liferay.com/w/commerce/payment-management#payment-management"
id="payment-management">Payment Management</a></p>
<h3>Organisation Management Chart Enhancements</h3>
<p> <strong>Feature Status: Release</strong> </p>
<p>New link added to the chart from the organisation management
admin pages. Chart now supports search functionality. Users with
permissions can view or edit more details of the Organisation,
Account or User when you select them on the chart.</p>
<p>Documentation: <a
href="https://learn.liferay.com/w/commerce/creating-store-content/liferay-commerce-widgets/using-the-new-organization-management-chart-widget#using-the-new-organization-management-chart-widget"
id="using-the-new-organization-management-chart-widget">Using the
New Organization Management Chart Widget</a></p>
<h3>Channel Restrictions</h3>
<p> <strong>Feature Status: GA</strong> </p>
<p>Users now have the ability to restrict channel access to certain
Accounts via the eligibility tab on the channel admin pages. The
ability to restrict certain Account Address to be used on certain
channels has also been added. This is managed via an Eligibility tab
on the address that allows the user set channel usage.</p>
<p>Documentation: <a
href="https://learn.liferay.com/w/commerce/store-management/channels/channels-reference-guide#eligibility"
target="_blank" rel="noopener noreferrer">Channels Reference Guide</a></p>
<h3>Configurable Product Options</h3>
<p> <strong>Feature Status: Release</strong> </p>
<p>Use the platform's configurable purchasing rules to dictate
which product options need to purchased together when setting up
products with options. You can set which product options must be
purchased together and/or which product options cannot be combined
together to create a valid product bundle. You can also limit the
number of products from a particular group of products that can be
purchased together.</p>
<h3>Extending Permissions with "Channel Default" tab
being able to activate/inactivate</h3>
<p> <strong>Feature Status: GA</strong> </p>
<p>Two new permissions added to View and Edit the Channel Defaults
Tab on the Account Management Pages.</p>
<p>Documentation:<strong> </strong> <a
href="https://learn.liferay.com/w/dxp/users-and-permissions/accounts/channel-defaults/setting-channel-defaults#setting-channel-defaults"
target="_blank" rel="noopener noreferrer">Setting Channel Defaults</a></p>
<h3>Edit Product Bundles in the Mini-Cart\</h3>
<p> <strong>Feature Status: Release</strong> </p>
<p> <strong></strong>The ability for a buyer or sales
agent to edit a product bundle directly from the mini-cart has now
been added. Previously, if a change had to be made the bundle had
to be deleted from the cart and added again. This allows for more
speedy alterations to be made before proceeding with check out.</p>
<p>Documentation: <strong></strong> <a
href="https://learn.liferay.com/w/commerce/product-management/creating-and-managing-products/products/creating-product-bundles#editing-bundles-from-the-mini-cart"
id="editing-bundles-from-the-mini-cart">Editing Bundles from the
Mini Cart</a></p>
<h3>Increasing database AccountEntry.name column character limit</h3>
<p> <strong>Feature Status: GA</strong> </p>
<p>The allowable size of an Account Name has been increased to 250
characters in line with industry standards</p>
<p>Documentation: <a
href="https://learn.liferay.com/w/dxp/users-and-permissions/accounts#creating-an-account"
id="creating-an-account">Creating an Account</a></p>
<h3>Standalone Payment Gateway & Client Extension</h3>
<p> <strong>Feature Status: GA</strong> </p>
<p>Decoupling the payment gateway from the order engine in order to
allow you to capture payments against objects and to support new
payment methods via client extension</p>
<p>Documentation: <a
href="https://learn.liferay.com/w/commerce/developer-guide/using-client-extensions/using-the-payment-integration-client-extension#using-the-payment-integration-client-extension"
id="using-the-payment-integration-client-extension">Using the
Payment Integration Client Extension</a></p>
<h3>Shipping Method Client Extension</h3>
<p> <strong>Feature Status: GA</strong> </p>
<p>Ability to add new remote shipping calculations via client extension</p>
<p>Documentation: <a
href="https://learn.liferay.com/w/commerce/developer-guide/using-client-extensions/using-the-shipping-engine-client-extension#using-the-shipping-engine-client-extension"
id="using-the-shipping-engine-client-extension">Using the Shipping
Engine Client Extension</a></p>
<h3>Client Extension for custom checkout steps</h3>
<p> <strong>Feature Status: Release</strong> </p>
<p>Ability to add new steps to the checkout process via client extension</p>
<p>Normal: <a
href="https://learn.liferay.com/w/commerce/developer-guide/using-client-extensions/using-the-checkout-step-client-extension#using-the-checkout-step-client-extension"
id="using-the-checkout-step-client-extension">Using the Checkout
Step Client Extension</a></p>
<h2>Business Process Management</h2>
<h3>Auto-Increment Object Fields</h3>
<p> <strong>Feature Status: Release</strong> </p>
<p data-renderer-start-pos="6532">Users can add read-only fields to
object definitions that automatically increment for each new entry.</p>
<p data-renderer-start-pos="6635">Key Features:</p>
<ol> <li> <p data-renderer-start-pos="6652">Customize Starting
values - Users can set a starting value. This flexibility allows
for better alignment with specific business needs and
workflows</p> </li> <li> <p
data-renderer-start-pos="6803">Customize labels - Users can set
prefixes and suffixes to provide alphanumeric keys appended to the
incrementing values</p> </li> </ol>
<p data-renderer-start-pos="6803">Documentation: <a
href="https://learn.liferay.com/w/dxp/building-applications/objects/creating-and-managing-objects/fields/auto-increment-fields#auto-increment-fields"
id="auto-increment-fields">Auto-Increment Fields</a></p>
<h3>Validate using CurrentDate</h3>
<p> <strong>Feature Status: GA</strong> </p>
<p> <strong></strong>When adding validations in Objects,
many times is necessary to validate dates against the current date.</p>
<p data-renderer-start-pos="7099">Key Features:</p>
<ol> <li> <p data-renderer-start-pos="7116">Dynamic Date
Comparison - Users can compare a selected date with the
current system date. This empowers you to create rules that
trigger based on the relationship between a date field and
the present date.</p> </li> <li> <p
data-renderer-start-pos="7325">Versatile Date Range Checks -
Validate whether a date falls within a specified date range
relative to the present date</p> </li> </ol>
<p>Documentation: <a
href="https://learn.liferay.com/w/dxp/building-applications/objects/creating-and-managing-objects/validations/expression-builder-validations-reference#expression-builder-validations-reference"
id="expression-builder-validations-reference">Expression Builder
Validations Reference</a></p>
<h3>Validate Uniqueness of Object Fields </h3>
<p> <strong>Feature Status: Release</strong> </p>
<p data-renderer-start-pos="7529">Users can add validations that
verify field values, when used in combination, are unique.</p>
<p data-renderer-start-pos="7620">Limitation:<br> Numeric fields
such as Integer, Long Integer, Decimal, Precision Decimal will be
stored as null.</p>
<p data-renderer-start-pos="7620">Documentation: <a
href="https://learn.liferay.com/w/dxp/building-applications/objects/creating-and-managing-objects/validations/adding-field-validations#using-composite-key-validations"
id="using-composite-key-validations">Using Composite Key Validations</a></p>
<h3>Validate Relationship Names</h3>
<p> <strong>Feature Status: GA</strong> </p>
<p>Validate relationships to prevent duplicate names in child objects.</p>
<p>Documentation: <a
href="https://learn.liferay.com/w/dxp/building-applications/objects/creating-and-managing-objects/relationships/defining-object-relationships#defining-object-relationships"
id="defining-object-relationships">Defining Object Relationships</a></p>
<h3>Support for account restriction in Elastic Search\</h3>
<p> <strong>Feature Status: GA</strong> </p>
<p>Now, in applications that have data restricted by account, users
are able to properly search in the portal using the Search widget.</p>
<p>Documentation:</p>
<ul> <li> <a
href="https://learn.liferay.com/w/dxp/building-applications/objects/creating-and-managing-objects/using-system-objects-with-custom-objects/restricting-access-to-object-data-by-account#restricting-access-to-object-data-by-account"
id="restricting-access-to-object-data-by-account">Restricting
Access to Object Data by Account</a></li> <li> <a
href="https://learn.liferay.com/w/dxp/building-applications/objects/creating-and-managing-objects/using-system-objects-with-custom-objects/accessing-accounts-data-from-custom-objects#accessing-accounts-data-from-custom-objects"
id="accessing-accounts-data-from-custom-objects">Accessing
Accounts Data from Custom Objects</a></li> <li> <a
href="https://learn.liferay.com/w/dxp/using-search/search-pages-and-widgets/search-results/search-results-behavior#returning-objects-in-search-results"
id="returning-objects-in-search-results">Returning Objects in
Search Results</a></li> </ul>
<h2>Experience Management</h2>
<h3>Case-Sensitive Tags</h3>
<p> <strong>Feature Status: Release</strong> </p>
<p>We've introduced <strong>case-sensitive tagging</strong> for
page creators, allowing <strong>tags to be saved and displayed
exactly as created</strong>, whether in uppercase or lowercase.
The feature ensures case sensitivity for tag creation,
case-insensitive searching, and autocomplete, while maintaining
backward compatibility for existing tags.</p>
<p> <a href="https://liferay.atlassian.net/browse/LPS-194362"
target="_blank" rel="noopener noreferrer">https://liferay.atlassian.net/browse/LPS-194362</a></p>
<h3>Enhanced Web Content Management experience to make it easier
for Content Creators to manage daily content</h3>
<p> <strong>Feature Status: Release</strong> </p>
<p>In response to user feedback, several enhancements have been
introduced for content creators. The first functionality addresses the
challenge of easily finding and <strong>managing Web Content created
within specific structures</strong>, allowing creators to review and
edit content more efficiently. The rest of functionalities enable
refined filtering and searching options, including the ability to
<strong>search in the title</strong>, <strong>filter by categories
and tag</strong>, filter search results and multi-select filters.</p>
<ul> <li> <a
href="https://liferay.atlassian.net/browse/LPD-10752"
target="_blank"
rel="noopener noreferrer">https://liferay.atlassian.net/browse/LPD-10752</a></li>
<li> <a href="https://liferay.atlassian.net/browse/LPS-196768"
target="_blank"
rel="noopener noreferrer">https://liferay.atlassian.net/browse/LPS-196768</a></li>
<li> <a href="https://liferay.atlassian.net/browse/LPS-196766"
target="_blank"
rel="noopener noreferrer">https://liferay.atlassian.net/browse/LPS-196766</a></li> </ul>
<h3>Asset Library Vocabulary configuration to be required on the
Sites Connected</h3>
<p> <strong>Feature Status: GA</strong> </p>
<p>We have introduced a configuration feature within the Asset
Library, enabling users to decide whether a Vocabulary created there
is solely <strong>required </strong>at the <strong>Asset Library
level</strong> or if it extends to the <strong>connected
Sites</strong> as well.</p>
<p> <a href="https://liferay.atlassian.net/browse/LPS-202016"
target="_blank" rel="noopener noreferrer">https://liferay.atlassian.net/browse/LPS-202016</a></p>
<h3>Set file size limit at asset library level</h3>
<p> <strong>Feature Status: GA</strong> </p>
<p>This development introduces the ability for the user to set the
file size at asset library’s level. This settings will override the
already existing one at System level when the latter is bigger. As
general rule, when both are set the lowest applies.</p>
<p> <img data-fileentryid="122466007"
src="https://liferay.dev/documents/portlet_file_entry/14/liferay-portal-ce-7.4-ce-ga112-image1.png/e99960f0-4101-d918-5074-75750f106a3f"> </p>
<p> <a href="https://liferay.atlassian.net/browse/LPS-188033"
target="_blank" rel="noopener noreferrer">https://liferay.atlassian.net/browse/LPS-188033</a></p>
<h3>Documents Library Preview</h3>
<p> <strong>Feature Status: GA</strong> </p>
<p data-renderer-start-pos="17412">To reduce the maintenance costs
of providing a full document’s preview, the following changes are
being introduced:</p>
<ul> <li> <p data-renderer-start-pos="17531">“<strong>No preview
available</strong>” state when the file size exceeds the preview
size limit</p> <div
class="cc-9bhdww image-center mediaSingleView-content-wrap rich-media-item"
data-layout="center" data-node-type="mediaSingle" data-width="500"
data-width-type="pixel"> <div class="cc-1o0186r"> <div
data-alt="image-20240129-144246.png"
data-collection="contentId-2518384649"
data-context-id="2518384649" data-file-mime-type=""
data-file-name="file" data-file-size="1" data-height="756"
data-id="f4dea759-af41-4fc1-81b0-288c10112458"
data-node-type="media" data-type="file" data-width="1061">
<img data-fileentryid="122466018"
src="https://liferay.dev/documents/portlet_file_entry/14/liferay-portal-ce-7.4-ce-ga112-image2.png/dcae4eac-f131-064e-9523-e97bf8e7e7b3">
</div> </div> </div> </li> </ul>
<ul> <li> <p data-renderer-start-pos="17616">Merge “File Entries”
and “PDF Preview” settings under “File Preview Limits”<br> <br>
<img data-fileentryid="122466037"
src="https://liferay.dev/documents/portlet_file_entry/14/liferay-portal-ce-7.4-ce-ga112-image3+%281%29.png/fd4cc15c-7d4e-fe14-0c9a-0d97d83d16d1">
<br> </p> </li> <li> <p
data-renderer-start-pos="17699">Rename "File Size
Limits" to "File Upload Limits" with a reviewed and
more understandable Description copy<br> <br> <img
data-fileentryid="122466046"
src="https://liferay.dev/documents/portlet_file_entry/14/liferay-portal-ce-7.4-ce-ga112-image4.png/9b342e68-fdf2-a96e-28c9-b4eebdb7be36">
<br> </p> </li> <li> <p data-renderer-start-pos="17813">The
user is being informed that the generated preview may not
correspond to the entire document</p> <div
class="cc-9bhdww image-center mediaSingleView-content-wrap rich-media-item"
data-layout="center" data-node-type="mediaSingle" data-width="500"
data-width-type="pixel"> <div class="cc-w03d9j"> <div
data-alt="image-20240129-145244.png"
data-collection="contentId-2518384649"
data-context-id="2518384649" data-file-mime-type=""
data-file-name="file" data-file-size="1" data-height="766"
data-id="8bf7147f-2b1b-4e3f-b532-1937e12e56ab"
data-node-type="media" data-type="file" data-width="881">
<img data-fileentryid="122466055"
src="https://liferay.dev/documents/portlet_file_entry/14/liferay-portal-ce-7.4-ce-ga112-image5.png/60af31bd-a09a-bfc5-c76c-d9890b87be1f"></div>
</div> </div> </li> </ul>
<p> <a href="https://liferay.atlassian.net/browse/LPS-175868"
target="_blank" rel="noopener noreferrer">https://liferay.atlassian.net/browse/LPS-175868</a></p>
<h3>Expanded features for Copy Documents and Folders</h3>
<p> <strong>Feature Status: GA</strong> </p>
<p data-renderer-start-pos="18001">As sequel to epic LPS-182512,
this development aims to provide the user the following abilities:</p>
<ul> <li> <p data-renderer-start-pos="18101">Configuration of
document size for copying in D&M</p> <div
class="cc-9bhdww image-center mediaSingleView-content-wrap rich-media-item"
data-layout="center" data-node-type="mediaSingle" data-width="500"
data-width-type="pixel"> <div class="cc-1hnzoow"> <div
data-alt="image-20240130-134807.png"
data-collection="contentId-2518384649"
data-context-id="2518384649" data-file-mime-type=""
data-file-name="file" data-file-size="1" data-height="746"
data-id="5b87161b-f0ee-4593-a464-90bba63eb853"
data-node-type="media" data-type="file" data-width="961">
<img data-fileentryid="122466069"
src="https://liferay.dev/documents/portlet_file_entry/14/liferay-portal-ce-7.4-ce-ga112-image6.png/90971654-27a8-451c-b526-92d73d372655"></div>
</div> </div> </li> </ul>
<ul> <li> <p data-renderer-start-pos="18157">Bulk copying of
documents and folders</p> <div
class="cc-9bhdww image-center mediaSingleView-content-wrap rich-media-item"
data-layout="center" data-node-type="mediaSingle" data-width="500"
data-width-type="pixel"> <div class="cc-u4eg7y"> <div
data-alt="image-20240130-140718.png"
data-collection="contentId-2518384649"
data-context-id="2518384649" data-file-mime-type=""
data-file-name="file" data-file-size="1" data-height="701"
data-id="e34b6a07-09d6-4705-8baa-41853a81851e"
data-node-type="media" data-type="file" data-width="1086">
<img data-fileentryid="122466079"
src="https://liferay.dev/documents/portlet_file_entry/14/liferay-portal-ce-7.4-ce-ga112-image7.png/06e627d6-be7a-c5fe-e065-dfb026710932"></div>
</div> </div> </li> <li> <p
data-renderer-start-pos="18202">Currently the copy action for
documents and folders is able to be performed in Portal without
fully respecting the site-asset library relationship. The expected
behavior is:</p> <ul> <li> <p
data-renderer-start-pos="18379">An asset library must be
connected to a site in order to copy a document over to the
site</p> </li> <li> <p data-renderer-start-pos="18472">A
document cannot be copied from a site to an asset library
(only the other way around)</p> </li> </ul> <p
data-renderer-start-pos="18563">We can enforce a stricter check
when performing the copy action between asset libraries and sites.
Instead of the end user receiving a success message when copying a
document to a disconnected site, an error message should appear
telling the user to set the connection first.</p> </li> <li> <p
data-renderer-start-pos="18842">Document Types contained in a
document are being copied in Documents and Media so that they can
be easily re-used</p> <div
class="cc-9bhdww image-center mediaSingleView-content-wrap rich-media-item"
data-layout="center" data-node-type="mediaSingle" data-width="500"
data-width-type="pixel"> <div class="cc-dscyia"> <div
data-alt="image-20240130-143100.png"
data-collection="contentId-2518384649"
data-context-id="2518384649" data-file-mime-type=""
data-file-name="file" data-file-size="1" data-height="767"
data-id="2e16a9a6-78ca-4973-9a10-092dc3b0eaad"
data-node-type="media" data-type="file" data-width="635">
<img data-fileentryid="122466088"
src="https://liferay.dev/documents/portlet_file_entry/14/liferay-portal-ce-7.4-ce-ga112-image8.png/a9f3a705-f6ec-45c2-b640-0a4bfc765f17"></div>
<div data-alt="image-20240130-143100.png"
data-collection="contentId-2518384649"
data-context-id="2518384649" data-file-mime-type=""
data-file-name="file" data-file-size="1" data-height="767"
data-id="2e16a9a6-78ca-4973-9a10-092dc3b0eaad"
data-node-type="media" data-type="file"
data-width="635"></div> </div> </div> </li> <li> <p
data-renderer-start-pos="18963">Categories and Tags in Documents
and Media are being automatically copied so that<br> so that they
can be easily re-used on a new site</p> </li> </ul>
<p data-renderer-start-pos="18963"> <a
href="https://liferay.atlassian.net/browse/LPD-16751"
target="_blank" rel="noopener noreferrer">https://liferay.atlassian.net/browse/LPD-16751</a></p>
<h3>Schedule and Delete Knowledge Base Articles</h3>
<p> <strong>Feature Status: Release</strong> </p>
<p data-renderer-start-pos="19183">With this development, the
publication of Knowledge Base articles will be schedulable, in
particular, the feature is so designed:</p>
<ul> <li> <p data-renderer-start-pos="19316">The primary Publish
button will have an arrow down with the “<em>Publish</em>” option,
that will immediately publish the article, and the “<em>
<strong>Schedule</strong> </em>” one.</p> <div
class="cc-9bhdww image-center mediaSingleView-content-wrap rich-media-item"
data-layout="center" data-node-type="mediaSingle" data-width="500"
data-width-type="pixel"> <div class="cc-16g5a1v"> <div
data-alt="image-20240130-153902.png"
data-collection="contentId-2518384649"
data-context-id="2518384649" data-file-mime-type=""
data-file-name="file" data-file-size="1" data-height="630"
data-id="56ed65b3-7963-487d-8594-8b03fe7660b3"
data-node-type="media" data-type="file" data-width="998">
<img data-fileentryid="122466101"
src="https://liferay.dev/documents/portlet_file_entry/14/liferay-portal-ce-7.4-ce-ga112-image9.png/8df59d38-0c6e-c5e9-d49e-217fabc1b451"></div>
</div> </div> </li> </ul>
<ul> <li> <p data-renderer-start-pos="19468">Clicking the
<em>Schedule<strong> </strong> </em>option, will make a modal
appearing so the user can set a date and time for the article to
be published.<br> <img data-fileentryid="122466110"
src="https://liferay.dev/documents/portlet_file_entry/14/liferay-portal-ce-7.4-ce-ga112-image10.png/241dd80f-7b5c-eb80-241b-cd83da45c1af"></p>
</li> <li> <p data-renderer-start-pos="19602">The Scheduled article
will have the SCHEDULED status that will change to Approved on the
scheduled date</p> </li> <li> <p
data-renderer-start-pos="19694">A tooltip with the scheduled date
information will appear when hovering the
<code>question-circle-full</code> icon placed next to the
scheduled state.</p> <div
class="cc-9bhdww image-center mediaSingleView-content-wrap rich-media-item"
data-layout="center" data-node-type="mediaSingle" data-width="500"
data-width-type="pixel"> <div class="cc-1ki9q24"> <div
data-alt="image-20240130-154136.png"
data-collection="contentId-2518384649"
data-context-id="2518384649" data-file-mime-type=""
data-file-name="file" data-file-size="1" data-height="693"
data-id="e5badcbd-740a-4711-86e4-c5182162344c"
data-node-type="media" data-type="file" data-width="1117">
<img data-fileentryid="122466119"
src="https://liferay.dev/documents/portlet_file_entry/14/liferay-portal-ce-7.4-ce-ga112-image11.png/cc1991c1-de97-a616-923c-45e075aae02c"></div>
</div> </div> </li> <li> <p
data-renderer-start-pos="19832">Editing a scheduled article will
provide the user the ability, by clicking the primary button that
has turned into “<em> <strong>Scheduled</strong> </em>”, to:</p>
<ul> <li> <p data-renderer-start-pos="19967">Cancel the
operation</p> </li> <li> <p
data-renderer-start-pos="19991">Publish Now</p> </li> <li>
<p data-renderer-start-pos="20006">Schedule: this option will
save the article, date & time changes and the user will
return to the previous screen before entering edit mode</p>
</li> </ul> </li> </ul>
<div
class="cc-9bhdww image-center mediaSingleView-content-wrap rich-media-item"
data-layout="center" data-node-type="mediaSingle" data-width="500"
data-width-type="pixel"> <div class="cc-sqiazo"> <div
data-alt="image-20240130-154534.png"
data-collection="contentId-2518384649"
data-context-id="2518384649" data-file-mime-type=""
data-file-name="file" data-file-size="1" data-height="593"
data-id="b41e44a5-00ca-4980-bbf4-7a0dbd2d507f"
data-node-type="media" data-type="file" data-width="955"> <div
class="cc-10p26u2 new-file-experience-wrapper"
data-testid="media-card-view" id="newFileExperienceWrapper">
<div class="cc-1yn77bd media-file-card-view"
data-test-media-name="image-20240130-154534.png"
data-test-progress="1" data-test-status="complete"
data-testid="media-file-card-view"> </div> </div> </div> </div> </div>
<p data-renderer-start-pos="20151"> <img
data-fileentryid="122466128" src="https://liferay.dev/documents/portlet_file_entry/14/liferay-portal-ce-7.4-ce-ga112-image12.png/a7ef86cf-4622-5ba0-201b-28d0e0229a08"></p>
<ul> <li> <p data-renderer-start-pos="20157">
<strong>Additional feature</strong>: A Knowledge Base article
deletion action will move it to the <strong>Recycle Bin</strong>
so that it can be restored.</p> </li> </ul>
<p data-renderer-start-pos="20157"> <a
href="https://liferay.atlassian.net/browse/LPS-188058"
target="_blank" rel="noopener noreferrer">https://liferay.atlassian.net/browse/LPS-188058</a></p>
<h2>Content Publishing</h2>
<h3>Improve performance of large publications with Liferay’s
caching framework</h3>
<p> <strong>Feature Status: GA</strong> </p>
<p>The goal of the epic is to leverage the functions of Liferay’s
caching framework. <strong>Our current measurements indicate 10%-30%
performance increase in database writing operations, also 10 times
faster reading performance!</strong> </p>
<p> <a target="_blank" rel="noopener noreferrer">https://liferay.atlassian.net/browse/LPD-10782</a></p>
<h2>Frontend Infrastructure</h2>
<h3>Management Toolbar UX Improvements</h3>
<p> <strong>Feature Status: GA</strong> </p>
<p data-renderer-start-pos="23178">The user experience of the
Management Toolbar present in multiple applications (Web Content,
Blogs, Documents & Media) has been improved:</p>
<ul> <li> <p data-renderer-start-pos="23319">Separate sections
for filter and order.</p> </li> <li> <p
data-renderer-start-pos="23362">Changed “+” with “New” for
clarity.</p> </li> <li> <p data-renderer-start-pos="23401">Moved
info icon to the right.</p> </li> </ul>
<p data-renderer-start-pos="23401"> <img
data-fileentryid="122466143"
src="https://liferay.dev/documents/portlet_file_entry/14/liferay-portal-ce-7.4-ce-ga112-image13.png/4ec7654a-3c63-e5ff-a635-69e87b4e1e90">
<br> </p>
<p data-renderer-start-pos="23401"> <a
href="https://liferay.atlassian.net/browse/LPS-144527"
target="_blank" rel="noopener noreferrer">https://liferay.atlassian.net/browse/LPS-144527</a></p>
<h3>Customize Rich Text Editors Configurations through Client Extensions</h3>
<p> <strong>Feature Status: Release</strong> </p>
<p>Now, admins can customize the configuration of the Rich Text
Editors with this new client extension, allowing them to set the
toolbars that are available on different applications. In the form,
you will have to define the instances in which the configuration will
be taken into account as well as the JS that contains the configuration:</p>
<p> <img data-fileentryid="122466153"
src="https://liferay.dev/documents/portlet_file_entry/14/liferay-portal-ce-7.4-ce-ga112-image14.png/315d3e6c-ca6e-ec21-a841-a9e0a20e88ec">
<br> </p>
<p> <a href="https://liferay.atlassian.net/browse/LPS-186870"
target="_blank" rel="noopener noreferrer">https://liferay.atlassian.net/browse/LPS-186870</a></p>
<h3>Content Security Policies accept stricter policies for
Javascript resources</h3>
<p> <strong>Feature Status: Beta</strong> </p>
<p data-renderer-start-pos="24007">We refactored the way Liferay
scripts were managed, so admins can define stricter policies like:</p>
<p data-renderer-start-pos="24105"> <code>script-src '[$NONCE$]';
script-src-attr 'unsafe-inline';</code> </p>
<p data-renderer-start-pos="24164"> <strong>Limitations:
</strong>If a policy like the one above is configured, the parts of
the product that uses a rich-text editor will not work correctly.
Analyze your scenario to identify how strict your policy can be in
order to make the solution work.</p>
<p data-renderer-start-pos="24164"> <a
href="https://liferay.atlassian.net/browse/LPS-178065"
target="_blank" rel="noopener noreferrer">https://liferay.atlassian.net/browse/LPS-178065</a></p>
<h3>Make session.timeout.auto properties available as configurations</h3>
<p data-renderer-start-pos="24503">Now users can configure the
values for the following properties from settings (system, instance
or site settings):</p>
<ul> <li> <p data-renderer-start-pos="24621">
<code>session.timeout.auto.extend</code> </p> </li> <li> <p
data-renderer-start-pos="24652">
<code>session.timeout.auto.extend.offset</code></p> </li> </ul>
<p data-renderer-start-pos="24652"> <code><img
data-fileentryid="122466162"
src="https://liferay.dev/documents/portlet_file_entry/14/liferay-portal-ce-7.4-ce-ga112-image15.png/e430146c-8311-a5a2-6d05-b3ce08035ab2">
</code> </p>
<p data-renderer-start-pos="24652">Beware that now the properties
are no longer available in portal-ext.properties</p>
<p data-renderer-start-pos="24652"> <a
href="https://liferay.atlassian.net/browse/LPS-167626"
target="_blank" rel="noopener noreferrer">https://liferay.atlassian.net/browse/LPS-167626</a></p>
<h3>Mechanism to manage the user consent for 3rd Party Cookies</h3>
<p> <strong>Feature Status: GA</strong> </p>
<p data-renderer-start-pos="24868">Pages served by Liferay might
make use of third party cookies. In order to inform Liferay to
manage them as part of the user cookie consent, now Portal
developers and Content creators have a way to indicate the portal
about them.<br> </p>
<p data-renderer-start-pos="25100">In order to make Liferay aware
of them, the content creator or portal developer will need to update
the markup to the following:<br> <br> First, indicate the type of
cookie following this format:</p>
<div
class="cc-37qm0r image-center mediaSingleView-content-wrap rich-media-item"
data-layout="center" data-node-type="mediaSingle" data-width="856"
data-width-type="pixel"> <div class="cc-yv1wuz"> <div
data-alt="image-20240125-112530.png"
data-collection="contentId-2518384649"
data-context-id="2518384649" data-file-mime-type=""
data-file-name="file" data-file-size="1" data-height="693"
data-id="5bb72c59-39e0-491b-93a0-c620c5b4450d"
data-node-type="media" data-type="file" data-width="1664">
<img data-fileentryid="122466190"
src="https://liferay.dev/documents/portlet_file_entry/14/liferay-portal-ce-7.4-ce-ga112-image16.png/bf776870-1ac1-6674-9af4-603b5f90dc2c">
</div> </div> </div>
<div
class="cc-37qm0r image-center mediaSingleView-content-wrap rich-media-item"
data-layout="center" data-node-type="mediaSingle" data-width="856"
data-width-type="pixel"> <div class="cc-yv1wuz"> <div
data-alt="image-20240125-112530.png"
data-collection="contentId-2518384649"
data-context-id="2518384649" data-file-mime-type=""
data-file-name="file" data-file-size="1" data-height="693"
data-id="5bb72c59-39e0-491b-93a0-c620c5b4450d"
data-node-type="media" data-type="file" data-width="1664"> <div
class="cc-aeoe5l new-file-experience-wrapper"
data-testid="media-card-view" id="newFileExperienceWrapper">
<div class="cc-1yn77bd media-file-card-view"
data-test-media-name="image-20240125-112530.png"
data-test-progress="1" data-test-status="complete"
data-testid="media-file-card-view"> </div> </div> </div> </div> </div>
<p data-renderer-start-pos="25293">Second, change the markup
depending on the element affected:</p>
<div
class="cc-37qm0r image-center mediaSingleView-content-wrap rich-media-item"
data-layout="center" data-node-type="mediaSingle" data-width="856"
data-width-type="pixel"> <div class="cc-1yv9y9p"> <div
data-alt="image-20240125-112757.png"
data-collection="contentId-2518384649"
data-context-id="2518384649" data-file-mime-type=""
data-file-name="file" data-file-size="1" data-height="580"
data-id="98e851d0-2a99-470b-b3d7-52e67c7bc2b1"
data-node-type="media" data-type="file" data-width="1564">
<img data-fileentryid="122466199"
src="https://liferay.dev/documents/portlet_file_entry/14/liferay-portal-ce-7.4-ce-ga112-image17.png/78d40c99-a368-d29b-f28d-f0d5f33c8d7d"><br>
</div> </div> </div>
<div
class="cc-37qm0r image-center mediaSingleView-content-wrap rich-media-item"
data-layout="center" data-node-type="mediaSingle" data-width="856"
data-width-type="pixel"> <div class="cc-1yv9y9p"> <div
data-alt="image-20240125-112757.png"
data-collection="contentId-2518384649"
data-context-id="2518384649" data-file-mime-type=""
data-file-name="file" data-file-size="1" data-height="580"
data-id="98e851d0-2a99-470b-b3d7-52e67c7bc2b1"
data-node-type="media" data-type="file" data-width="1564"> <div
class="cc-1ha365d new-file-experience-wrapper"
data-testid="media-card-view" id="newFileExperienceWrapper">
<div class="cc-1yn77bd media-file-card-view"
data-test-media-name="image-20240125-112757.png"
data-test-progress="1" data-test-status="complete"
data-testid="media-file-card-view"> </div> </div> </div> </div> </div>
<p data-renderer-start-pos="25358">For example, embedding a video
from a video provider, will see the markup changed in the following way:</p>
<h4>Before:</h4>
<div class="cc-ceksvt code-block"> <code><iframe
src="..." /></code> </div>
<h4> <br> After:</h4>
<p> <code><iframe data-src="..."
`data-third-party-cookie="CONSENT_TYPE_FUNCTIONAL"
/></code> </p>
<p> <a href="https://liferay.atlassian.net/browse/LPS-154290"
target="_blank" rel="noopener noreferrer">https://liferay.atlassian.net/browse/LPS-154290</a></p>
<h2>Headless</h2>
<h3>API Builder: new endpoint to retrieve a single entry</h3>
<p> <strong>Feature Status: Beta</strong> </p>
<p data-renderer-start-pos="26823">We continue to improve the API
Builder by adding a new endpoints: <strong>GET single element</strong>.</p>
<p data-renderer-start-pos="26911">With the <strong>GET single
element</strong> new endpoint, we allow users to create a <strong>GET
</strong>endpoint that only retrieves a single element using a path parameter.</p>
<p data-renderer-start-pos="27055">One particular thing is that
users can select as path parameter any unique custom field defined
in the object apart from ID and External Reference Code.</p>
<p data-renderer-start-pos="27055"> <img
data-fileentryid="122466208" src="https://liferay.dev/documents/portlet_file_entry/14/liferay-portal-ce-7.4-ce-ga112-image18.png/c92b45bf-7d9d-6d36-667d-5ad21754f583"></p>
<p> <a href="https://liferay.atlassian.net/browse/LPS-170000"
target="_blank" rel="noopener noreferrer">https://liferay.atlassian.net/browse/LPS-170000</a></p>
<h3>API Builder: new endpoint to create entries of the main custom object</h3>
<p> <strong>Feature Status: Beta</strong> </p>
<p data-renderer-start-pos="27335">New <strong>POST
</strong>endpoint inside API Builder to add the possibility not only
of recovering information of the entries but also of creating them.</p>
<p data-renderer-start-pos="27474">As in the GET methods, user can
define the schema to use for the request and the response. Both can be
the same or two different ones. And can even be one already used
previously on another endpoint definition.</p>
<p data-renderer-start-pos="27686">Company and Site scope is
available too.</p>
<div class="ak-editor-panel__content"> <p
data-renderer-start-pos="27729"> <strong>POST</strong> method
only works for creating new entries of the main object . No
entries for the related objects can be created yet at the same
time.</p> <div class="embed-responsive embed-responsive-16by9"
data-embed-id="https://liferay.dev/documents/14/0/liferay-portal-ce-7.4-ce-ga112-movie1.mp4/a1fb1a63-522d-7ee8-750f-8a334eaf8376?version=1.0&t=1710372396747&videoEmbed=true"
data-styles="{"width":"77%"}"
style="width: 77.0%;"> <iframe data-video-liferay=""
frameborder="0" height="315"
src="https://liferay.dev/documents/14/0/liferay-portal-ce-7.4-ce-ga112-movie1.mp4/a1fb1a63-522d-7ee8-750f-8a334eaf8376?version=1.0&t=1710372396747&videoEmbed=true"
width="560"></iframe> </div> <p
data-renderer-start-pos="27729"> </p> <p
data-renderer-start-pos="27729"> <a
href="https://liferay.atlassian.net/browse/LPS-182878"
target="_blank"
rel="noopener noreferrer">https://liferay.atlassian.net/browse/LPS-182878</a></p>
<h3>Data Migration Center: support for Object entries at site
level</h3> <p> <strong>Feature Status: Beta</strong> </p>
<p>Under all supported formats, users can select the origin and
destination site of the data so it's possible to export and import
object entries between any site.</p> <p> <a
href="https://liferay.atlassian.net/browse/LPS-196018"
target="_blank"
rel="noopener noreferrer">https://liferay.atlassian.net/browse/LPS-196018</a></p>
<h3>Data Migration Center: support for CSV files to export and
import object entries</h3> <p> <strong>Feature Status:
Beta</strong> </p> <p data-renderer-start-pos="28264">With this
improvement, the Data Migration Center allows users to export
and import Object Entries using CSV files. Not all field types
are supported but many of the most used ones are:</p> <div
class="ak-editor-panel cc-1f60buh" data-panel-type="warning"> <div
class="ak-editor-panel__content"> <p
data-renderer-start-pos="28451">Supported field types:
<strong>dateandtime, date, decimal, integer, longint,
longtext, precissiondecimal, richtext, text and picklist.<br>
</strong> </p> </div> </div> <p
data-renderer-start-pos="28580">In order to help users, only
supported fields are available for exporting at the UI. As new
types are allowed, they will appear available in the UI.</p> <div
class="embed-responsive embed-responsive-16by9"
data-embed-id="https://liferay.dev/documents/14/0/liferay-portal-ce-7.4-ce-ga112-movie2.mp4/4528f870-52a9-00de-fb4e-da0f7eca6984?version=1.0&t=1710372642110&videoEmbed=true"
data-styles="{"width":"77%"}"
style="width: 77.0%;"> <iframe data-video-liferay=""
frameborder="0" height="315"
src="https://liferay.dev/documents/14/0/liferay-portal-ce-7.4-ce-ga112-movie2.mp4/4528f870-52a9-00de-fb4e-da0f7eca6984?version=1.0&t=1710372642110&videoEmbed=true"
width="560"></iframe> </div> <p
data-renderer-start-pos="28580"> </p> <p
data-renderer-start-pos="28580"> <a
href="https://liferay.atlassian.net/browse/LPS-172870"
target="_blank"
rel="noopener noreferrer">https://liferay.atlassian.net/browse/LPS-172870</a></p>
<h2>Application Security</h2> <h3>Provide SCIM (System for
Cross-domain Identity Management) Adapter</h3> <p>
<strong>Feature Status: Beta</strong> </p> <p>SCIM provides a
unified, RFC compliant way to keep user/group data in sync
between different applications. Liferay is a service provider
and enables clients to be connected. Though the defined RESTful
APIs and schemas, clients can perform CRUD operations to keep
resources in sync.</p> <p> <a
href="https://liferay.atlassian.net/browse/LPS-96845"
target="_blank"
rel="noopener noreferrer">https://liferay.atlassian.net/browse/LPS-96845</a></p>
<h3>Complete SCIM (System for Cross-domain Identity Management)
Adapter</h3> <p> <strong>Feature Status: Beta</strong> </p>
<p>With this feature we extending the SCIM implementation with
User Group un/provisioning as well with the User Group membership
un/provisioning</p> <p> <a
href="https://liferay.atlassian.net/browse/LPS-201188"
target="_blank"
rel="noopener noreferrer">https://liferay.atlassian.net/browse/LPS-201188</a></p>
<h3>Captcha authentication via Headless API</h3> <p>
<strong>Feature Status: Release</strong> </p> <p>Previously
customers would add captchas using a TagLib. But this is no
longer useful because customers are not deploying JSPs. The
Captcha API enables this functionality to be used in arbitrary
contexts.</p> <p> <a
href="https://liferay.atlassian.net/browse/LPS-185150"
target="_blank"
rel="noopener noreferrer">https://liferay.atlassian.net/browse/LPS-185150</a></p>
<h2>Core Upgrades</h2> <h3>Upgrade Report output directory is now
configurable </h3> <p> <strong>Feature Status: GA</strong> </p>
<p data-renderer-start-pos="32513">When the Upgrade Report is
enabled, the output directory of the report can be configured with
the new portal property <code>upgrade.report.dir</code> </p> <p
data-renderer-start-pos="32651"> <br> If this property is not
set, the upgrade report can be found in the default
directory:</p> <ul> <li> <p data-renderer-start-pos="32742">
<code>portal-tools-db-upgrade-client/reports</code> for the
upgrade tool.</p> </li> <li> <p
data-renderer-start-pos="32806">
<code>{liferay-home}/reports</code> if upgrade was executed on
startup.</p> </li> </ul> <p
data-renderer-start-pos="32806">Documentation: <a
href="https://learn.liferay.com/w/dxp/installation-and-upgrades/upgrading-liferay/reference/upgrade-report#upgrade-report-through-the-upgrade-tool"
id="upgrade-report-through-the-upgrade-tool">Upgrade Report
through the Upgrade Tool</a></p> </div>
<h1>Documentation</h1>
<p>All documentation for Liferay Portal and Liferay Commerce can
now be found on our documentation site: <a
href="https://learn.liferay.com" target="_blank"
rel="noopener noreferrer">learn.liferay.com</a>. For more information
on upgrading to Liferay Portal 7.4 GA112 see refer to the <a
href="https://learn.liferay.com/dxp/latest/en/installation-and-upgrades/upgrading-liferay/upgrade-basics/upgrade-overview.html"
target="_blank" rel="noopener noreferrer">Upgrade Overview</a>.</p>
<h1>Compatibility Matrix</h1>
<p>Liferay's general policy is to test Liferay Portal and Liferay
Commerce against newer major releases of operating systems, open
source app servers, browsers, and open source databases (we regularly
update the bundled upstream libraries to fix bugs or take advantage of
new features in the open source we depend on). </p>
<p>Liferay Portal 7.4 GA112 and Liferay Commerce 4.0 GA112 were
tested extensively for use with the following Application/Database Servers: </p>
<p>Application Server</p>
<ul> <li> <p>Tomcat 9.0</p> </li> <li> <p>Wildfly 18.0,
23.0</p> </li> </ul>
<p>Database</p>
<ul> <li> <p>MySQL 5.7, 8.0</p> </li> <li> <p>MariaDB 10.2,
10.4, 10.6</p> </li> <li> <p>PostgreSQL 12.x, 13.x, 14.x,
15.x</p> </li> </ul>
<p>JDK</p>
<ul> <li> <p>IBM J9 JDK 8</p> </li> <li> <p>Oracle JDK 8</p>
</li> <li> <p>Oracle JDK 11</p> </li> <li> <p>All Java Technical
Compatibility Kit (TCK) compliant builds of Java 11 and Java 8</p>
</li> </ul>
<p> <a
href="https://help.liferay.com/hc/en-us/search?utf8=%E2%9C%112&query=Search+Engine+Compatibility+Matrix"
target="_blank" rel="noopener noreferrer">Search Engine
Compatibility Matrix</a></p>
<h1>Source Code</h1>
<p>Source is available as a zip archive on the<a
href="https://github.com/liferay/liferay-portal/releases/tag/7.4.3.112-ga112"
target="_blank" rel="noopener noreferrer"> release page</a>, or on
its<a href="http://github.com/liferay/liferay-portal"
target="_blank" rel="noopener noreferrer"> home on GitHub</a>.</p>
<h1>Bug Reporting</h1>
<p>If you believe you have encountered a bug in the new release you
can report your issue by following the<a
href="https://liferay.dev/feedback/report-a-bug" target="_blank"
rel="noopener noreferrer"> bug reporting instructions</a>.</p>
<h1>Getting Support</h1>
<p>Support is provided by our awesome community. Please visit <a
href="https://liferay.dev/help" target="_blank"
rel="noopener noreferrer">getting started</a> for more details on
how you can receive support.</p>
<h1>Fixes and Known Issues</h1>
<ul> <li> <p> <a
href="https://liferay.atlassian.net/issues/?jql=fixVersion%20%3D%20%227.4.3.112%20GA112%22"
target="_blank" rel="noopener noreferrer">Fixes</a></p> </li>
<li> <p> <a
href="https://liferay.atlassian.net/issues/?jql=labels%20%3D%20liferay-ga112-ce-743-known-issues%20OR%20affectedVersion%20%3D%20%227.4.3.112%20GA112%22"
target="_blank" rel="noopener noreferrer">List of known
issues</a></p> </li> </ul>Jamie Sammons2024-03-14T21:48:00ZLiferay Announces Deprecation of JDK 11David H Nebinger/es/c/blogs/rss&entryId=1224675502024-03-21T10:51:50Z2024-03-14T19:15:00Z<p>Liferay released Liferay DXP 2024.Q1 this week, and in the
corresponding <a
href="https://support.liferay.com/release-notes/2024-q1">release
notes</a>, you'll find the following nugget.</p>
<p>Just search for "Java JDK 11 Runtime" to find in the
notes, but here's the text for it:</p>
<div class="overflow-auto portlet-msg-alert">Support for Java JDK
11 is flagged for deprecation as of Q1.2024. As DXP continues to
evolve as a platform to deliver powerful solutions, we are planning
to release support for the latest Java LTS runtime environment (JDK
21) later this year. JDK 11 will remain supported while we prepare
for this next evolution.</div>
<h2>So, what is actually going on?</h2>
<p>Liferay is actively working on updating the JDK to JDK-21. As
with previous JDK updates, it is not always the Liferay source code
that Liferay has to fix, it always comes down to the third party
jars and other transitive dependencies that Liferay mostly has to
worry about.</p>
<p>Additionally, to build Liferay using JDK-21, there will be
updates to the developer tools necessary.</p>
<p>However, once you update the third party jars and dev tools, you
shut down support for older versions of the JDK, including Java 11.</p>
<p>Although the announcement doesn't say it, this will cause
significant ripples...</p>
<p>First and foremost, it will remove any app server from the
compatibility matrix that cannot run under JDK-21. So all app servers
are going to either be bumped to versions that support JDK-21 or
they'll be dropped from the list (like WebSphere 9 was).</p>
<p>Updating third party jars will likely touch the major Liferay
dependencies such as Spring, Hibernate, EhCache, Quartz, OpenSAML, ...
For third party jars that Liferay needs but are dead projects, Liferay
will likely end up creating a custom fork to have JDK-21 compatibility.</p>
<p>As mentioned, the developer toolchain will see updates for the
Gradle plugins, but may also see changes necessary for CSS and other
build-time artifacts, updates to Jenkins CI/CD processes for Liferay
PaaS, etc.</p>
<p>For any customizations that you may have, you'll likely need to
at least recompile them for JDK-21 and perhaps also need to update
your third-party dependencies for JDK-21 compatibility. Note that if
you're using Client Extensions (CX) for your customization aspects,
you won't be forced to update those because they run in separate
docker containers and can use whatever they want for implementation
purposes. It's just those customizations deployed as OSGi jars or
wars into Liferay that you'll need to worry about.</p>
<h1>Later this year?</h1>
<p>One important aspect to understand is the portion of the text
that says "we are planning to release support for the latest
Java LTS runtime environment (JDK 21) later this year."</p>
<p>Sounds kind of ambiguous, right? I mean, don't they know it's
going to happen in Q3 or Q4 or something?</p>
<p>Well, that's really kind of a problem. See we don't know what we
don't know, yeah? I mean, we do know that we have to update
dependencies and code and build tools and whatnot, but we don't know
how hard or how long it will take to get through that.</p>
<p>Plus it's not like there will be a code freeze while this work
is going on, so there's that...</p>
<p>The plan, as far as I know, is to shoot for Q3, but it could get
pushed to Q4 or even 2025 because of problems encountered during the
whole process.</p>
<p>So "later this year" is all Liferay will commit to at
this point, but when will ultimately depend upon whatever trials and
tribulations we face trying to get there.</p>
<p>I can promise, however, that this will happen in conjunction
with a quarterly release... You're not going to get a 2024.Q3 that
would support both JDK-11 and JDK-21 at the same time, no it will be
more of an "to upgrade to 2024.Q3, you must upgrade to
JDK-21" sort of deals. But again it could align on Q3, it could
align on Q4, or maybe it doesn't happen unti 2025?</p>
<p>We won't know that until we get closer...</p>
<h1>What I Recommend</h1>
<p>You might be asking yourself what this will mean for your
Liferay installation.</p>
<p>Personally, I'd recommend taking the following steps:</p>
<p> <strong>1.</strong> If you're not using Tomcat, start making
the change now. Liferay uses Tomcat in its bundles and docker
containers, and even if JDK-21 requires an updated version of
Tomcat, that's what Liferay will focus on. Any other application
servers will be an afterthought and may not be tested as much as
Tomcat. Moving to Tomcat for your application server will position
you on the right platform for Liferay's JDK-21 changes.</p>
<p> <strong>2.</strong> If you have OSGi-based jar or war
modules, start collecting details about your third party jar
dependencies. Find out which versions you might need to update to
for JDK-21 support and plan for upgrading those, or for
dependencies without JDK-21 compatibility, develop a plan for
either moving to a different dependency or refactoring to remove
the dependency altogether.</p>
<p> <strong>3.</strong> If you are using a CI/CD process for your
builds, start the research and planning for how you will be
upgrading that to support using JDK-21 for builds.</p>
<p> <strong>4.</strong> Consider switching JDK vendors. If you're
still using Oracle's JDK (I know you're out there, Oracle knows
you're out there, and they may come looking for you), this may be an
opportune time to switch out your JDK for a non-Oracle version.
Personally I tend to favor <a
href="https://www.azul.com/downloads/?package=jdk#zulu">Azul's Zulu
JDKs</a>, but any flavor is fine as long as it passes the Java TCK.</p>
<p> <strong>5.</strong> And probably the most important recommendation:</p>
<h1>Start Planning Now!</h1>
<p>Yes, that's right, start planning now. Only you know how long it
would take to push a major platform change like this through your
organization. Some of you will be kicking back saying "Oh, I can
do this over a long weekend!", but those of you with more
stringent SCM processes and approved software lists (including version
details) and long update cycles, you folks should start your planning now.</p>
<p>We all now know this is coming, so if we start planning for this
now, when it happens it will not be such a big shock to our organizations...</p>
<p>If you have any questions or want more info, drop a comment
below or hit me up on the Liferay Community Slack channels.</p>David H Nebinger2024-03-14T19:15:00ZLiferay Announces Deprecation of WebSphere Application ServerDavid H Nebinger/es/c/blogs/rss&entryId=1224632202024-03-12T19:08:48Z2024-03-12T18:41:00Z<p>In case you missed it, Liferay has announced the deprecation of
Websphere for Liferay DXP 7.4.</p>
<p>The official announcement was made here: <a
href="https://support.liferay.com/release-notes/2023-q4">https://support.liferay.com/release-notes/2023-q4</a>,
scroll down near the bottom to the <strong>Feature Status
Change</strong> section or just search the page for WebSphere and
you'll find the reference. Additionally, there's a note on the <a
href="https://help.liferay.com/hc/en-us/articles/4411310034829-Liferay-DXP-Quarterly-Releases-7-4-Compatibility-Matrix">7.4
Compatibility Matrix</a> that also indicates WebSphere is deprecated
as of the 2023.Q4 release.</p>
<p>The fault is on IBM really. WebSphere's latest version is 9, and
WAS 9 only supports <strong>JDK 8</strong> which is EOL.</p>
<p>By end of year, Liferay plans on being compatible with
<strong>JDK 21</strong> and updating the Java compile target and
third party dependencies, so that will leave WAS 9 completely in the
rearview as we drive away.</p>
<p>So if you're currently running Liferay on WebSphere and you're
on 7.4 and staying up to date on the quarterly releases, you may
want to start planning your migration off of WebSphere sooner,
rather than later.</p>
<p>For what it's worth, I'd recommend ditching any application
server besides Tomcat... The incompatibility with WebSphere and
higher JDKs is going to hit WebLogic and some of the other older
application servers that are no longer seeing updates.</p>
<p>Even for Tomcat, Liferay will be using an updated version to
leverage the newer JDKs. Being on Tomcat 9 now (or soon) will be an
easy transition to Tomcat 10+ when the time comes.</p>David H Nebinger2024-03-12T18:41:00ZDown with Web Contents, Long Live Objects!David H Nebinger/es/c/blogs/rss&entryId=1224350082024-03-09T04:06:51Z2024-03-09T03:35:00Z<h1> <a name="introduction">Introduction</a></h1>
<p>I was recently in a meeting reviewing some FreeMarker templates
that were extracting web contents using a structure, parsing and
processing the data, and rendering an output. Basically the
implementation was kind of the classic or "legacy" way of
doing specialized presentation of structured contents in Liferay.</p>
<p>In this case they had a Carousel implementation, so they had a
web content structure with the fields for the image, the title, the
summary, a link, etc. They also had the web content template in FM
that would render the carousel card, then they also had an Asset
Publisher w/ another template that could render the DOM for the
carousel, plus there was additional theme JS/CSS that were necessary
to make it all work together.</p>
<p>While this works, it does present a number of challenges. First
and foremost, it absolutely requires developer resources to handle
the FM templates and anything going on inside of the templates.
Additionally, I'm just cringing at all of the FM use in general
because I know that it's interpreted and also that it is probably
doing too darn much and will negatively impact performance.</p>
<p>Additionally, the web content is hard to use from a headless
perspective. I mean, sure you can get the article, but the fields may
be a lot harder to get to, plus you can't do things like request three
fields from the content, you like always get them all.</p>
<p>When you have content creators with different roles, i.e. some
are allowed to create Carousel records but others are not, you need
to permission your structures and templates correctly so the users
who can't create Carousel records can't access the structures, but
this in turn can cause issues if they are just browsing the site and
it has the structured contents on it...</p>
<p>There's just so much not to like about structured web contents,
especially given Liferay's adoption of low code/no code as a way to
empower business users to be able to create and maintain the content pages.</p>
<p>When you're on a later version of Liferay, there actually are
better ways to do this kind of thing. Come along with me while we
build this out. I know that at times you're going to be like "Why
is he doing that?" or "Why is he using this?", but I
encourage you to follow along and do the same things and, when we get
to the end and we're discussing pros/cons, it will all kind of make sense...</p>
<h1> <a name="challenge">Challenge</a></h1>
<p>So the challenge we're going to tackle here is building a
Carousel in Liferay. I can hear it now, "I already have at
least one carousel, why do I need another?"</p>
<p>The point here is not to present just another carousel, the
point here is to present an alternative way to capture, store and
render custom structured content. A carousel is just a convenient
meme that everyone will understand.</p>
<p>It is taking the new and better way to approach structured data
and putting this way to use in a UI element that everyone can understand.</p>
<h1> <a name="defining-the-carousel">Defining the Carousel</a></h1>
<p>So our carousel is going to have the following fields:</p>
<ul> <li>A background image</li> <li>A title</li> <li>A summary
text</li> <li>A call to action URL</li> </ul>
<p>So our carousel will have all of the typical stuff, there will
also be a call to action button where we can select a page to
navigate to if the user clicks the button.</p>
<p>We could add more things on here like a "read more..."
link and, you know, other custom structure fields as necessary to
define what will be displayed.</p>
<h1> <a name="defining-the-object-defn">Defining the Carousel
Object Definition</a></h1>
<p>If you're familiar with the classic way of handling structured
content, you're probably already navigating to the
<strong>Site</strong> menu, <strong>Content & Data</strong>,
<strong>Web Content</strong> to create the structure.</p>
<p>But you'd be wrong. We're actually going to head to the
<strong>Waffle</strong>, <strong>Control Panel</strong>,
<strong>Objects</strong> so we can define our Object there.</p>
<p>We're actually going to use an Object for the structured data
instead of a Web Content. We'll save the full list of whys for the
conclusion of the article, but at this point the only thing that
matters is that we can create an Object that contains all of the
fields we need for rendering.</p>
<div class="overflow-auto portlet-msg-info"> <p>Before we go
further, a bit of setup...</p> <p>So I'm using DXP Q4.6
release, this should equate to CE GA 102. Newer should work,
even older may work, but UIs may change and feature set,
etc.</p> <p>Additionally, I've navigated to the Waffle, Instance
Settings, Feature Flags and have enabled the following feature
flags. These will change the UI from the default views, so
you'll want to enable these settings also. In newer or older
versions these flags may be permanently enabled or
promoted/demoted or not available at all. For the most part this
shouldn't matter much as the blog doesn't pivot on these specific
changes but it will give you a look at the things to come...</p>
<p>Oh and I've also edited the images to remove the irrelevant
items, so don't worry if your list doesn't match this one exactly,
just enable the same ones selected below.</p> <p> <img
data-fileentryid="122437384"
src="https://liferay.dev/documents/portlet_file_entry/14/structured-content-01.png/710d8e97-aaf1-e69e-0e0e-b583f76676e3">
<br> <img data-fileentryid="122437396"
src="https://liferay.dev/documents/portlet_file_entry/14/structured-content-02.png/f29545c4-fc3c-f1ab-da5a-2b95ce2f7fc9">
</p> </div>
<p>Since I like to stay a bit organized, the first thing I'm going
to do is create an Object Folder named <strong>Structured
Contents</strong>. This is where I would create different types of
objects that represent structured contents that I plan on displaying.</p>
<p>In the Structured Contents folder, I'm going to create a new
object called <strong>Carousel Entry</strong>.</p>
<p> <img data-fileentryid="122437420"
src="https://liferay.dev/documents/portlet_file_entry/14/structured-content-03+%281%29.png/0a399853-742d-6119-0114-38564abacbf7"
style="height: auto;width: 454.0px;display: block;margin-left: auto;margin-right: auto;"
width="454">Once my entry is created, and since it's my first one, my
list is pretty simple:</p>
<p> <img data-fileentryid="122437432"
src="https://liferay.dev/documents/portlet_file_entry/14/structured-content-04.png/093bf995-a9e4-dcd6-cc99-370b149e9a42">
<br> Click into the <strong>Carousel Entry</strong> object to define
it further.</p>
<p>The very first decision we need to make on our object is the
<strong>Scope</strong>. We have two options, we can pick
<em>Company</em> (aka Instance) and this would be akin to making the
carousel entry available to all sites (basically same as creating web
content in the global site), or we can pick <em>Site</em> and the
carousel entries would only be visible in the site that we define them for.</p>
<p>In my implementation, I'm going with <em>Site</em> because each
site would have individual content creators and they each have
different things they are displaying in their carousels, so its a good
choice for me. In other implementations or for other structured
contents, the choice may be better to use the <em>Company</em> scope.
Just be sure to pick the right one now since we can't easily change it
in the future.</p>
<div class="overflow-auto portlet-msg-info">Now, you might say that
you need both, maybe some that are shared and others which are not...
There are ways that you can do this (i.e. define a CompanyCarousel and
a SiteCarousel and pull the data together using a Blueprint search or
via API builder, etc), but those implementation details are beyond the
scope of this blog post...</div>
<p>With <em>Site</em> for my scope, I'm also going to set a panel
link as <strong>Site Administration > Content & Data</strong>
because that's the obvious place to have a control panel for
Carousels, yeah?</p>
<div class="overflow-auto portlet-msg-alert">So here's another
benefit for using Objects - you get specific control panels for each
one. The classic way, you have to go to Web Content, maybe navigate
into a folder, click the Add button, pick the right structure, then
you can start setting the fields. With content objects like this
one, a site admin can just go to the Carousel Entries control panel
and add new entries there.<img data-fileentryid="122437893"
src="https://liferay.dev/documents/portlet_file_entry/14/structured-content-07.png/2ffda7e0-c04f-b0db-fd9d-b2ebe386aaa9"> </div>
<p>After setting the scope and the panel link, I'm also going to
enable the <strong>Entry History in Audit Framework</strong> option
because I want to track the changes made to the entries.</p>
<div class="overflow-auto portlet-msg-error">Actually I planned on
enabling object translations, and the screenshots here in the blog
reflect that. However, I encountered a bug in Liferay that translated
fields in site-scoped objects cannot be mapped. Since I was more
interested in finishing the blog than I was on solving the defect, I
opened a bug report, deleted my object and re-created the object but
without the translations enabled. Maybe by the time you are reading
this, translation mapped fields will be fixed and you can proceed as I
started, but if you hit the same issue as me, at least you'll know
what the cause was.</div>
<p>Click the <strong>Save</strong> button (not the
<strong>Publish</strong> button), then go to the
<strong>Fields</strong> tab to define the fields for the carousel entries.</p>
<p>I've jumped ahead and added my 4 fields. They are pretty
straight forward:</p>
<p> <img data-fileentryid="122437464"
src="https://liferay.dev/documents/portlet_file_entry/14/structured-content-05.png/dcd16fda-206c-5f65-e340-59703d4f0ff1">
<br> The only one that needs more explanation is the
<strong>Background Image</strong> guy. It's type is
<strong>Attachment</strong>, and for the <strong>Select Files</strong>
dropdown I selected the <strong>Upload or Select from Documents and
Media Item Selector</strong>. There's not much other configuration
you can do directly in the form, but once you add the field and then
click on it in the list, the right-side fly out gives you a few more
options. I've edited mine as follows:</p>
<p> <img data-fileentryid="122437584"
src="https://liferay.dev/documents/portlet_file_entry/14/structured-content-06.png/53423391-affd-b8db-e295-eb134f7d1bd1">
<br> By limiting the file types to images, I know that I'm only going
to allow the user to select image files. I've also set the max file
size to <strong>0</strong> because even though I'd prefer images of
<strong>100MB</strong> or less, I just can't guarantee that all images
will conform to that limitation.</p>
<p>We have what we need now for the Carousel Entry object, so we
can go to the <strong>Details</strong> tab and now
<strong>Publish</strong> the Object Definition.</p>
<div class="overflow-auto portlet-msg-info">In my own
implementation, I did take a moment before publishing to define a
default <strong>Layout</strong> (in the <strong>Layouts</strong>
tab) and <strong>View</strong> (in the <strong>Views</strong> tab).
These aren't really necessary, but they will be used by the
generated Control Panel, so defining these will let you control the
presentation. You can add this before publishing the object or come
back and add after publishing the object. Just remember that the
default Layout and View will be used by the Control Panel.</div>
<p>At this point we have defined an Object that represents what our
structured data represents, in this case it is an individual carousel
entry. We also have a control panel entry that content admins can use
to define new carousel entries, so we have the data entry partially covered.</p>
<p>The next part we need to address is rendering the carousel with
our new carousel entry objects.</p>
<h1> <a name="building-carousel-display">Building The Carousel Display</a></h1>
<p>The cornerstone of the carousel display is going to involve
using a <strong>Collection Display</strong> fragment on a
content page. The <strong>Collection Display</strong> fragment
is bound to a collection (or a collection provider) and typically
handles the rendering of each item in the collection, but also is
done using fragments and mappings to fields within the item (in this
case a Carousel Entry) to display.</p>
<p>What we want to display is a standard sort of Carousel, so the
large background image, highlighted title and summary detail, plus a
"read more" type button to get to further detail.</p>
<p>Now to keep things simple (from an implementation perspective),
I am going to use <a
href="https://swiperjs.com/">https://swiperjs.com/</a> as the
foundational implementation. This will require using a hierarchy of
divs with assigned classes...</p>
<p>To make this happen, we'll use a <strong>Header</strong>, a
<strong>Paragraph</strong>, and a <strong>Button</strong> fragments to
display the title, summary and call to action. In addition, we'll need
a container fragment for rendering the individual carousel entry
(Swiper calls them slides), (will have the assigned background image
and necessary Swiper classes), and an outer container fragment that
will wrap the collection display and pull all of the Swiper stuff together...</p>
<p>So basically I'll need <strong>two custom fragments</strong>,
one for the container at the item level, and the other is the outer
container that controls the carousel. I'll start with the item-level container...</p>
<h1> <a name="creating-carousel-slide">Creating the Carousel
Slide Container</a></h1>
<p>So we're going to create a custom fragment! So I navigated to
<strong>Design</strong> -> <strong>Fragments</strong>, then I
created a new <strong>Fragment Set</strong> titled <strong>Structured
Content Fragments</strong>, and in there I created a new
<strong>Basic</strong> Fragment which I called the <strong>Carousel Slide</strong>.</p>
<p>The Carousel Slide HTML content was really all that I defined
for the fragment, and it consists of:</p>
<pre>
<div class="swiper-slide carousel-slide"
data-lfr-background-image-id="carousel-slide-background-img">
<lfr-drop-zone></lfr-drop-zone>
</div>
</pre>
<p>So our <code><div /></code> has two classes assigned, one
is the <code>swiper-slide</code> which is used by SwiperJS, the
other is <code>carousel-slide</code> which can be used to style the
content of the slide itself.</p>
<p>The <code><div /></code> also has a background image
assigned, and using the <code>data-lfr-background-image-id</code>
attribute will allow us to map to an image selected when the fragment
is used. With this in place, when we are setting up the fragment in
the UI we'll be able to map to the image file that is assigned in the
Carousel Entry.</p>
<p>The <code><lfr-drop-zone /></code> tag inside of the
<code><div /></code> is what turns the div into a container;
we're declaring that any fragment can be dropped in the drop zone, so
once this container is used on a page we'll be able to drop the
<strong>Header</strong>, <strong>Paragraph</strong> and
<strong>Button</strong> fragments into the container.</p>
<p>In fact, I created a Carousel page and then started adding the
Carousel Slide fragment and dropped the Heading, Paragraph and Button
fragments inside of the container. It doesn't look pretty (at the
moment) and doesn't have the mapping enabled, but it certainly has the
structure and items that we'll be needing:</p>
<p> <img data-fileentryid="122454788"
src="https://liferay.dev/documents/portlet_file_entry/14/structured-content-08.png/135c6116-283d-647d-cd85-ace9f79a49b3"> </p>
<h1> <a name="creating-carousel">Creating the Carousel Container</a></h1>
<p>With the carousel slide out of the way, we can now move on to
the carousel container itself.</p>
<p>It's the responsibility of this container to set up and
configure SliderJS to work on the content, and even though it is a
container it will only hold the <strong>Collection Display</strong> fragment.</p>
<p>The <strong>Collection Display</strong> fragment will be bound
to the collection, then we'll be using the Carousel Slide container
for each item in the collection to display.</p>
<p>So again I go to <strong>Design</strong> ->
<strong>Fragments</strong> and in the <strong>Structured Content
Fragments</strong> fragment set I created a new Carousel
<strong>basic</strong> fragment. Like the Carousel Slide fragment, the
HTML for the Carousel fragment is pretty simple:</p>
<pre>
<div class="swiper carousel">
<div class="swiper-wrapper" id="swiperWrapper">
<lfr-drop-zone></lfr-drop-zone>
</div>
<div class="swiper-button-next"></div>
<div class="swiper-button-prev"></div>
<div class="swiper-pagination"></div>
</div></pre>
<p>Unlike the Carousel Slide fragment, the Carousel fragment has
some JS associated with it:</p>
<pre>
const editMode = document.body.classList.contains('has-edit-mode-menu');
if (!editMode) {
const script = document.createElement('script');
script.type = 'text/javascript';
script.src = 'https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js';
document.head.appendChild(script);
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = 'https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css';
document.head.appendChild(link);
script.onload = () => {
// eslint-disable-next-line
var swiper = new Swiper(".carousel", {
spaceBetween: 30,
effect: "fade",
navigation: {
nextEl: ".swiper-button-next",
prevEl: ".swiper-button-prev",
},
pagination: {
el: ".swiper-pagination",
clickable: true,
},
});
};
} else {
document.getElementsByClassName('swiper-wrapper')[0].style.zIndex = -1;
}
</pre>
<p>This script first figures out if currently in edit mode. When in
edit mode, the z index for the div is modified, but otherwise nothing happens.</p>
<p>The magic is when not in edit mode. The JS and the CSS for
Swiper are loaded, then the Swiper is initialized for the
<code><div /></code> with the <code>carousel</code> class.
When not in edit mode, the Carousel should work as expected. In edit
mode, the controls will be easier to access and configure because
the Swiper will not be activated, it will just render as a normal
Collection Display fragment.</p>
<h1> <a name="building-carousel-page">Building the Carousel Page</a></h1>
<p>We now have all of the pieces that we need to make the Carousel
so we are ready to create the page.</p>
<p>Before creating the page, I did go to <strong>Content &
Data</strong> -> <strong>Carousel Entries</strong> (the control
panel I get by setting the panel link in the object definition) and I
created a few entries. Google was a great help in finding some free
banner images which I downloaded and set on each of the carousel entries.</p>
<p>I then went to the <strong>Site Builder</strong> ->
<strong>Pages</strong> and created a new <strong>Carousel</strong>
page from the <strong>Blank Page</strong> template.</p>
<p>With the page in edit mode, I first dropped a
<strong>Carousel</strong> fragment on the page, within that I dropped
and configured a <strong>Collection Display</strong> fragment to use
the <strong>Carousel Entries</strong> collection provider, and in the
Collection Item drop zone I dropped the <strong>Carousel
Slide</strong> fragment, mapping the background image to the Carousel
Entry's background image, and then I dropped a
<strong>Header</strong>, <strong>Paragraph</strong> and
<strong>Button</strong> fragments into the Carousel Slide. I mapped
the Header to the Title, the Paragraph to the Summary, and the Button
Link to the Call To Action URL.</p>
<p>Thus my carousel was complete so I published the page.</p>
<p> <img data-fileentryid="122457435"
src="https://liferay.dev/documents/portlet_file_entry/14/structured-content-10.png/d3d10e25-bdc4-eefe-50db-4b11b68d231c"
style="height: auto;width: 515.0px;display: block;margin-left: auto;margin-right: auto;"
width="515">And... nothing. The published page had my stuff on it, the
DOM elements were there, I even verified that Swiper JS and CSS were
being loaded...</p>
<p>I then took a look at some of the sample Swiper demos, and I
found that their DOM was like:</p>
<pre>
<div class="swiper mySwiper">
<div class="swiper-wrapper">
<div class="swiper-slide">
...
</div>
<div class="swiper-slide">
...
</div>
<div class="swiper-slide">
...
</div>
<div class="swiper-slide">
...
</div>
</div>
<div class="swiper-button-next"></div>
<div class="swiper-button-prev"></div>
<div class="swiper-pagination"></div>
</div>
</pre>
<p>When I checked the DOM for what my collection display fragment
was using, I had <div /> inside of <div /> inside of
<div />, many more levels than what SwiperJS was expecting.</p>
<p>So I changed the javascript on the Carousel fragment to be:</p>
<pre>
const editMode = document.body.classList.contains('has-edit-mode-menu');
if (!editMode) {
const script = document.createElement('script');
script.type = 'text/javascript';
script.src = 'https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js';
document.head.appendChild(script);
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = 'https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css';
document.head.appendChild(link);
script.onload = () => {
var slides = document.getElementsByClassName('carousel-slide');
var slidesHtml = '';
for (let item of slides) {
slidesHtml = slidesHtml + item.outerHTML;
}
var wrapper = document.getElementById('swiperWrapper');
wrapper.innerHTML = slidesHtml;
// eslint-disable-next-line
var swiper = new Swiper(".carousel", {
spaceBetween: 30,
effect: "fade",
navigation: {
nextEl: ".swiper-button-next",
prevEl: ".swiper-button-prev",
},
pagination: {
el: ".swiper-pagination",
clickable: true,
},
});
};
} else {
document.getElementsByClassName('swiper-wrapper')[0].style.zIndex = -1;
}
</pre>
<p>This allowed all of the fragments to be rendered, but then I
found all of the carousel-slide divs, concatenated their DOM, then
overrode the innerHTML on the swiperWrapper element, basically
removing all of the extra <div />s that the collection display
was using that I no longer needed.</p>
<p>After saving and propagating the fragment, my page now looked like:</p>
<p> <img data-fileentryid="122457402"
src="https://liferay.dev/documents/portlet_file_entry/14/structured-content-09.png/2d37d7f5-f40e-8aef-5eb7-36040c30cf17">
<br> Sure, it still needs a bit of style applied to it (beyond what I
had done in my testing), but the carousel itself works, the
forward/backward arrows work as does the pagination dots at the bottom
of the page.</p>
<p>So while it's not quite ready for production, it is certainly
close enough to prove the thing is doable.</p>
<h1> <a name="stop-or-continue">Stop or Continue?</a></h1>
<p>So my implementation looks good, so now the question becomes,
should I stop here or continue on?</p>
<p>Consider:</p>
<ul> <li>My Carousel Entry collection provider returns all of the
entries defined for the site. As the content creator, I can use the
generated control panel to add new entries, but I'd also have to use
it to delete the entries that should no longer be in the carousel. I
could add a boolean checkbox to the Carousel Entry to display the
entry or not, then add a special Blueprint Collection Provider
(enabled via a feature flag) that would return all Carousel Entries
that had the boolean checked.</li> <li>I only allow for a single
carousel per site, but maybe different pages would need different
carousels. I could add a key field to the Carousel Entry, plus a
Blueprint Collection Provider to filter for specific keys. That way
my carousel would be tied to a specific key and that key would match
a set of Carousel Entries that had the same key.</li> <li>Content
creators would need to have access to the control panel to get to
the Carousel Entries panel. I can use a regular old Form Container
bound to the Carousel Entry object on a protected page so content
creators could manage the Carousel Entries without navigating to the
control panel at all.</li> <li>In fact, I could use a "content
creator" segment to add list and form views under the carousel
so they could make changes in the page itself, but the segment
wouldn't be available to others, they'd only see the carousel.</li>
<li>As objects, the Carousel Entries are available via the Headless
API generated for the object, also available via the Headless Batch
API, are subject to the new Datasets and Data Migration Center
features in Liferay, so I have a long list of options available to
support export/import and content promotion/demotion between
environments.</li> <li>Objects are subject to Workflow and
Publications, so these tools can be leveraged to review and approve
content prior to going live.</li> </ul>
<p>Given all of these opportunities (and probably others which I've
overlooked), although I'm choosing to stop at this point, there's no
reason why you couldn't continue and build out the solution you need...</p>
<h1> <a name="conclusion">Conclusion</a></h1>
<p>So granted this Carousel was a contrived solution, but generally
it is a good representation of what is possible if you can get away
from web content structures and focus on Objects for your structured
content needs.</p>
<p>Doing so offers the following advantages:</p>
<ul> <li>You can get a specific control panel for your structured
content, making creating and maintaining these a lot easier because
they are permissioned separately and don't push every solution into
web content and the web content display.</li> <li>Eliminates the
need to use FreeMarker, instead leveraging the Collection Display
fragment and other fragments with field mappings to handle a
customized layout but created visually without support from
developers.</li> <li>Because I get a complete Headless API for my
Carousel Entry objects, I can easily access, create, update and
delete entries without having to jump through the structured web
content hoops.</li> <li>Impressive number of export/import options
to support moving Objects between environments where traditional
Liferay tools (i.e. LAR export/import) tend to fail easily and
often.</li> <li>Using form container fragments, you can move your
content creators out of the control panel entirely, giving them
either generated or handmade (well, handmade in the UI, not handmade
as in writing code or JSPs or what not) interfaces for maintaining
content outside of the control panel.</li> </ul>
<p>By leveraging Objects and custom fragments, it is possible to
envision a world where, instead of using OOTB Liferay entities such as
blogs, wikis, etc, perhaps you build them as Objects and custom
fragments for rendering?</p>
<p>Or taking what you previously would do in a web content
structure with FreeMarker web content templates and reworking them
as Objects/Fragments?</p>
<p>Before you start down this road, it is important to consider the disadvantages/shortcomings:</p>
<ul> <li>Objects lack some of the field types (and custom field
types) that you might define for a web content structure.</li>
<li>Objects lack the kind of composition support you now have with
Web Content Structures and Field Sets. Objects can have
relationships, but the lack of a 1:1 relationship is still a
drawback here.</li> <li>An "Object Display" fragment is
missing... I would want to be able to drop an object display
fragment onto a page (like a web content display), select the object
to display, then drop child fragments in the container and map to
the object fields for display. This way I could drop a bunch of
Object Display fragments on a page, point them at different objects,
and use fragments w/ field mappings for the display. Right now you
are kind of stuck going through a collection display / collection
item even when you only have a single object you want to display,
plus you have to leverage a blueprint to get there. Don't worry, I
did open a feature request for this guy, now I just have to fight to
get him added to the roadmap...</li> </ul>
<p>So there you have it, a brand new way to approach structured
data/structured content by leveraging Objects and Fragments instead of
Web Contents.</p>
<p>While they're not yet at feature parity, I'd still argue that
the benefits you get from this approach can be worth becoming an
early adopter of this technique.</p>
<p>Let me know in the comments below the kind of structured
contents you build using this technique, what kinds of things you
find easy to do, and especially call out things that you think are
missing (like the Object Display fragment) so we can take them to
the product team and try to get them implemented.</p>David H Nebinger2024-03-09T03:35:00ZLiferay Certification ChangesDavid H Nebinger/es/c/blogs/rss&entryId=1224465932024-03-05T16:48:07Z2024-03-04T18:12:00Z<p>If you've recently been looking for the Liferay Certifications, tried
to find the materials or register to take a certification exam, you
may have found that they are no longer where they used to be.</p>
<p>In the near future Liferay will be unveiling and launching a new
Enablement and Certification program that will be a major step forward
in delivering concrete enablement for our partners, clients and community.</p>
<p>To make way for that announcement, we have made the following changes:</p>
<ul> <li> <b>Retiring Legacy Certifications:</b> User purchase
of previous certification programs has been discontinued. This
includes all of the various versions of “Front-end” / ”Back-end”
developer exams that were associated with the various Liferay 6
and earlier 7 versions. Pages on <a href="http://liferay.com/"
target="_blank" rel="noopener noreferrer">liferay.com</a> that
refer to these certifications were removed and redirected to <a
href="https://www.liferay.com/learning" target="_blank"
rel="noopener noreferrer">https://www.liferay.com/learning</a>.</li>
<li> <b>Content Accessibility:</b> While the old certification
process will no longer be available to purchase, the
certification content will remain available on YouTube, and
presented as <a
href="https://learn.liferay.com/w/reference/liferay-university"
target="_blank" rel="noopener noreferrer">an index</a> on <a
href="http://learn.liferay.com/" target="_blank"
rel="noopener noreferrer">learn.liferay.com</a>}, in case there are
users who require access to those older resources.</li> <li>
<b>New Design for <a href="http://learn.liferay.com/"
target="_blank"
rel="noopener noreferrer">learn.liferay.com</a></b>: We will be
making major changes to how we present Courses, Learning Paths, and
Certifications on <a href="http://learn.liferay.com/"
target="_blank" rel="noopener noreferrer">learn.liferay.com</a> in
the coming months to deliver a great experience to all of our
users.</li> <li> <b>Why Are We Doing This?</b>: Certifications
based upon outdated content that is at least two years removed
from the current state of Liferay’s capabilities and offerings
only certifies how things used to be done and doesn't
demonstrate an understanding of how to deliver current solutions
on Liferay.</li> </ul>
<p>These changes serve as the prologue to significant evolutions in
our Enablement and Certification strategy. Thank you for your patience
as we continue to evolve the Enablement program!</p>
<p>For further information, the FAQ and recent updates, check <a href="https://www.liferay.com/learning">https://www.liferay.com/learning</a>...</p>
<div class="overflow-auto portlet-msg-error">If you have previously
paid for an exam but scheduled it for a future date, WebAssessor may
still show your exam as scheduled, but it really isn't. You'll likely
be turned away at the door.<br> <br> If you are in this boat, the FAQ
on <a
href="https://www.liferay.com/learning">https://www.liferay.com/learning</a>
to explain how to request a refund.</div>David H Nebinger2024-03-04T18:12:00ZLiferay CE 7.4 Removes Support for ClamAVDavid H Nebinger/es/c/blogs/rss&entryId=1224341722024-02-29T18:48:58Z2024-02-29T17:00:00Z<p>So this has come up a couple of times recently, so after some
research I thought I'd share the news.</p>
<p>In 7.4, built-in support for ClamAV has been removed, and
Liferay has no current plans on restoring that functionality.</p>
<p>If you have upgraded from a previous version and you were using
ClamAV, you may not be aware that your antivirus scanning actually has
not been happening as there is nothing in CE 7.4 to tell you that
there are no instances of
<code>com.liferay.document.library.kernel.antivirus.AntivirusScanner</code>
available to actually perform the virus scanning.</p>
<p>If you want or need virus scanning in CE 7.4, your only option
at the moment is to roll your own implementation of
<code>AntivirusScanner</code> and implement the functionality yourself.</p>
<p>The only guidance I can give you here is to look towards the CE
7.3 implementation code as it can help frame how you need to build
the implementation.</p>
<div class="overflow-auto portlet-msg-alert">If you are on Liferay
DXP 7.4 (or you upgrade from CE to DXP), you don't have to worry
about ClamAV support there, the ClamAV connector is still fully part
of the product, just configure for your ClamAV service and you're
good to go.</div>David H Nebinger2024-02-29T17:00:00ZCreating a Blockchain in LiferayAndre Fabbro/es/c/blogs/rss&entryId=1224112042024-02-21T16:53:02Z2024-02-20T22:57:00Z<h1>Introduction</h1>
<p>The Liferay platform has been used for the development of
corporate applications by many companies for several years. This is
because it stands out for its flexibility in extending its
ready-made components, broadly meeting the needs of business
applications, such as security, clustering, indexing, and internationalization.</p>
<p>However, directly building corporate systems on the platform can
complicate future updates due to changes in the internal API. Liferay
Inc. has, therefore, focused on features that allow extension to
external runtimes, minimizing the impact of updates on the API.
Concurrently, the platform encourages the development of low-code
applications, facilitating the updating of versions.</p>
<p>In this context, this article aims to explore these low-code
features of Liferay, demonstrating the construction of a blockchain
and the mining of a fictitious cryptocurrency, RayCoin, in addition to
presenting a system for managing digital wallets and transactions,
highlighting Liferay's adaptability to various business needs.</p>
<h1>Blockchain Fundamentals</h1>
<p>Before we proceed with the implementation using Liferay, it's
crucial to understand the fundamental concepts of the blockchain data
structure. This structure is essentially a sequence of interconnected
blocks, starting with the initial block, known as the "genesis
block". Each subsequent block maintains a reference to its
predecessor through a cryptographic hash, generated from the block's
information and including the previous block's hash. This methodology
ensures that the blockchain is a reliable and virtually immutable data structure.</p>
<p> <img data-fileentryid="122411229"
src="https://liferay.dev/documents/portlet_file_entry/14/image-01.png/28b9bcf9-2bce-2774-3762-6491bd2194d7">
<br> The connection between consecutive blocks in the blockchain is
established through a hash, which is calculated from the information
contained in the block itself and incorporates the hash of the
preceding block. Thus, the hash of each block is derived from a
dataset that includes the current block's information along with the
previous block's hash. This approach, despite its apparent simplicity,
is profoundly ingenious, giving the blockchain its characteristic of
being a highly secure and practically immutable data structure.</p>
<p>Understanding the fundamental concept of blockchain, it is
essential to detail the components and attributes that make up this
structure. The central element is the Block, with the following attributes:</p>
<table border="0" cellpadding="0" cellspacing="0"
style="width: 100.0%;"> <tbody> <tr> <td
style="width: 167.0px;"><img data-fileentryid="122411256"
src="https://liferay.dev/documents/portlet_file_entry/14/image-02.png/810ba405-7e7d-6607-66e6-ec252c045613">
<br> </td> <td style="width: 558.0px;"> <ul> <li>
<strong>Index</strong>: Identifies the block's position in
the chain.</li> <li>
<strong>Header</strong>: Contains the calculated hash of the
block.</li> <li> <strong>Previous
Hash</strong>: Refers to the hash of the
preceding block, connecting it in the chain sequence.</li>
<li> <strong>Nonce</strong>: A unique number
used to find the solution to the cryptographic
problem that allows the block to be mined,
adjusting to the current level of difficulty.</li>
<li> <strong>Transactions</strong>: A set of all
transactions included in the block, detailing
transfers of value between different wallet
addresses.</li> </ul> </td> </tr> </tbody> </table>
<p> <br> Therefore, a block not only stores its intrinsic
attributes but also encapsulates multiple transactions. These
transactions, representing debit and credit movements between
digital wallets, are fundamental to the immutability of the
blockchain, as the hash of each block is calculated by incorporating
these data. In the context of this article, in building a
cryptocurrency system, blocks contain transactions involving
RayCoin, evidencing transfers between digital wallets associated
with this fictitious cryptocurrency.</p>
<p> <img data-fileentryid="122411269"
src="https://liferay.dev/documents/portlet_file_entry/14/image-04.png/9e52da9c-7208-6fdb-aa80-cfc77196f3f7">
<br> Continuing with the detailing of the blockchain elements, each
transaction within a block is defined by specific attributes that
ensure its integrity and traceability:</p>
<ul> <li> <strong>Transaction Id</strong>: Serves as a unique
identifier for each transaction, ensuring its uniqueness within
the chain.</li> <li> <strong>Address From</strong>: Denotes the
digital wallet address from where the values will be debited,
initiating the transfer of funds.</li> <li> <strong>Address
To</strong>: Specifies the transaction's destination, i.e., the
digital wallet address where the values will be credited.</li>
<li> <strong>Signature</strong>: Represents the digital
signature generated by the sender, essential for verifying the
transaction's authenticity and authorizing the debit.</li> <li>
<strong>Amount</strong>: Quantifies the value of the transaction,
expressed in the cryptocurrency's unit, facilitating the
understanding of the transferred volume.</li> <li>
<strong>Status</strong>: Indicates the current condition of the
transaction within the system, which can vary between states such as
approved, pending, or denied, reflecting its processing and
validation.</li> </ul>
<p>This detailed structuring of transactions not only reinforces
the security and transparency of the blockchain system but also
facilitates the tracking and auditing of all movements.</p>
<p>Finally, we address the representation of a digital wallet, a
fundamental element for blockchain users, enabling the management and
transaction of cryptocurrency units. The conception of a digital
wallet is straightforward, encompassing essential attributes for its
functionality and security:</p>
<p> <img data-fileentryid="122411278"
src="https://liferay.dev/documents/portlet_file_entry/14/image-05.png/0c095137-cd48-492b-516d-1fe699e46668">
<br> The digital wallet is quite simple, composed of the following attributes:</p>
<ul> <li> <strong>Name</strong>: Informative designation of
the wallet, facilitating identification by the user.</li>
<li> <strong>Public Key</strong>: Acts as the address for
sending and receiving cryptocurrency units, functioning as a
public identifier of the wallet.</li> <li> <strong>Private
Key</strong>: Essential for signing transactions, ensuring
the authorship and security of financial movements. This is
sensitive data, known exclusively to the wallet's owner.</li> </ul>
<p>This article proposes the construction of a blockchain composed
of blocks that record transfer transactions of the cryptocurrency
RayCoin between different wallet addresses. Understanding how each
block is formed, through the mining process, is crucial.
Traditionally, mining involves solving a complex cryptographic
problem that requires significant computational effort. In most
blockchain networks, the node that solves this challenge first
propagates the new block to the others, initiating a consensus
mechanism to validate the block's authenticity. If validated, the
block is added to all participants' blockchain. Several consensus
mechanisms exist, with Proof-of-Work (PoW) being one of the most
well-known. For the purposes of this article, we will focus on
constructing a single node of the blockchain, without the ambition
of developing a complete network, and, consequently, without
implementing a consensus algorithm.</p>
<p>The generation of blocks occurs when a defined number of
transactions is reached, triggering the mining process that
incorporates these transactions into the new block. This block is then
added to the chain, containing its own hash, calculated from the
included transactions and the previous block's hash. As part of the
mining process, units of RayCoin are credited to the miner's wallet as
a reward, a value also defined by the system. Thus, the blockchain
grows with the continuous addition of new blocks, reflecting the
received RayCoin transactions.</p>
<p> <img data-fileentryid="122411298"
src="https://liferay.dev/documents/portlet_file_entry/14/image-06.png/b6f6d371-a55a-eb43-78ae-43d2708c2250">
<br> </p>
<h1>Project Plan</h1>
<p>After understanding the fundamental concepts of blockchain
technology, it is imperative to establish a clear strategy for project
implementation. In this context, we will explore the resources and
functionalities provided by the Liferay platform, which will form the
basis for developing a comprehensive solution. Recognized for its
robustness and adaptability, the Liferay platform offers a series of
low-code tools that allow for the rapid construction and customization
of applications, without compromising the complexity or security
necessary for a blockchain project. Our goal is to demonstrate how
these resources can be effectively used to create a functional
blockchain structure, including the mining of a fictitious
cryptocurrency and the management of digital wallets, leveraging the
full capabilities of Liferay.</p>
<h2>Multi-tenancy</h2>
<p>To simulate the process of buying and selling RayCoin, the
multi-tenancy functionality of Liferay will be adopted, which allows
for the creation of multiple logical instances within a single
physical instance. This feature is crucial for structuring the
project, where two distinct instances will be configured:</p>
<ul> <li> <strong>RayCoin Instance</strong>: Aimed at building
the blockchain itself, this instance is responsible for
receiving transactions and mining the blocks, which will later
be organized in the chain.</li> <li> <strong>XChangeRay
Instance</strong>: Designed to simulate an external service,
this instance functions as a cryptocurrency exchange, where
users can manage their RayCoin digital wallets and initiate
transactions. The goal is to facilitate user interaction with
the blockchain, allowing for the sending of transactions to the
first instance in a simplified and intuitive manner.</li> </ul>
<p> <img data-fileentryid="122411309"
src="https://liferay.dev/documents/portlet_file_entry/14/image-07.png/7410d971-2c53-dbee-a5ef-8fce2b779f06">
<br> </p>
<h2>Liferay Objects</h2>
<p>To effectively represent the entities necessary for our
blockchain solution in Liferay, we will employ a feature called
Liferay Objects: <a href="https://learn.liferay.com/w/dxp/building-applications/objects">https://learn.liferay.com/w/dxp/building-applications/objects</a></p>
<p>This functionality is a cornerstone of the low-code concept
offered by the platform, allowing for the creation of custom objects
that represent entities for the development of customized solutions.
Through Liferay Objects, the platform provides an integrated approach
to all the necessary layers for the persistence of these objects in
the context of business logic, including the automatic generation of
an integration layer via RestFul or GraphQL protocols. The Liferay
Objects framework stands out for its versatility and power, enabling a
wide range of customizations and extensions.</p>
<p>In this project, the objects to be created to compose our
blockchain solution include:</p>
<ul> <li> <strong>Blockchain</strong>: Object that
encapsulates the main information of the blockchain.</li>
<li> <strong>Block</strong>: Represents the individual blocks of
the chain, containing transactions, their unique hash, and the
reference to the hash of the previous block.</li> <li>
<strong>Transaction</strong>: Used to record transactions and
associate them with the corresponding blocks after mining.</li>
<li> <strong>Wallet</strong>: Allows users to create and manage
their digital wallets, essential for signing the transactions
sent to the blockchain.</li> <li> <strong>Wallet
Balance</strong>: Stores the balances of wallets, facilitating
the consultation of balances without the need to traverse the
entire block chain for transaction validation.</li> </ul>
<h1>Implementation</h1>
<p>After an initial understanding of the main functionalities that
will support the implementation, we are ready to begin the project
setup. The first step involves activating a Liferay instance in a
local environment. For this article, we will use Liferay version
GA109. The instance can be easily set up in a Docker environment using
the following command:</p>
<pre>
<code>
docker run -it -m 8g -p 8080:8080 liferay/portal:7.4.3.109-ga109
</code>
</pre>
<p>In this project, we intend to create two logical instances in
Liferay, each operating under a distinct Virtual Host, thereby
allowing each instance to respond to a specific address:</p>
<ul> <li> <strong>RayCoin Instance</strong>: Will be the core
of the Blockchain, responsible for processing transactions
and mining blocks, forming the block chain.</li> <li>
<strong>XChangeRay Instance</strong>: Will simulate an external
service, functioning as a cryptocurrency exchange, where users can
manage their RayCoin digital wallets and execute transactions.</li> </ul>
<p>To configure the Virtual Hosts in the local environment, it is
necessary to edit the <code>/etc/hosts</code> file (Linux) or
<code>C:/Windows/System32/Drivers/etc/hosts</code> (Windows),
including the following line:</p>
<pre>
127.0.0.1 raycoin.local xchangeray.local</pre>
<p>This directs accesses to the addresses
http://raycoin.local:8080/ or http://xchangeray.local:8080/ to
http://localhost:8080/, allowing the running Liferay physical
instance to recognize and load the appropriate logical instance
based on the accessed Virtual Host. With Liferay active, accessing
http://raycoin.local:8080/, the main instance will be loaded. After
logging into this instance, navigate to Control Panel -> Virtual
Instances, and in the instance with Web Id equal to
"liferay.com", edit the Virtual Host to include "raycoin.local".</p>
<p> <img data-fileentryid="122411321"
src="https://liferay.dev/documents/portlet_file_entry/14/image-08.png/e37c2e80-5e5e-b38f-b3f3-d6e9b99cfeb3">
<br> </p>
<p>Next, proceed with creating a new virtual instance, entering the
necessary information:</p>
<ul> <li>Web Id: xchangeray</li> <li>Virtual Host:
xchangeray.local</li> <li>Mail Domain: liferay.com</li> <li>Virtual
Instance Initializer: Blank Site</li> </ul>
<h2> <img data-fileentryid="122411330"
src="https://liferay.dev/documents/portlet_file_entry/14/image-09.png/1002133b-3c95-d566-a147-7d0717364253">
<br> XChangeRay Instance</h2>
<h3>Wallet Object</h3>
<p>After creating the new instance, access
http://xchangeray.local:8080/ and log in as an administrator, then go
to the menu Control Panel -> Objects to create the first object.
Access the option to add a new custom object (button '+') and include
the Wallet object information:</p>
<ul> <li>Label: Wallet</li> <li>Plural Label: Wallets</li> </ul>
<p> <img data-fileentryid="122411340"
src="https://liferay.dev/documents/portlet_file_entry/14/image-10.png/e46d4f9e-5b31-a086-b36d-2f20d61ee698">
<br> </p>
<p>When creating a new Object in Liferay, we are essentially
defining the records that will compose this Object. Initially,
Liferay keeps these definitions in a draft state, allowing
adjustments and refinements until the user finalizes the settings
and opts to publish the Object.</p>
<p>Upon accessing the panel of the newly created Object, a range of
tabs becomes available for exploration: Details, Fields,
Relationships, Layouts, Actions, Views, Validations, and State
Manager. Each of these tabs unlocks distinct features, offering
substantial flexibility for composing complex solutions. For example,
in the Fields tab, it's possible to create custom fields that
specifically align with the business logic of the application. This
level of customization emphasizes the power and versatility of the
Objects framework within Liferay.</p>
<p> <img data-fileentryid="122411349"
src="https://liferay.dev/documents/portlet_file_entry/14/image-11.png/15084f95-6578-1c82-79a5-b019a32e2e63">
<br> <br> To create a custom field, go to the Fields tab, trigger the
'+' button (Add Object Field) and fill in the field information.</p>
<p> <img data-fileentryid="122411368"
src="https://liferay.dev/documents/portlet_file_entry/14/image-13.png/2a6d37e3-229b-a5e2-7f26-413beeb6c8ec">
<br> </p>
<p>Therefore, for this Wallet object, create the following custom fields:</p>
<table border="1" cellpadding="0" cellspacing="0"
style="width: 100.0%;"> <thead> <tr> <th scope="col">Label</th>
<th scope="col">Field Name</th> <th scope="col">Type</th>
<th scope="col">Mandatory</th> </tr> </thead> <tbody> <tr>
<td>Name</td> <td>name</td> <td>Text</td> <td>Yes</td>
</tr> <tr> <td>Private Key</td> <td>privateKey</td>
<td>Long Text</td> <td>No</td> </tr> <tr> <td>Public
Key</td> <td>publicKey</td> <td>Long Text</td>
<td>No</td> </tr> </tbody> </table>
<p> <br> When defining custom fields for the Wallet object, it
becomes essential to establish a logic for the automatic generation
of public and private keys every time a new Wallet is created. This
functionality is managed by the "Actions" tab of the object,
allowing the insertion of business logics at different stages of the
object's lifecycle, such as its creation or update.</p>
<p>Specifically for the Wallet object, it is necessary to generate
a new private key and a public key at the act of creating the
object. For this, go to the "Actions" tab, trigger the
"Add Object Action" button, filling in the "Action
Label" field with "SetKeys".</p>
<p> <img data-fileentryid="122411395"
src="https://liferay.dev/documents/portlet_file_entry/14/image-16.png/3a577e59-0034-8955-fe53-b910ec28b2a8">
<br> </p>
<p>Navigate to the "Action Builder" tab. Here, we define
the logic to be executed, choose "On After Add" in the
"Trigger" option so that the action occurs immediately after
creating a new record.</p>
<p> <img data-fileentryid="122411404"
src="https://liferay.dev/documents/portlet_file_entry/14/image-17.png/677f6828-628c-ff14-9eda-79c25c0077b3">
<br> </p>
<p>Select the "Groovy Script" option for the
"Action" section. This Groovy script, leveraging the
available attributes of the object, executes the desired logic,
including the generation of keys.</p>
<p> <img data-fileentryid="122411413"
src="https://liferay.dev/documents/portlet_file_entry/14/image-18.png/df34d4c0-20e4-2923-f0df-458338246daa">
<br> </p>
<p>Upon selecting "Groovy Script," note that a field
opens up for including the script.</p>
<p> <img data-fileentryid="122411423"
src="https://liferay.dev/documents/portlet_file_entry/14/image-19.png/0cd5e5b2-98dd-14f2-82e8-71e0bc425f50">
<br> </p>
<p>To generate the keys, we will implement in this script a class
named Wallet, as below:</p>
<pre>
class Wallet {
def KeyPair keyPair
Wallet() {
this.keyPair = this.generateKeyPair()
}
String getPrivateKey() {
return Base64.encoder.encodeToString(keyPair.getPrivate().getEncoded())
}
String getPublicKey() {
return Base64.encoder.encodeToString(keyPair.getPublic().getEncoded())
}
private KeyPair generateKeyPair() {
def keyGen = KeyPairGenerator.getInstance("EC")
def ecSpec = new ECGenParameterSpec("secp256r1")
keyGen.initialize(ecSpec, new SecureRandom())
return keyGen.generateKeyPair()
}
}
</pre>
<p>With this class, when a new Wallet object is created, the method
generateKeyPair() will automatically be called, generating the KeyPair
using classes from the java.security package. Then, the methods
getPrivateKey() and getPublicKey() will return the private and public
keys, respectively, already in string format, to be stored in the
corresponding fields of the object.</p>
<p>To store the keys in the object, we will use the
updateObjectEntry() method from the ObjectEntryLocalServiceUtil class
of Liferay's Objects API. The code should be as follows: </p>
<pre>
def wallet = new Wallet()
def userId = Long.valueOf(creator)
def entryValues = [privateKey: wallet.privateKey, publicKey: wallet.publicKey]
ObjectEntryLocalServiceUtil.updateObjectEntry(userId, id, entryValues, new ServiceContext())
</pre>
<p>From this code, it's observable that the object fields can be
passed through a Map, in this case, entryValues, where we include the
public and private keys for the object with the id that is in the
context (variable id). The final script should be as follows:</p>
<pre>
import java.security.KeyPair
import java.security.KeyPairGenerator
import java.security.SecureRandom
import java.security.spec.ECGenParameterSpec
import com.liferay.object.service.ObjectEntryLocalServiceUtil
import com.liferay.portal.kernel.service.ServiceContext
def wallet = new Wallet()
def userId = Long.valueOf(creator)
def entryValues = [privateKey: wallet.privateKey, publicKey: wallet.publicKey]
ObjectEntryLocalServiceUtil.updateObjectEntry(userId, id, entryValues, new ServiceContext())
class Wallet {
def KeyPair keyPair
Wallet() {
this.keyPair = this.generateKeyPair()
}
String getPrivateKey() {
return Base64.encoder.encodeToString(keyPair.getPrivate().getEncoded())
}
String getPublicKey() {
return Base64.encoder.encodeToString(keyPair.getPublic().getEncoded())
}
private KeyPair generateKeyPair() {
def keyGen = KeyPairGenerator.getInstance("EC")
def ecSpec = new ECGenParameterSpec("secp256r1")
keyGen.initialize(ecSpec, new SecureRandom())
return keyGen.generateKeyPair()
}
}
</pre>
<p>Copy the code and add it to the script field, and then after
saving, you will notice that the new Action has been included in the
Wallet object.</p>
<p> <img data-fileentryid="122411440"
src="https://liferay.dev/documents/portlet_file_entry/14/image-20.png/2ec9e279-4d61-22dc-1c7d-92fcb77b08dc">
<br> </p>
<p>Next, go to the Details tab and change the following fields:</p>
<ul> <li>Entry Title Field: Name</li> <li>Panel Link: Custom
Apps</li> </ul>
<p>Disable the "Enable Categorization of Object entries"
option, and then click the "Publish" button. This will
publish the object. Note that Liferay already creates a default view
of the object in the Applications -> Custom Apps menu option, as
selected in the Details tab of the object.</p>
<p> <img data-fileentryid="122411449"
src="https://liferay.dev/documents/portlet_file_entry/14/image-21.png/c94a30b5-0ae4-b12b-7247-0376ea20abac">
<br> <br> Upon entering this option, click the "Add Wallet" button.</p>
<p>Fill in a name, leave the Private Key and Public Key fields
empty, and click the "Save" button.</p>
<p> <img data-fileentryid="122411476"
src="https://liferay.dev/documents/portlet_file_entry/14/image-24.png/9c7d6db7-866d-9c9a-42d4-2f0d8d1ca329">
<br> </p>
<p>Note that when adding a new record, the Groovy script code is
triggered, generating the public and private keys for the Wallet.</p>
<p>Now let's add the wallets widget so that site users can create
their own wallets. For this, go to the main site of XChangeRay and add
a new page called "My Wallets" through the Site Builder
-> Pages menu.</p>
<p> <img data-fileentryid="122411487"
src="https://liferay.dev/documents/portlet_file_entry/14/image-25.png/0997d428-6b91-9c8f-d11b-87755dad4094">
<br> <br> <img data-fileentryid="122411496"
src="https://liferay.dev/documents/portlet_file_entry/14/image-26.png/4b5889c2-e066-f0eb-1df6-2b70c7088c2c">
<br> </p>
<p>Next, in the page editing screen, select the "Widgets"
tab from the left sidebar, go to the "Objects" category, and
add the Wallets widget to the page.</p>
<p> <img data-fileentryid="122411505"
src="https://liferay.dev/documents/portlet_file_entry/14/image-27.png/8ff32e87-543c-f789-04ba-995e1ff34dbd">
<br> </p>
<p>Now, to make the page visible to site users, go to the
"Permissions" option of the "My Wallets" page and
remove the "View" permission for "Guest" users,
then add the "View" permission for "User".</p>
<p>This way, only registered site users can view this page. Now, it
is still necessary to add permission for authenticated users to
create their own wallets. For this, go to the Control Panel ->
Roles menu, select the "Users" option. Next, select the
"Define Permissions" tab, search for "Wallets",
and select the "View" permission under the
"Application Permissions" category and "Add Object
Entry" under "Resource Permissions", then click "Save".</p>
<p> <img data-fileentryid="122411542"
src="https://liferay.dev/documents/portlet_file_entry/14/image-31.png/7ed0ee2e-d6a3-b060-3419-c34954662451">
<br> </p>
<p>When creating digital wallets in Liferay, the platform
automatically links each wallet to its corresponding user, ensuring
that only the owner can view and manage their wallets. This access
control is an integrated functionality of Liferay's Objects,
significantly simplifying permission management.</p>
<p>Try creating a new user and access the "My Wallets"
page to add digital wallets. You will observe that, when creating a
new wallet, it is exclusively associated with the creating user,
reinforcing security and privacy. If a second user is added to the
system, they will not have visibility or access to the first user's
wallets. Thus, each user maintains complete control over their own
wallets, being able to create, edit, and remove records as needed,
without interference or visibility from other users.</p>
<p>This method of direct association between users and their
digital wallets highlights Liferay's approach to data security and
customizing the user experience, facilitating the implementation of
complex functionalities intuitively and securely.<br> </p>
<p> <img data-fileentryid="122411560"
src="https://liferay.dev/documents/portlet_file_entry/14/image-33.png/ffe77142-038f-98ec-ac6e-2600952f4de8"> </p>
<p> </p>
<h3> <img data-fileentryid="122411579"
src="https://liferay.dev/documents/portlet_file_entry/14/image-35.png/a2c736ab-f112-9767-ebae-acc3fbb192ed">
<br> <br> Transaction Object</h3>
<p>After creating the Wallet object, the next step is to establish
the Transaction object, which consolidates the execution of
transactions of RayCoin units from one wallet to another. This
process will allow the user to select one of their wallets for debit
and specify the recipient wallet's address for credit. To facilitate
this operation, we will create a new object named
"Transaction". It is important to note that the recipient
wallet may not belong to the user conducting the transaction.</p>
<p>To add a layer of security and authentication to the
transactions, we will implement a Groovy script within the
"Actions" tab of the Transaction object. This script will
be responsible for digitally signing the transaction using the
private key of the wallet selected by the user. After the signature,
the transaction will be sent to another Liferay instance via the
Restful API, ensuring the integrity and truthfulness of the operation.</p>
<p>Therefore, select the option to create a new object called
Transaction. Add the following custom fields to this object:</p>
<table border="1" cellpadding="0" cellspacing="0"
style="width: 100.0%;"> <thead> <tr> <th scope="col">Label</th>
<th scope="col">Field Name</th> <th scope="col">Type</th>
<th scope="col">Mandatory</th> </tr> </thead> <tbody> <tr>
<td>To Address</td> <td>toAddress</td> <td>Long Text</td>
<td>Yes</td> </tr> <tr> <td>Amount</td> <td>amount</td>
<td>Precision Decimal</td> <td>Yes</td> </tr> <tr>
<td>Signature</td> <td>signature</td> <td>Long Text</td>
<td>No</td> </tr> </tbody> </table>
<p> </p>
<p>Next, we will link the Transaction object to the Wallet object,
so that the user can select from which wallet they wish to have
their RayCoins debited. Select the "Relationships" tab,
then "Add Object Relationship," and fill in with the
following information:</p>
<ul> <li>Label: Wallet</li> <li>Name: wallet</li> <li>Type: One
to Many</li> <li>One Record Of: Wallet</li> <li>Many Records Of:
Transaction</li> </ul>
<p> <img data-fileentryid="122411603"
src="https://liferay.dev/documents/portlet_file_entry/14/image-37.png/258fcafe-e116-3067-4da9-61954f230223">
<br> </p>
<p>Note that upon saving, a new field named Wallet will appear in
the "Fields" tab. Edit this field to set the
"Mandatory" option to true, which will require the user to
select a source wallet when creating a new transaction.</p>
<p> <img data-fileentryid="122411622"
src="https://liferay.dev/documents/portlet_file_entry/14/image-39.png/8fccf239-092e-1695-9832-25232b63a06a">
<br> </p>
<p>Next, select the "Actions" tab and add a new action
named "Process Transaction," select the trigger type
"On After Add" and action type "Groovy Script"
as was done during the creation of the Wallet object. In this
script, we first create a Wallet class that retrieves a wallet from
the "walletId" parameter and stores the public and private
keys, as below:</p>
<pre>
class Wallet {
def privateKey
def publicKey
Wallet(walletId) {
def objEntry = ObjectEntryLocalServiceUtil.getObjectEntry(walletId)
this.privateKey = objEntry.getValues().get("privateKey")
this.publicKey = objEntry.getValues().get("publicKey")
}
String getPrivateKey() {
return privateKey
}
String getPublicKey() {
return publicKey
}
}
</pre>
<p>Next, we will create a class named "Transaction," as below:</p>
<pre>
class Transaction {
def toAddress
def amount
Wallet wallet
def transactionData
def signature
Transaction(toAddress, amount, wallet) {
this.toAddress = toAddress
this.amount = amount
this.wallet = wallet
this.transactionData = "${this.wallet.publicKey}${toAddress}${amount}"
}
String getSignature() {
return signature
}
void sign() {
PrivateKey privateKey = stringToPrivateKey(wallet.privateKey)
byte[] signedMessage = sign(transactionData, privateKey)
signature = Base64.getEncoder().encodeToString(signedMessage)
}
byte[] sign(String data, PrivateKey privateKey) {
def signatureInstance = Signature.getInstance("SHA256withECDSA")
signatureInstance.initSign(privateKey)
signatureInstance.update(data.bytes)
return signatureInstance.sign()
}
PrivateKey stringToPrivateKey(String privateKeyAsString) throws NoSuchAlgorithmException, InvalidKeySpecException {
byte[] privateKeyBytes = Base64.decoder.decode(privateKeyAsString)
KeyFactory kf = KeyFactory.getInstance("EC")
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyBytes)
return kf.generatePrivate(keySpec)
}
}
</pre>
<p> </p>
<p>This class is designed to encapsulate the details of a
transaction, receiving in its constructor essential parameters for
executing a RayCoins transfer:</p>
<ul> <li> <strong>toAddress</strong>: The destination address
where the RayCoins will be credited.</li> <li>
<strong>amount</strong>: The quantity of RayCoins to be
transferred.</li> <li> <strong>wallet</strong>: The source wallet
from where the RayCoins will be debited.</li> </ul>
<p>The sign() method is responsible for implementing the necessary
logic to sign the transaction. It uses the private key of the source
wallet, based on the transactionData, which compiles the essential
data of the transaction: the source address, destination address, and
the amount to be transferred. This approach ensures the authenticity
of the transaction by encrypting the data with the private key of the
source wallet.</p>
<p>After generating the signature, it's crucial to incorporate it
into the previously retrieved Transaction object:</p>
<pre>
// Create Wallet and Transaction objects
Wallet wallet = new Wallet(r_wallet_c_walletId)
Transaction transaction = new Transaction(toAddress, amount, wallet)
// Sign the transaction
transaction.sign()
// Update the object entry with the new signature
def userId = Long.valueOf(creator)
ObjectEntryLocalServiceUtil.updateObjectEntry(userId, id, [signature: transaction.signature], new ServiceContext())
</pre>
<p> <br> Note that there are variables corresponding to the
Object fields that are made available in the context of the
script, such as r_wallet_c_walletId, which corresponds to the
identifier of the Wallet object selected by the user for the
transaction creation. This field is used by the Wallet class to
retrieve the related record and then retrieve the privateKey that
will be used in generating the signature. This signature will also
be validated by the raycoin instance when it is sent. At the end of
the algorithm, the ObjectEntryLocalServiceUtil class is used to
store the signature in the corresponding field of the created object.</p>
<p>Finally, the script would be as follows:</p>
<pre>
import java.security.KeyFactory
import java.security.NoSuchAlgorithmException
import java.security.PrivateKey
import java.security.Signature
import java.security.spec.InvalidKeySpecException
import java.security.spec.PKCS8EncodedKeySpec
import javax.json.Json
import javax.json.JsonObject
import org.apache.http.HttpEntity
import org.apache.http.HttpResponse
import org.apache.http.client.HttpClient
import org.apache.http.client.methods.HttpPost
import org.apache.http.entity.StringEntity
import org.apache.http.impl.client.HttpClientBuilder
import org.apache.http.util.EntityUtils
import com.liferay.object.service.ObjectEntryLocalServiceUtil
import com.liferay.portal.kernel.service.ServiceContext
Wallet wallet = new Wallet(r_wallet_c_walletId)
Transaction transaction = new Transaction(toAddress, amount, wallet)
transaction.sign()
def userId = Long.valueOf(creator)
ObjectEntryLocalServiceUtil.updateObjectEntry(userId, id, [signature: transaction.signature], new ServiceContext())
class Transaction {
def toAddress
def amount
Wallet wallet
def transactionData
def signature
Transaction(toAddress, amount, wallet) {
this.toAddress = toAddress
this.amount = amount
this.wallet = wallet
this.transactionData = "${this.wallet.publicKey}${toAddress}${amount}"
}
String getSignature() {
return signature
}
void sign() {
PrivateKey privateKey = stringToPrivateKey(wallet.privateKey)
byte[] signedMessage = sign(transactionData, privateKey)
signature = Base64.getEncoder().encodeToString(signedMessage)
}
byte[] sign(String data, PrivateKey privateKey) {
def signatureInstance = Signature.getInstance("SHA256withECDSA")
signatureInstance.initSign(privateKey)
signatureInstance.update(data.bytes)
return signatureInstance.sign()
}
PrivateKey stringToPrivateKey(String privateKeyAsString) throws NoSuchAlgorithmException, InvalidKeySpecException {
byte[] privateKeyBytes = Base64.decoder.decode(privateKeyAsString)
KeyFactory kf = KeyFactory.getInstance("EC")
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyBytes)
return kf.generatePrivate(keySpec)
}
}
class Wallet {
def privateKey
def publicKey
Wallet(walletId) {
def objEntry = ObjectEntryLocalServiceUtil.getObjectEntry(walletId)
this.privateKey = objEntry.getValues().get("privateKey")
this.publicKey = objEntry.getValues().get("publicKey")
}
String getPrivateKey() {
return privateKey
}
String getPublicKey() {
return publicKey
}
}
</pre>
<p>After saving the script, select the Details tab, choose
"Custom Apps" in the "Panel Link" option, disable
the "Enable Categorization of Object entries" option, and
then publish the object. Note that the Transactions option will now be
available in the Applications -> Custom Apps menu.</p>
<p> <img data-fileentryid="122415388"
src="https://liferay.dev/documents/portlet_file_entry/14/image-40.png/ff5c2c67-152b-f56a-9f79-ac83b6cde762">
<br> </p>
<p>Add a new Transaction and see that the Signature field will be
automatically generated.</p>
<p> <img data-fileentryid="122415397"
src="https://liferay.dev/documents/portlet_file_entry/14/image-42.png/af21a457-027d-7eaf-5f74-bf98ee609190">
<br> </p>
<p>Similar to the process of creating the Wallet object, it is
necessary to add a new page called "My Transactions" on the
XChangeRay site. This step includes configuring appropriate
permissions for the User Role, allowing the addition of new entries.
Subsequently, the Transactions widget should be incorporated into the
new page, analogous to what was done on the "My Wallets"
page. This way, users will be able to enter their transactions directly.</p>
<p> <img data-fileentryid="122415407"
src="https://liferay.dev/documents/portlet_file_entry/14/image-43.png/2efbcc2e-4b52-1a50-a26b-f0ffeb6f60a2">
<br> Up to now, transactions created on the XChangeRay instance are
not sent to the Blockchain (RayCoin) instance, due to the absence of
the necessary objects in the other instance. However, it is possible
to anticipate and prepare the code for sending transactions. This is
done by editing the Groovy script of the Transactions object,
incorporating the sendToBlockchain(postUrl) method into the
Transaction class:</p>
<pre>
class Transaction {
. . .
void sendToBlockchain(postUrl) {
JsonObject jsonParams = Json.createObjectBuilder()
.add("fromAddress", this.wallet.publicKey)
.add("toAddress", toAddress)
.add("amount", amount)
.add("signature", signature)
.add("signatureValid", true)
.add("transactionData", transactionData)
.add("transactionStatus", "pending")
.build()
String json = jsonParams.toString()
HttpPost httpPost = new HttpPost(postUrl)
httpPost.setHeader('Content-Type', 'application/json')
httpPost.setEntity(new StringEntity(json))
HttpClient httpClient = HttpClientBuilder.create().build()
httpClient.execute(httpPost)
}
}
</pre>
<p>This method performs a POST request to the specified endpoint in
"postUrl," which will correspond to the Transaction object's
endpoint in the RayCoin instance. This endpoint, which will be
detailed in subsequent sections of this article, is anticipated as:
"http://raycoin.local:8080/o/c/transactions/". Therefore, at
the beginning of the script, include the call to the sendToBlockchain
method, passing the URL of the endpoint:</p>
<pre>
transaction.sendToBlockchain("http://raycoin.local:8080/o/c/transactions/")
</pre>
<p>Thus, it would be as follows:</p>
<pre>
Wallet wallet = new Wallet(r_wallet_c_walletId)
Transaction transaction = new Transaction(toAddress, amount, wallet)
transaction.sign()
def userId = Long.valueOf(creator)
ObjectEntryLocalServiceUtil.updateObjectEntry(userId, id, [signature: transaction.signature], new ServiceContext())
transaction.sendToBlockchain("http://raycoin.local:8080/o/c/transactions/")
</pre>
<p>The complete final version of the script can be found at the
following link: <a href="https://github.com/andrefabbro/blockchain-demo/blob/main/src/main/java/com/liferay/xchangeray/objects/transaction/AddTransaction.groovy">https://github.com/andrefabbro/blockchain-demo/blob/main/src/main/java/com/liferay/xchangeray/objects/transaction/AddTransaction.groovy</a></p>
<h2>RayCoin Instance</h2>
<h3>Creating Objects</h3>
<p>After setting up the XChangeRay instance with its objects, it's
time to turn our attention to the main instance, RayCoin, to configure
the objects that will form the backbone of our business logic. Access
the URL http://raycoin.local:8080/ and log in as an administrator. In
the Control Panel, find and select the "Objects" option to
start creating the necessary objects for this instance. The objects
and their fields should be configured as follows:</p>
<p> <strong>Block</strong>:</p>
<table border="1" cellpadding="2" cellspacing="2"
style="width: 100.0%;"> <thead> <tr> <th scope="col">Label</th>
<th scope="col">Field Name</th> <th scope="col">Type</th>
<th scope="col">Mandatory</th> </tr> </thead> <tbody> <tr>
<td>Index</td> <td>index</td> <td>Integer</td>
<td>Yes</td> </tr> <tr> <td>Previous Hash</td>
<td>previousHash</td> <td>Long Text</td> <td>Yes</td>
</tr> <tr> <td>Nounce</td> <td>nounce</td>
<td>Integer</td> <td>Yes</td> </tr> </tbody> </table>
<p> </p>
<p> <strong>Transaction</strong>:</p>
<table border="1" cellpadding="2" cellspacing="2"
style="width: 100.0%;"> <thead> <tr> <th scope="col">Label</th>
<th scope="col">Field Name</th> <th scope="col">Type</th>
<th scope="col">Mandatory</th> </tr> </thead> <tbody> <tr>
<td>From Address</td> <td>fromAddress</td> <td>Long
Text</td> <td>Yes</td> </tr> <tr> <td>To Address</td>
<td>toAddress</td> <td>Long Text</td> <td>Yes</td> </tr>
<tr> <td>Amount</td> <td>amount</td> <td>Precision
Decimal</td> <td>Yes</td> </tr> <tr> <td>Signature</td>
<td>signature</td> <td>Long Text</td> <td>Yes</td> </tr>
<tr> <td>Signature Valid</td> <td>signatureValid</td>
<td>Boolean</td> <td>No</td> </tr> </tbody> </table>
<p> </p>
<p> <strong>Wallet Balance</strong>:</p>
<table border="1" cellpadding="2" cellspacing="2"
style="width: 100.0%;"> <thead> <tr> <th scope="col">Label</th>
<th scope="col">Field Name</th> <th scope="col">Type</th>
<th scope="col">Mandatory</th> </tr> </thead> <tbody> <tr>
<td>Address</td> <td>address</td> <td>Long Text</td>
<td>Yes</td> </tr> <tr> <td>Balance</td>
<td>balance</td> <td>Precision Decimal</td> <td>Yes</td>
</tr> </tbody> </table>
<p> </p>
<p> <strong>Blockchain</strong>:</p>
<table border="1" cellpadding="2" cellspacing="2"
style="width: 100.0%;"> <thead> <tr> <th scope="col">Label</th>
<th scope="col">Field Name</th> <th scope="col">Type</th>
<th scope="col">Mandatory</th> </tr> </thead> <tbody> <tr>
<td>Name</td> <td>name</td> <td>Text</td> <td>Yes</td>
</tr> <tr> <td>Max Pending Transactions</td>
<td>maxPendingTransactions</td> <td>Integer</td>
<td>Yes</td> </tr> <tr> <td>Reward Address</td>
<td>rewardAddress</td> <td>Long Text</td> <td>Yes</td>
</tr> <tr> <td>Reward Value</td> <td>rewardValue</td>
<td>Precision Decimal</td> <td>Yes</td> </tr> <tr>
<td>Authorization</td> <td>authorization</td> <td>Long
Text</td> <td>Yes</td> </tr> <tr> <td>Blockchain
URL</td> <td>BlockchainURL</td> <td>Text</td>
<td>Yes</td> </tr> </tbody> </table>
<p> </p>
<p>For each object created, access the "Details" tab,
adjust the "Custom Apps" value in the "Panel
Link" option, disable "Enable Categorization of Object
entries," and finally, publish the object by clicking on "Publish".</p>
<h3>Relationships</h3>
<p>After establishing the objects, it's necessary to configure the
relationships between them, simulating the links of a database. In
Liferay, these relationships can be many-to-many or one-to-many. For
our application, a Blockchain object will contain multiple Blocks, and
each Block, in turn, will house multiple Transactions. To configure
these relationships, edit the Transaction object, select the
"Relationships" tab, and add a new relationship with the information:</p>
<ul> <li>Label: Block</li> <li>Name: block</li> <li>Type: One to
Many</li> <li>One Record Of: Block</li> <li>Many Records Of:
Transaction</li> </ul>
<p> <img data-fileentryid="122415449"
src="https://liferay.dev/documents/portlet_file_entry/14/image-44.png/0f008969-1801-27a1-1217-94d0183c6afe">
<br> </p>
<p>Go back to the "Fields" tab and note that a new field
named "Block" has been created, with the type
"Relationship". As transactions initially do not belong to
any block when they are recorded, this field does not need to be mandatory.</p>
<p>Now go to the Block object's editing screen, navigate to the
"Relationships" tab, and you will see that the relationship
created in the Transaction object also appears on this screen.</p>
<p>Click the "+" button to then include the relationship
between the Block and the Blockchain, fill in the following information:</p>
<ul> <li>Label: Blockchain</li> <li>Name: blockchain</li>
<li>Type: One to Many</li> <li>One Record Of: Blockchain</li>
<li>Many Records Of: Block</li> </ul>
<p>Now in the "Fields" tab, see the
"Relationship" type field named Blockchain, change this
field to make it mandatory by marking the "Mandatory" field.</p>
<p> <img data-fileentryid="122415458"
src="https://liferay.dev/documents/portlet_file_entry/14/image-47.png/9fd17858-a96e-8087-00f3-13a7c9b969ee">
<br> </p>
<h3>Access Permissions</h3>
<p>Finally, it is crucial to set access permissions to determine
which objects will be public for reading and/or writing. This
setting is vital, especially to allow external systems, such as
XChangeRay, to interact with the blockchain by creating Transactions
and querying Blocks and Wallet Balances. Access the "Control
Panel" menu, go to "Roles," select "Guest"
under "Regular Roles," and in the "Define
Permissions" tab, search for the term "Transactions,"
and check the "Add Object Entry" option in the
"TRANSACTIONS" category and "View" in the
"TRANSACTION" category, both under "RESOURCE PERMISSIONS".</p>
<p> <img data-fileentryid="122415468"
src="https://liferay.dev/documents/portlet_file_entry/14/image-48.png/169ea33f-11e6-6cc7-52e2-1f83fea15452">
<br> </p>
<p>After saving, search for the term "Blocks" and select
only the "View" option in the "BLOCK" category
also under "RESOURCE PERMISSIONS".</p>
<p> <img data-fileentryid="122415577"
src="https://liferay.dev/documents/portlet_file_entry/14/image-49.png/02b580d7-c27c-35bc-fdf8-d8e73202ca86">
<br> </p>
<p>Next, do the same procedure for "Wallets Balances,"
marking only the "View" option.</p>
<p> <img data-fileentryid="122415587"
src="https://liferay.dev/documents/portlet_file_entry/14/image-50.png/39a1ef55-6658-7e35-ba5d-d297cf1b106f">
<br> </p>
<h3>Initial Objects Data</h3>
<p>After creating the objects, their relationships, and access
control through the permission system, we have obtained the
fundamental structure of the Blockchain. Let's now create the first
records of these Objects, which will serve as the basis for the next
data that will be received by the API.</p>
<p>Navigate to "Applications" and choose "Custom
Apps," select "Blockchains," and use the "+"
button to add a new record. Fill in the information as follows:</p>
<ul> <li> <strong>Authorization</strong>: Use for
authentication in calls to the Liferay RestFul API for
non-public methods. For example, to check the need for
mining after a transaction. Use "Basic"
authentication by converting the username and password to
base64. For example, for the user 'test@liferay.com' with
password 'test', the value will be "<code>Basic
dGVzdEBsaWZlcmF5LmNvbTp0ZXN0</code>".</li> <li>
<strong>BlockchainURL</strong>: Enter the endpoint of the RayCoin
instance, such as "http://raycoin.local:8080".</li> <li>
<strong>Max Pending Transactions</strong>: Define the number of
pending transactions required to start the mining process, for
example, "3".</li> <li> <strong>Reward
Address</strong>: Use the public key of a wallet generated for
the miner, for example, the public key can be
"<code>MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEKutm5pE/nTelGk9p2mk9HpULdy3ID2tWTx/2O+D65dsAzA1MQNSdfF6RwGnCZfik++V6trwdCPTmomz0rWD73g==</code>".</li>
<li> <strong>Reward Value</strong>: Set the mining reward value,
such as "10".</li> </ul>
<p> <img data-fileentryid="122415596"
src="https://liferay.dev/documents/portlet_file_entry/14/image-51.png/45349fb9-84eb-6154-5a36-ec4f2ebcc8ee">
<br> </p>
<p>After saving this record, proceed to the "Blocks"
object and add the initial block, the genesis block, with the
following information:</p>
<ul> <li>Hash: Use "0" for the initial hash.</li>
<li>Index: Position "0" for the first block.</li>
<li>Nonce: Value "0", indicative of being the
genesis.</li> <li>Previous Hash: Enter "none," since it is
the first block.</li> <li>Blockchain: Select the previously created
"RayCoin" option.</li> </ul>
<p> <img data-fileentryid="122415605"
src="https://liferay.dev/documents/portlet_file_entry/14/image-52.png/038d0602-e0cc-8193-f79d-3c7ec4583012">
<br> </p>
<p>Having done that, let's now create a record for
"Transaction," representing the initial transaction with an
allocation of 500,000 RayCoins to a specific wallet, let's use the
following wallet in this example:</p>
<ul> <li>Name: XChangeRay Wallet</li> <li>Private Key:
<code>MEECAQAwEwYHKoZIzj0CAQYIKoZIzj0DAQcEJzAlAgEBBCC6yL3me8HgFbKaTdx83TsDab8EptOc1qTV8xMZvfVktQ==</code>
</li> <li>Public Key:
<code>MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8QK/c9ZSIcVMN8HYfPcZ2cW/CXfN7c8hqFdazgE1lsFmes8UeIVovMgivL2qyeZAOuECrE8eg7HixuYcwhNFOw==</code>
</li> </ul>
<p>For this, create a new record in "Transactions" with
the following attributes:</p>
<ul> <li>Amount: 500000</li> <li>From Address: none</li>
<li>Block: 0</li> <li>Signature: 0</li> <li>To Address:
<code>MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8QK/c9ZSIcVMN8HYfPcZ2cW/CXfN7c8hqFdazgE1lsFmes8UeIVovMgivL2qyeZAOuECrE8eg7HixuYcwhNFOw==</code>
</li> </ul>
<p> <img data-fileentryid="122415614"
src="https://liferay.dev/documents/portlet_file_entry/14/image-54.png/79d88bd6-f2c1-75e3-9789-08bd77243faa">
<br> </p>
<h3>Object Actions</h3>
<p>After establishing the fundamental structure and initial
records, we are ready to integrate essential business logic through
scripts associated with the "Actions" functionality of the
objects. For each type of object involved, we will develop four
scripts, detailed below:</p>
<p>Blockchain Object:</p>
<ul> <li> <strong>Mine Pending Transactions</strong>: This
action, configured as standalone in the Blockchain object,
diverges from actions linked to the object's lifecycle, such as
the one implemented for the Wallet object in XChangeRay.
Instead, this action becomes available for activation via API.
Its primary function is to check if the number of pending
transactions is sufficient to initiate a mining process. If
affirmative, it proceeds to mine a new block that, once
validated, is added to the blockchain.</li> <li>
<strong>Compute Balances</strong>: Also a standalone action in the
Blockchain object, aimed at updating the Wallet Balance object
records. It calculates the balance of each wallet, aggregating this
data to facilitate future balance queries.</li> </ul>
<p>Transaction Object:</p>
<ul> <li> <strong>Validate Transaction</strong>: This action
is triggered when a new Transaction is created. Its purpose
is to validate the transaction's signature and verify if the
originating wallet has sufficient funds to carry out the
transaction.</li> <li> <strong>Trigger Blockchain</strong>: This
action, also triggered upon the creation of a new Transaction,
aims to activate the Mine Pending Transactions method of the
Blockchain object. This method assesses whether it is the
appropriate moment to start the mining process.</li> </ul>
<h3>Validate Transaction</h3>
<p>To begin implementing the "Validate Transaction"
functionality, go to the "Objects" section in the Control
Panel, navigate to "Object -> Objects," select the
"Transaction" object and proceed to the "Actions"
tab. Add a new action named "Validate Transaction." In the
"Action Builder" interface, choose "On After Add"
in "Trigger" and select "Groovy Script" in
"Action" to start drafting the script.</p>
<p>Initially, we will create a class called TransactionValidator,
incorporating attributes that mirror those of the
"Transaction" object, in addition to support attributes. The
initial structure of the class will be:</p>
<pre>
class TransactionValidator {
long companyId
String fromAddress
String transactionData
String signature
BigDecimal amount
TransactionValidator(companyId, amount, fromAddress, transactionData, signature) {
this.companyId = companyId
this.amount = amount
this.fromAddress = fromAddress
this.transactionData = transactionData
this.signature = signature
}
</pre>
<p>Next, we will develop a method within this class to validate the
signature associated with the object, using classes from the
java.security package, available in the JVM. This process is similar
to the one used for generating public and private keys and signing in
the XChangeRay instance. However, in this context, we focus on validation:</p>
<pre>
void validateSignature() throws Exception {
if (this.fromAddress == 'none') {
return
}
byte[] signatureBytes = Base64.getDecoder().decode(this.signature)
if (signatureBytes == null || signatureBytes.length == 0) {
throw new RuntimeException("No signature found in this transaction")
}
def keyFactory = KeyFactory.getInstance("EC")
def publicKeySpec = new X509EncodedKeySpec(Base64.decoder.decode(this.fromAddress))
def publicKey = keyFactory.generatePublic(publicKeySpec)
def signatureInstance = Signature.getInstance("SHA256withECDSA")
signatureInstance.initVerify(publicKey)
signatureInstance.update(this.transactionData.bytes)
if(!signatureInstance.verify(signatureBytes)) {
throw new Exception("Signature validation failed")
}
}
</pre>
<p>Then, we will develop a method to verify if the originating
wallet has a sufficient balance for the transaction. This process
requires querying the "Wallet Balance" object to access
the balances processed by the blockchain. We will use the
"Blockchain" object identifier to find the appropriate
address for the HTTP call, retrieving the URL of the RayCoin
instance and completing it with the specific endpoint for the
"Wallet Balances" API.</p>
<p>First, it's necessary to access the information contained in the
"Blockchain" object, which stores the RayCoin instance URL.
This is done through the ObjectDefinitionLocalServiceUtil class,
allowing the retrieval of the "Blockchain" object definition
identifier with the following code:</p>
<pre>
def blockChainObjDef = ObjectDefinitionLocalServiceUtil.fetchObjectDefinition(companyId, "C_Blockchain")
</pre>
<p>With this identifier in hand, we access all records of the
"Blockchain" type and select the first one, considering the
existence of only one record of this type:</p>
<pre>
def blockChainObj = ObjectEntryLocalServiceUtil.getObjectEntries(0, blockChainObjDef.objectDefinitionId, 0, 1).get(0)
</pre>
<p>From this object, we extract the value of the
"blockchainURL" field, which provides the prefix of the
address needed to perform HTTP calls:</p>
<pre>
def blockchainUrl = blockChainObj.values.get("blockchainURL")
</pre>
<p>However, this value represents only the beginning of the
endpoint. To complete it, it's necessary to add the specific path of
the API that manages the "Wallet Balance" objects,
resulting in /o/c/walletbalances. To discover the available
endpoints for each object created on the Liferay platform, the
platform offers an API catalog accessible at /o/api/. By visiting
http://raycoin.local:8080/o/api/, it's possible to explore and select
the desired object, thus obtaining detailed information about the
available endpoints.</p>
<p> <img data-fileentryid="122415624"
src="https://liferay.dev/documents/portlet_file_entry/14/image-55.png/a692c046-e224-11ed-39d4-7069a9b751a5">
<br> </p>
<p>This Liferay feature is extremely powerful as it allows the
platform to be manipulated in a consistent and flexible manner,
enabling both read and write operations, and also allowing the
application of filters, sorting, searching, among other features. With
this, developers can create remote applications in any technology and
use the API as part of their business rules.</p>
<p>Therefore, to validate the balance, the script should contain a
GET call to the "Wallet Balance" object endpoint, using a
filter by the "Address" field to identify the balance of
the originating wallet. The result of this call is processed to
verify the availability of sufficient funds for the transaction:</p>
<pre>
def encodedFilter = URLEncoder.encode("address eq '${fromAddress}'", "UTF-8")
def getUrl = "${blockchainUrl}/o/c/walletbalances/?filter=${encodedFilter}"
HttpGet httpGet = new HttpGet(getUrl)
httpGet.setHeader('Content-Type', 'application/json')
HttpClient httpClient = HttpClientBuilder.create().build()
HttpResponse getResponse = httpClient.execute(httpGet)
String getResponseBody = EntityUtils.toString(getResponse.getEntity())
JsonObject getResponseJson = Json.createReader(new StringReader(getResponseBody)).readObject()
</pre>
<p>After the execution of the call, if the wallet does not have
sufficient funds, an exception will be thrown, indicating insufficient
balance. This process ensures that only valid transactions are
processed on the blockchain:</p>
<pre>
JsonArray walletBalancesJsonArray = getResponseJson.getJsonArray("items")
if(walletBalancesJsonArray.empty) {
throw new Exception("Wallet doesn't have enough funds")
} else {
JsonObject walletBalance = walletBalancesJsonArray.getJsonObject(0)
def balance = new BigDecimal(walletBalance.getJsonNumber("balance").toString())
if(amount > balance) {
throw new Exception("Wallet doesn't have enough funds")
}
}
</pre>
<p>Thus, the complete method should be as follows:</p>
<pre>
void validateBalance() {
def blockChainObjDef = ObjectDefinitionLocalServiceUtil.fetchObjectDefinition(companyId, "C_Blockchain")
def blockChainObj = ObjectEntryLocalServiceUtil.getObjectEntries(0, blockChainObjDef.objectDefinitionId, 0, 1).get(0)
def blockchainUrl = blockChainObj.values.get("blockchainURL")
def encodedFilter = URLEncoder.encode("address eq '${fromAddress}'", "UTF-8")
def getUrl = "${blockchainUrl}/o/c/walletbalances/?filter=${encodedFilter}"
HttpGet httpGet = new HttpGet(getUrl)
httpGet.setHeader('Content-Type', 'application/json')
HttpClient httpClient = HttpClientBuilder.create().build()
HttpResponse getResponse = httpClient.execute(httpGet)
String getResponseBody = EntityUtils.toString(getResponse.getEntity())
JsonObject getResponseJson = Json.createReader(new StringReader(getResponseBody)).readObject()
JsonArray walletBalancesJsonArray = getResponseJson.getJsonArray("items")
if(walletBalancesJsonArray.empty) {
throw new Exception("Wallet doesn't have enough funds")
} else {
JsonObject walletBalance = walletBalancesJsonArray.getJsonObject(0)
def balance = new BigDecimal(walletBalance.getJsonNumber("balance").toString())
if(amount > balance) {
throw new Exception("Wallet doesn't have enough funds")
}
}
}
</pre>
<p>It is now also necessary to create a method to update the
transaction status. We will use the "status" field, a
standard attribute in all object definitions in Liferay. It's
important to recognize that changing the status should occur after the
record creation, as by default, all objects are initially marked as
"APPROVED" in the absence of a defined workflow. To simplify
and avoid the complexity of introducing a workflow process, we opt to
modify the status asynchronously, with a delay of three seconds after
including the record in the system. The implementation of this method
is described below:</p>
<pre>
void updateTransactionStatus(userId, id, status) {
Thread.start {
sleep(3000) // Sleep for 3 seconds
ObjectEntryLocalServiceUtil.updateStatus(userId, id, status, new ServiceContext())
}
}
</pre>
<p>With the TransactionValidator class now complete with all
necessary methods, we can proceed with the transaction validation
logic as follows:</p>
<ul> <li>Transaction Origin Validation: Initially, we check if the
source address is 'none', which would indicate a reward transaction,
exempting it from validations.</li> <li>Creation of the
TransactionValidator Object: Next, we instantiate the
TransactionValidator object, filling its essential attributes with
the transaction data.</li> <li>Validation Process: Within a
try-catch block, we perform the signature validation and wallet
balance check. If both validations are successful, we update the
transaction status to 'PENDING'. If any validation fails, the status
is changed to 'DENIED'.</li> </ul>
<pre>
try {
// first, set the signatureValid to false
ObjectEntryLocalServiceUtil.updateObjectEntry(userId, id, [signatureValid: false], new ServiceContext())
// then, validate the signature
transactionValidator.validateSignature()
// set the signatureValid to true
ObjectEntryLocalServiceUtil.updateObjectEntry(userId, id, [signatureValid: true], new ServiceContext())
// validate if the wallet has enough funds for this transaction
transactionValidator.validateBalance()
// set the transaction status to pending
transactionValidator.updateTransactionStatus(userId, id, WorkflowConstants.STATUS_PENDING)
} catch (Exception e) {
// as the signature wasn't validated or the wallet doesn't have funds, set the status to denied
transactionValidator.updateTransactionStatus(userId, id, WorkflowConstants.STATUS_DENIED)
}
</pre>
<p>The complete code of the script can be found at the following
address: <a href="https://github.com/andrefabbro/blockchain-demo/blob/main/src/main/java/com/liferay/raycoin/objects/transaction/ValidateTransaction.groovy">https://github.com/andrefabbro/blockchain-demo/blob/main/src/main/java/com/liferay/raycoin/objects/transaction/ValidateTransaction.groovy</a></p>
<h3>Trigger Blockchain</h3>
<p>Within the same "Transaction" object, add a new Action
titled "Trigger Blockchain," setting it up with the type
"On After Add" and selecting "Groovy Script" as
the Action type. This Action will introduce the logic that, with every
new transaction received by the API, will trigger the "Mine
Pending Transactions" action on the "Blockchain"
object, which will be detailed later.</p>
<p>Initially, the action will check if the transaction's
originating address is 'none', indicating a reward transaction that
does not require further processing:</p>
<pre>
if(fromAddress == 'none') return
</pre>
<p>Next, the script retrieves information from the
"Blockchain" object to access the instance's URL and the
value of the "Authorization" attribute, necessary for
authentication in HTTP calls, given that the "Blockchain"
object has access restrictions:</p>
<pre>
def obj = ObjectEntryLocalServiceUtil.getObjectEntry(id)
def objDef = ObjectDefinitionLocalServiceUtil.fetchObjectDefinition(obj.companyId, "C_Blockchain")
def objectsEntries = ObjectEntryLocalServiceUtil.getObjectEntries(
0, objDef.objectDefinitionId, 0, 1)
def objEntry = objectsEntries.get(0)
def authheader = objEntry.values.get("authorization")
def blockchainUrl = objEntry.values.get("blockchainURL")
</pre>
<p>Considering that the action on the "Blockchain" object
will be of the "standalone" type, the API call will use the
PUT method. The call's URL is constructed by concatenating the
blockchain's URL with the specific API endpoint to trigger the
standalone action:</p>
<pre>
def putUrl = "${blockchainUrl}/o/c/blockchains/${objEntry.objectEntryId}/object-actions/minePendingTransactions"
</pre>
<p>To ensure that the call is executed asynchronously and after the
successful inclusion of the record, the script runs within a new
Thread, with an initial delay of 5 seconds:</p>
<pre>
Thread.start {
sleep(5000) // Sleep for 5 seconds
HttpClient httpClient = HttpClientBuilder.create().build()
HttpPut httpPut = new HttpPut(putUrl)
httpPut.setHeader('Content-Type', 'application/json')
httpPut.setHeader('Authorization', "$authheader")
HttpResponse putResponse = httpClient.execute(httpPut)
}
</pre>
<p>The complete code of the script can be checked at the following
address: <a href="https://github.com/andrefabbro/blockchain-demo/blob/main/src/main/java/com/liferay/raycoin/objects/transaction/CheckPendingTransactions.groovy">https://github.com/andrefabbro/blockchain-demo/blob/main/src/main/java/com/liferay/raycoin/objects/transaction/CheckPendingTransactions.groovy</a></p>
<h3>Compute Balances</h3>
<p>To integrate the logic of updating balances in the Liferay
blockchain, first access the Blockchain object definition through the
control panel and add a new Action called "Compute
Balances". In this context, choose "Standalone" as the
"Trigger" type and select "Groovy Script" as the
action to be executed.</p>
<p> <img data-fileentryid="122415633"
src="https://liferay.dev/documents/portlet_file_entry/14/image-58.png/750d15bf-61f8-89ba-000a-e70d8449b39e">
<br> </p>
<p>We will develop a class called BlockchainBalance, which will
incorporate essential attributes such as companyId and userId, in
addition to an additional parameter objectDefinitionId to record the
Wallet Balance object definition ID:</p>
<pre>
class BlockchainBalance {
long companyId
long userId
long objectDefinitionId
BlockchainBalance(long companyId, long userId) {
this.companyId = companyId
this.userId = userId
this.objectDefinitionId = ObjectDefinitionLocalServiceUtil.fetchObjectDefinition(
this.companyId, "C_WalletBalance").getObjectDefinitionId()
}
}
</pre>
<p>We will implement a method removeAllBalances() to clear all
balance records, preparing the system for a balance update based on
Blockchain transactions:</p>
<pre>
void removeAllBalances() {
def balanceEntries = ObjectEntryLocalServiceUtil.getObjectEntries(
0, objectDefinitionId, QueryUtil.ALL_POS, QueryUtil.ALL_POS)
balanceEntries.each { b ->
ObjectEntryLocalServiceUtil.deleteObjectEntry(b.objectEntryId)
}
}
</pre>
<p>The computeBalance() method will be responsible for first
clearing existing balances and then calculating and recording
updated balances based on approved transactions. This includes
initializing the 'none' wallet with a significant balance for reward
distribution and updating balances according to processed transactions:</p>
<pre>
void computeBalance() {
removeAllBalances()
Map balances = new HashMap()
def objTransactionDef = ObjectDefinitionLocalServiceUtil.fetchObjectDefinition(
this.companyId, "C_Transaction")
List approvedTransactions = ObjectEntryLocalServiceUtil.getObjectEntries(
0, objTransactionDef.objectDefinitionId, WorkflowConstants.STATUS_APPROVED, QueryUtil.ALL_POS, QueryUtil.ALL_POS)
balances.put("none", 1000000000)
approvedTransactions.each { t ->
def v = ObjectEntryLocalServiceUtil.getValues(t.objectEntryId)
def fromAddress = v.get("fromAddress")
def toAddress = v.get("toAddress")
def amount = v.get("amount")
if(balances.get(fromAddress) == null) {
balances.put(fromAddress, new BigDecimal(0))
}
if(balances.get(toAddress) == null) {
balances.put(toAddress, new BigDecimal(0))
}
balances.put(fromAddress, balances.get(fromAddress) - amount)
balances.put(toAddress, balances.get(toAddress) + amount)
}
balances.each { address, balance ->
def walletBalanceValues = [address: address, balance: balance]
ObjectEntryLocalServiceUtil.addObjectEntry(
userId, 0, objectDefinitionId, walletBalanceValues, new ServiceContext())
}
}
</pre>
<p>Finalize by creating an instance of BlockchainBalance and
invoking the computeBalance() method to effectively update the
wallet balances:</p>
<pre>
def obj = ObjectEntryLocalServiceUtil.getObjectEntry(id)
balance = new BlockchainBalance(obj.getCompanyId(), Long.valueOf(creator))
balance.computeBalance()
</pre>
<p>The complete code of this script can be accessed at the
following address: <a href="https://github.com/andrefabbro/blockchain-demo/blob/main/src/main/java/com/liferay/raycoin/objects/blockchain/ComputeBalances.groovy">https://github.com/andrefabbro/blockchain-demo/blob/main/src/main/java/com/liferay/raycoin/objects/blockchain/ComputeBalances.groovy</a></p>
<h3>Mine Pending Transactions</h3>
<p>To effectively implement block mining and its inclusion in the
blockchain, we will add a new action to the Blockchain object called
"Mine Pending Transactions". This action will be of the
"Standalone" type and will use "Groovy Script" as
its execution method.</p>
<p>Within the script, we initially retrieve all transactions with
'PENDING' status:</p>
<pre>
def userId = Long.valueOf(creator)
def user = UserLocalServiceUtil.getUserById(userId)
def objDef = ObjectDefinitionLocalServiceUtil.fetchObjectDefinition(user.companyId, "C_Transaction")
List pendingTransactions = ObjectEntryLocalServiceUtil.getObjectEntries(
0, objDef.objectDefinitionId, WorkflowConstants.STATUS_PENDING, QueryUtil.ALL_POS, QueryUtil.ALL_POS)
</pre>
<p>We check if the number of pending transactions exceeds the limit
defined in the maxPendingTransactions attribute of the Blockchain
object. If so, we proceed to mine a new block:</p>
<pre>
if(pendingTransactions.size() > maxPendingTransactions) {
def blockchain = new Blockchain(
user.companyId, userId, id, rewardAddress, rewardValue, pendingTransactions, authorization, blockchainURL)
blockchain.minePendingTransactions()
}
</pre>
<p>Thus, the Blockchain class can start with the following attributes:</p>
<pre>
class Blockchain {
long companyId
long userId
long blockchainId
String rewardAddress
BigDecimal rewardValue
List pendingTransactions
String authorization
String blockchainURL
}
</pre>
<p>We will also need two other support classes, to handle blocks
and transactions, therefore we create a Block class:</p>
<pre>
class Block {
long blockchainId
int index
String timestamp
List transactions
String previousHash
String hash
int nonce
}
</pre>
<p>And also a Transaction class:</p>
<pre>
class Transaction {
Long id
String fromAddress
String toAddress
BigDecimal amount
String signature
}
</pre>
<p>In this Transaction class, we will add methods to return the
transaction data in the format as we have used in other classes, in
addition to the methods toJson(), toJsonArray(), and toString(), which
we will use as the basis for calculating the Block Hash:</p>
<pre>
String getTransactionData() {
return "${fromAddress}${toAddress}${amount}"
}
JsonObject toJson() {
JsonBuilderFactory factory = Json.createBuilderFactory(null);
JsonObject transactionJson = factory.createObjectBuilder()
.add("fromAddress", fromAddress)
.add("toAddress", toAddress)
.add("amount", amount)
.add("signature", signature)
.add("id", id)
.build();
return transactionJson;
}
static JsonArray toJsonArray(List transactions) {
JsonBuilderFactory factory = Json.createBuilderFactory(null);
JsonArrayBuilder arrayBuilder = factory.createArrayBuilder();
for (Transaction transaction : transactions) {
arrayBuilder.add(transaction.toJson());
}
return arrayBuilder.build();
}
String toString() {
return toJson().toString();
}
</pre>
<p>In addition, we will still need methods to save a new
transaction, and another to delete a transaction:</p>
<pre>
ObjectEntry save(long companyId, long userId) {
def values = [fromAddress: fromAddress, toAddress: toAddress, amount: amount, signature: signature]
def objDef = ObjectDefinitionLocalServiceUtil.fetchObjectDefinition(companyId, "C_Transaction")
ObjectEntry obj = ObjectEntryLocalServiceUtil.addObjectEntry(
userId, 0, objDef.objectDefinitionId, values, new ServiceContext())
this.id = obj.objectEntryId
return obj
}
void delete() {
ObjectEntryLocalServiceUtil.deleteObjectEntry(this.id)
}
</pre>
<p>Finally, two more methods, the first to update the transaction
status, and another to associate the transaction with a block:</p>
<pre>
ObjectEntry updateStatus(long userId, int status) {
return ObjectEntryLocalServiceUtil.updateStatus(userId, this.id, status, new ServiceContext())
}
ObjectEntry addToBlock(long userId, long blockId) {
def values = [r_transactions_c_blockId: blockId]
return ObjectEntryLocalServiceUtil.updateObjectEntry(userId, this.id, values, new ServiceContext())
}
</pre>
<p>With the Transaction class complete, we will now include four
more methods in the Block class, the first is to save the object:</p>
<pre>
ObjectEntry save(long companyId, long userId) {
def values = [index: index, hash: hash, previousHash: previousHash, nonce: nonce, r_blockchain_c_blockchainId: blockchainId]
def objDef = ObjectDefinitionLocalServiceUtil.fetchObjectDefinition(companyId, "C_Block")
return ObjectEntryLocalServiceUtil.addObjectEntry(
userId, 0, objDef.objectDefinitionId, values, new ServiceContext())
}
</pre>
<p>Then, three other methods that are involved in the mining
process of the block:</p>
<pre>
String calculateHash() {
return md5("${index}${timestamp}${Transaction.toJsonArray(transactions)}${previousHash}${nonce}")
}
void mineBlock(int difficulty) {
while (!hash[0..<difficulty].every { it == '0' }) {
nonce++
hash = calculateHash()
}
}
String md5(String input) {
MessageDigest digest = MessageDigest.getInstance("MD5")
byte[] hash = digest.digest(input.getBytes("UTF-8"))
return hash.encodeHex().toString()
}
</pre>
<p>The mineBlock() method is essential for the block mining process
in the blockchain, determined by the difficulty parameter, an
integer that defines the complexity of the task. The higher the
difficulty value, the greater the computational effort required to
find a valid hash for the block, based on the combination of various
elements like the block index, timestamp, the list of transactions,
the previous block's hash, and the nonce (an attempt counter). The
goal is to generate a hash that starts with a specific sequence of
zeros, meeting the established difficulty criterion. This mechanism,
although simplified in this example using the MD5 algorithm for hash
generation, illustrates the fundamental concept behind mining in
blockchains. In more advanced implementations, more complex
cryptographic algorithms and mathematical problems are used to enhance
the security and integrity of the chain. However, a simplified
approach was chosen in this case to facilitate understanding and
demonstration of the mining process.</p>
<p>Returning to the Blockchain class, we will implement a utility
method for making RestFul API calls:</p>
<pre>
private JsonArray fetchItemsFromApi(url) {
HttpGet httpGet = new HttpGet(url)
httpGet.setHeader('Content-Type', 'application/json')
HttpClient httpClient = HttpClientBuilder.create().build()
HttpResponse getResponse = httpClient.execute(httpGet)
String getResponseBody = EntityUtils.toString(getResponse.getEntity())
JsonObject getResponseJson = Json.createReader(new StringReader(getResponseBody)).readObject()
return getResponseJson.getJsonArray("items")
}
</pre>
<p>And another method to retrieve the balance of a specific wallet:</p>
<pre>
private BigDecimal getWalletBalance(String address) {
def encodedFilter = URLEncoder.encode("address eq '${address}'", "UTF-8")
def getUrl = "${this.blockchainURL}/o/c/walletbalances/?filter=${encodedFilter}"
JsonArray walletBalancesJsonArray = this.fetchItemsFromApi(getUrl)
if(walletBalancesJsonArray.empty) {
return new BigDecimal(0)
} else {
JsonObject walletBalance = walletBalancesJsonArray.getJsonObject(0)
return new BigDecimal(walletBalance.getJsonNumber("balance").toString())
}
}
</pre>
<p>It is still necessary to have two other methods, one to retrieve
the latest block in the chain, and another to retrieve a block from
its Previous Hash attribute:</p>
<pre>
private JsonArray getLatestBlockJson() {
def encodedSort = URLEncoder.encode("id:desc", "UTF-8")
def getUrl = "${this.blockchainURL}/o/c/blocks/?sort=${encodedSort}&pageSize=1"
return this.fetchItemsFromApi(getUrl)
}
private JsonArray getBlockWithPreviousHashJson(String previousHash) {
def encodedFilter = URLEncoder.encode("previousHash eq '${previousHash}'", "UTF-8")
def getUrl = "${this.blockchainURL}/o/c/blocks/?filter=${encodedFilter}"
return this.fetchItemsFromApi(getUrl)
}
</pre>
<p>After that, we will implement the final method, called
minePendingTransactions(), which will be responsible for implementing
the entire business logic. We create an internal list of Transactions
to receive the values from the list of pending transactions. Next, we
create a reward transaction, save this transaction in the database,
and also add it to the same list:</p>
<pre>
void minePendingTransactions() {
List transactions = new ArrayList()
pendingTransactions.each { pt ->
if(pt.getValues().get("signatureValid") == true) {
transactions << new Transaction(
pt.getValues().get("fromAddress"),
pt.getValues().get("toAddress"),
pt.getValues().get("amount"),
pt.getValues().get("signature"),
pt.objectEntryId)
}
}
def rewardTransaction = new Transaction("none", rewardAddress, rewardValue)
rewardTransaction.save(companyId, userId)
transactions << rewardTransaction
</pre>
<p>Then retrieve the Hash of the last block in the chain, as well
as its index, to increment it and use it as a parameter in the block
we need to create:</p>
<pre>
JsonObject latestBlockJson = this.latestBlockJson.getJsonObject(0)
def index = latestBlockJson.getJsonNumber("index").intValue()+1
def previousHash = latestBlockJson.getString("hash")
</pre>
<p>To ensure that the transactions are not processed by another
mining process, which may occur in parallel, we will change the
status of all transactions to 'SCHEDULED':</p>
<pre>
transactions.each { t ->
t.updateStatus(userId, WorkflowConstants.STATUS_SCHEDULED)
}
</pre>
<p>Next, we can create the new block and then execute the method
for mining, using a difficulty level of 2, meaning the generated
hash must contain two zeros at the beginning:</p>
<pre>
def block = new Block(index, new Date().toString(), transactions, previousHash, blockchainId)
def difficulty = 2
block.mineBlock(difficulty)
</pre>
<p>After the mining process, we check if another block that was
mined before this one and placed in the same position exists, if
there is, we must abandon the mining, deleting the reward
transaction, and returning the status of the transactions to 'PENDING':</p>
<pre>
def blockWithPreviousHashJson = this.getBlockWithPreviousHashJson(previousHash)
if(blockWithPreviousHashJson != null && blockWithPreviousHashJson.size() > 0) {
rewardTransaction.delete()
transactions.each { t ->
t.updateStatus(userId, WorkflowConstants.STATUS_PENDING)
}
// stop the execution
return
}
</pre>
<p>If everything went right, we can persist the block in the system:</p>
<pre>
ObjectEntry blockMined = block.save(companyId, userId)</pre>
<p>To efficiently manage and update the balances of the wallets
involved in the transactions within the block mining process, we will
implement the following logic using a HashMap. This map will store the
current balances of all wallets impacted by the transactions,
facilitating the validation of sufficient funds and the correct
association to the mined block. Each transaction will be individually
assessed to determine if the originating wallet has sufficient funds
for the transaction. Otherwise, the status of the transaction will be
marked as 'DENIED'. Approved transactions will receive the status 'APPROVED':</p>
<pre>
Map walletBalances = new HashMap()
transactions.each { t ->
walletBalances.put(t.fromAddress, this.getWalletBalance(t.fromAddress))
}
transactions.each { t ->
t.addToBlock(userId, blockMined.objectEntryId)
BigDecimal balance = walletBalances.get(t.fromAddress)
if(balance < t.amount) {
println "Wallet $t.fromAddress doesn't have sufficient coins"
t.updateStatus(userId, WorkflowConstants.STATUS_DENIED)
} else {
walletBalances.put(t.fromAddress, balance - t.amount)
t.updateStatus(userId, WorkflowConstants.STATUS_APPROVED)
}
if(walletBalances.containsKey(t.toAddress)) {
walletBalances.put(t.toAddress, (walletBalances.get(t.toAddress)+t.amount))
}
}
</pre>
<p>After the successful completion of the mining process and
transaction validation, we will update the reward transaction's status
to 'APPROVED' and associate it with the mined block. To ensure data
consistency and avoid potential execution conflicts, updating the
wallet balances will be performed asynchronously, with a brief delay:</p>
<pre>
def computeBalanceUrl = "${blockchainURL}/o/c/blockchains/${blockchainId}/object-actions/computeBalances"
Thread.start {
sleep(3000)
rewardTransaction.updateStatus(userId, WorkflowConstants.STATUS_APPROVED)
rewardTransaction.addToBlock(userId, blockMined.objectEntryId)
HttpClient httpClient = HttpClientBuilder.create().build()
HttpPut httpPut = new HttpPut(computeBalanceUrl)
httpPut.setHeader('Content-Type', 'application/json')
httpPut.setHeader('Authorization', "$authorization")
HttpResponse putResponse = httpClient.execute(httpPut)
}
</pre>
<p>The complete code, including all necessary classes for the
implementation, is available at the following link: <a href="https://github.com/andrefabbro/blockchain-demo/blob/main/src/main/java/com/liferay/raycoin/objects/blockchain/MinePendingTransactions.groovy">https://github.com/andrefabbro/blockchain-demo/blob/main/src/main/java/com/liferay/raycoin/objects/blockchain/MinePendingTransactions.groovy</a></p>
<h2>Tests</h2>
<p>With the setup of the necessary actions complete, it is now
possible to start creating new transactions using the XChangeRay
instance. But first, go to the Blockchain in Applications -> Custom
Apps -> Blockchain menu and execute the Compute Balances method
from the Blockchain record in order to compute the initial balance of
the wallets.</p>
<p> <img data-fileentryid="122415972"
src="https://liferay.dev/documents/portlet_file_entry/14/image-62.png/12fee76d-0f43-63d7-f781-5a5119df6b21">
<br> </p>
<p>After this, create some wallets and transactions from the
XChangeRay instance. This process will allow the direct observation of
how transactions are processed and integrated into the RayCoin
instance. It is recommended to start with the "XChangeRay
Wallet" as it initially holds a significant amount of coins,
facilitating the execution of test transactions. Thus, create some
wallets and transactions in this instance.</p>
<p> <img data-fileentryid="122415643"
src="https://liferay.dev/documents/portlet_file_entry/14/image-59.png/f33452d6-fa0c-735a-32e5-da51cc99613e">
<br> </p>
<p>It is also possible to follow the blocks records.</p>
<p> <img data-fileentryid="122415652"
src="https://liferay.dev/documents/portlet_file_entry/14/image-60.png/35866b3e-2681-a07e-8918-b7048a7d0501">
<br> </p>
<p>And also the wallet balances through the Wallet Balances object.</p>
<p> <img data-fileentryid="122415661"
src="https://liferay.dev/documents/portlet_file_entry/14/image-61.png/433a4e16-0ac5-ee3d-52ca-305fcf93b70c">
<br> </p>
<h1>Artifacts</h1>
<p>All the code for this project is available on the GitHub
repository: <a href="https://github.com/andrefabbro/blockchain-demo/">https://github.com/andrefabbro/blockchain-demo/</a></p>
<p>Moreover, the repository contains examples of implementing a
blockchain in Groovy, serving as the basis for the scripts developed
in this project. These examples can be found in the folder: <a href="https://github.com/andrefabbro/blockchain-demo/tree/main/src/main/groovy/com/liferay/poc/blockchain/example">https://github.com/andrefabbro/blockchain-demo/tree/main/src/main/groovy/com/liferay/poc/blockchain/example</a></p>
<p>You can also find the definition of all the objects used in this
article, which can be easily imported into a Liferay instance of the
same version, at the following link: <a href="https://github.com/andrefabbro/blockchain-demo/tree/main/objects">https://github.com/andrefabbro/blockchain-demo/tree/main/objects</a></p>
<p>For those who prefer a more direct approach and want to try the
demo showcased in this article without going through the whole
configuration process, it is feasible to start an instance of Liferay
version 7.4.3.109-ga109, connect it to a local MySQL database, and
import the DUMP available at: <a href="https://github.com/andrefabbro/blockchain-demo/tree/main/mysql-dump">https://github.com/andrefabbro/blockchain-demo/tree/main/mysql-dump</a></p>
<p>This DUMP includes all the object definitions, scripts, and
sample data necessary to simulate the blockchain described in this
article, providing an efficient way to visualize the application in action.</p>
<h1>Conclusion</h1>
<p>The popularity of blockchain, initially spurred by the
cryptocurrency market, barely scratches the surface of its disruptive
potential. Beyond the realm of digital currencies, blockchain has a
wide array of applications in various sectors, from smart contracts,
supply chains, and logistics, to real estate registry, digital
identity, and electronic voting. These applications underscore the
innovative capability of blockchain to offer robust solutions for
traditional challenges, transforming processes in a myriad of industries.</p>
<p>While this article focused on exploring the development of a
blockchain utilizing the native functionalities of Liferay,
specifically through Objects, it is important to highlight that the
Liferay platform is equipped with other tools and extensions, such as
Client Extensions (available at: <a
href="https://learn.liferay.com/w/dxp/building-applications/client-extensions">https://learn.liferay.com/w/dxp/building-applications/client-extensions</a>),
enabling the implementation of additional business logics. Therefore,
Liferay presents itself as a versatile and powerful platform, capable
of supporting the development of complex solutions, allowing
developers to concentrate their efforts on the specific requirements
of each business.</p>
<p>This article aims not only to demonstrate the applicability of
blockchain technology in diverse contexts but also to emphasize the
flexibility of Liferay as a development tool. The possibilities are
broad and promising, paving the way for innovations that can reshape
the fabric of various sectors, enhancing efficiency, transparency, and security.</p>Andre Fabbro2024-02-20T22:57:00ZPublications updateLászló Pap/es/c/blogs/rss&entryId=1223905442024-02-09T10:49:35Z2024-02-09T10:36:00Z<p>In 2024 Q1 we will release a performance boosting feature for larger
publications on <a
href="https://liferay.atlassian.net/browse/LPD-10782">LPD-10782</a>. "The
original vision for Publications was to allow customers to make as few
or as many changes as needed, without performance drops. This has been
true for few changes as the experience has been great but it has not
been true for large datasets." <strong>Our current measurements
indicate 10%-30% performance increase in database writing
operations, also 10 times faster reading performance!</strong> </p>László Pap2024-02-09T10:36:00ZAutomate your deploys using gitlab runner CI/CDDaniel Dias/es/c/blogs/rss&entryId=1223809562024-02-06T18:19:29Z2024-02-06T00:00:00Z<h1>Introduction</h1>
<p>GitLab is a web-based DevOps lifecycle tool that provides a Git
repository manager, continuous integration (CI), continuous delivery
(CD), and a range of collaboration and project management features. It
is used to manage the software development process, facilitating
collaboration among team members, version control with Git, and
automated build and deployment pipelines. In this blog, I will show
you how to create an automated process capable of checking out,
compiling, and deploying your Liferay code using GitLab.</p>
<h2> </h2>
<h2>What do we need to setup ?</h2>
<p>You will need a GitLab repository, either from gitlab.com or
your own local installation, where you can create and manage your projects.</p>
<p>Create a project inside GitLab with a name of your choice, such
as 'liferay-cicd-demo-project.'</p>
<p>After creating the project, you need to push the Liferay
workspace to your Git repository. For instance, you could use the
same name for the Git project, like 'liferay-cicd-demo-project.'</p>
<p>Once the project is created and pushed, you will need to perform
some configurations to enable communication between your machine and GitLab.</p>
<h2> </h2>
<h2>Install gitlab runner in your machine</h2>
<p>You should navigate to your GitLab project, then go to
<strong>Settings > CI/CD > Runners</strong>, and click on the
expand button.</p>
<p> <img data-fileentryid="122380966"
src="https://liferay.dev/documents/portlet_file_entry/14/gitlab01.png/522544ff-6a13-bc35-55e5-49dfe2f95fe3"> </p>
<p> </p>
<p>Next to the 'New project runner' button, you'll find a
three-dots icon; click on it to view the expanded menu.</p>
<p> </p>
<p> <img data-fileentryid="122381176"
src="https://liferay.dev/documents/portlet_file_entry/14/gitlab02.png/6b44d69e-29ff-bff8-3e9d-990ddbe28706"> </p>
<p> </p>
<p>Click on "<strong>Show runner installation and registration
instructions</strong>" and choose your operating system. For this
example i will consider <strong>Linux</strong>.</p>
<p> </p>
<p> <img data-fileentryid="122381185"
src="https://liferay.dev/documents/portlet_file_entry/14/gitlab03.png/aeb19be1-2266-dec6-1db8-30f39795b7ca"> </p>
<p>You can copy all these lines and execute them on your machine
one by one. In the last line where it says
'<strong>gitlab.com</strong>' and '<strong><token></strong>',
the values associated are specific to your Git project.</p>
<h2> </h2>
<h2>Register your runner in your machine</h2>
<p>When executing the last line of the previous command, it will
prompt for some information. For the <strong>Registration
name</strong> and <strong>tag</strong>, you can use something not
autogenerated to make it easier to reference later in a pipeline.</p>
<p> <code>sudo gitlab-runner register --url https://gitlab.com/
--registration-token GR1348941hNNk4382zPsyyM Runtime platform
arch=amd64 os=darwin pid=10894
revision=f86890c6 version=15.8.1 Running in system-mode.</code> </p>
<p> <code>Created missing unique system ID
system_id=s_1e2e418230d4<br> Enter the GitLab instance URL (for
example, https://gitlab.com/):<br> [https://gitlab.com/]:<br> Enter
the registration token:<br> [GR1348941hNNk4382zPsyyM]:<br> Enter a
description for the runner:<br> liferay-cicd-demo-project-runner<br>
Enter tags for the runner (comma-separated):<br>
liferay-cicd-demo-project-runner<br> Enter optional maintenance note
for the runner:</code> </p>
<p> <code>WARNING: Support for registration tokens and runner
parameters in the 'register' command has been deprecated in
GitLab Runner 15.6 and will be replaced with support for
authentication tokens. For more information, see
https://gitlab.com/gitlab-org/gitlab/-/issues/380872<br> Registering
runner... succeeded runner=GR1348941hNNkTppX<br>
Enter an executor: instance, docker-ssh, parallels, shell,
docker+machine, docker-ssh+machine, kubernetes, custom, docker, ssh,
virtualbox:<br> shell<br> Runner registered successfully. Feel free
to start it, but if it's running already the config should be
automatically reloaded!</code> </p>
<p> <code>Configuration (with the authentication token) was saved
in "/etc/gitlab-runner/config.toml"</code> </p>
<p> </p>
<p>After successfully executing the command, it will create a new
runner with a green signal on the same page in GitLab.</p>
<p> </p>
<p> <code> <img data-fileentryid="122381201"
src="https://liferay.dev/documents/portlet_file_entry/14/gitlab4.png/0bed11b1-c4d1-9614-1be1-6ef989f79be7">
</code> </p>
<h2>Create a pipeline</h2>
<p>Now that you have already created the connection between you
machine and gitlab, you need to create a pipeline to be executed
whenever you what (every commit on a specific branch, every new
git tag, every rule that you define)</p>
<p>To do this, you need to create a file in the root of your
project called <strong>.gitlab-cy.yaml</strong> </p>
<p>The next example shows how to create a pipeline to be executed
every commit in the <strong>main branch</strong> </p>
<pre>
<code><strong>stages</strong>:
- build-liferay
<strong>before_script</strong>:
- if [[ $CI_COMMIT_MESSAGE == *NOTCI* ]]; then exit 0; fi
#running in the runner
<strong>build-liferay</strong>:
<strong>stage</strong>: build-liferay
<strong>script</strong>:
- echo "Building lf"
- cd liferay-cicd-demo-project && ./gradlew deploy && if [ -d ./bundles ]; then cp ./bundles/osgi/modules/* /opt/liferay/deploy/ && cp ./bundles/deploy/* /opt/liferay/deploy/; fi
<strong>cache</strong>:
<strong>key</strong>: $CI_COMMIT_REF_NAME
<strong>policy</strong>: push
<strong>paths</strong>:
- build
- .gradle
<strong>tags</strong>:
- liferay-cicd-demo-project-runner
<strong>only</strong>:
- main
</code></pre>
<p> </p>
<h2>Conclusion</h2>
<p>With this setup and configuration, your machine will listen for
commits in the main branch and then automatically check out your code,
compile, and deploy it. This automated approach eliminates the need
for manual compilation, making it well-suited for use in test or UAT
environments and avoiding human interaction in the process of
generating and copying the JAR files to the project. This entire
process can also be accomplished using Docker containers running a
GitLab runner. Personally, I find this option appealing as it provides
a fully containerized system ready to build, deploy, and serve the
project with all components interconnected.</p>
<p> </p>
<p>If you are interested in learning more about this topic, please
feel free to comment or send a message.</p>
<p> </p>
<p> <strong>Danie Dias</strong> <br> Head of Liferay Business
Unit at PDMFC<br> daniel.dias@pdmfc.com</p>Daniel Dias2024-02-06T00:00:00ZUNICAST CLUSTERING WITH KUBERNETESDaniel Dias/es/c/blogs/rss&entryId=1223726012024-02-20T17:43:04Z2024-01-31T19:05:00Z<h1>Introduction</h1>
<p> <a
href="https://kubernetes.io/docs/concepts/overview/">Kubernetes</a>,
commonly referred to as K8s, is an open-source system designed for
automating the deployment, scaling, and management of containerized
applications. It is a robust and flexible solution for deploying
Liferay, facilitating scalability and monitoring. While creating a
Liferay cluster on Kubernetes is not overly complex, there are certain
nuanced aspects that require attention.</p>
<p> </p>
<h2>What do we need to setup the cluster?</h2>
<p>To ensure proper functionality, you need to copy the
<code>tcp.xml</code> file from the Liferay source code and modify it
to work with DNS_PING</p>
<p> <strong>DNS_PING</strong> utilizes DNS A or SRV entries for
discovery. To enable DNS discovery for applications deployed on
Kubernetes, it is necessary to create a <a
href="https://kubernetes.io/docs/concepts/services-networking/service/#headless-services"
target="_blank" rel="noopener noreferrer">Headless Service</a> with
appropriate selectors that cover the desired pods. This service
ensures that DNS entries are populated as soon as the pods reach the
ready state.</p>
<h2>Example of an headless service</h2>
<pre>
<code>apiVersion: v1
kind: Service
metadata:
name: liferay-cluster <strong>//Service name</strong>
labels:
name: liferay-cluster
spec:
clusterIP: None
selector:
app: liferay
ports:
- port: 7800
name: jgroupsliferay-control <strong>//Service port name</strong>
protocol: TCP
targetPort: 7800
- port: 7900
name: jgroupsliferay-transport <strong>//Service port name</strong>
protocol: TCP
targetPort: 7900</code></pre>
<p>To enable the use of DNS_PING, you must create your
<code>unicast.xml</code> file to store a <code>dns_query</code>. This
file can be dynamically injected into Kubernetes using a ConfigMap and
then injected into the container. If you are using the official
Liferay container, you have the option to store the file in <code>/mnt/liferay/files/tomcat/webapps/ROOT/WEB-INF/classes/unicast.xml</code>.</p>
<p>While it is possible to create a single file for both transport
and control, I recommend using two different files to make it easier
to define different ports for each channel.</p>
<p>For instance, you could name the files
<code>jgroupsliferay-control.xml</code> and
<code>jgroupsliferay-transport.xml</code> as examples.</p>
<h2>Example of the jgroupsliferay-control.xml</h2>
<p><config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"</p>
<p>xmlns="urn:org:jgroups"</p>
<p>xsi:schemaLocation="urn:org:jgroups http://www.jgroups.org/schema/jgroups.xsd"></p>
<p><TCP bind_port="7800"</p>
<p>recv_buf_size="${tcp.recv_buf_size:130k}"</p>
<p>send_buf_size="${tcp.send_buf_size:130k}"</p>
<p>max_bundle_size="64K"</p>
<p>sock_conn_timeout="300"</p>
<p>thread_pool.min_threads="0"</p>
<p>thread_pool.max_threads="20"</p>
<p>thread_pool.keep_alive_time="30000"/></p>
<p><dns.DNS_PING
dns_query="<strong><service-port-name></strong>._tcp.<strong><service-name></strong>.<strong><kubernetes-namespace></strong>.svc.cluster.local" dns_record_type="SRV"/></p>
<p><MERGE3 min_interval="10000"</p>
<p>max_interval="30000"/></p>
<p><FD_SOCK/></p>
<p><FD_ALL timeout="9000" interval="3000" /></p>
<p><VERIFY_SUSPECT timeout="1500" /></p>
<p><BARRIER /></p>
<p><pbcast.NAKACK2 use_mcast_xmit="false"</p>
<p>discard_delivered_msgs="true"/></p>
<p><UNICAST3 /></p>
<p><pbcast.STABLE desired_avg_gossip="50000"</p>
<p>max_bytes="4M"/></p>
<p><pbcast.GMS print_local_addr="true" join_timeout="2000"/></p>
<p><UFC max_credits="2M"</p>
<p>min_threshold="0.4"/></p>
<p><MFC max_credits="2M"</p>
<p>min_threshold="0.4"/></p>
<p><FRAG2 frag_size="60K" /></p>
<p><!--RSVP resend_interval="2000" timeout="10000"/--></p>
<p><pbcast.STATE_TRANSFER/></p>
<p></config></p>
<h2>What to add in Portal-ext.properties <code> </code> </h2>
<p>You will need to add 3 properties in you portal-ext.properties</p>
<p> <code>cluster.link.enabled=true</code> </p>
<p>
<code>cluster.link.channel.properties.control=/jgroupsliferay-control.xml</code> </p>
<p>
<code>cluster.link.channel.properties.transport.0=/jgroupsliferay-transport.xml</code> </p>
<h2>Conclusion</h2>
<p>While there are several methods to implement Liferay clustering,
the approach mentioned above is considered the easiest within the
constraints of Kubernetes network restrictions. Another option is to
utilize JDBC Ping, but if you opt for this route, caution is advised
regarding the management of dead zombies. Dead zombies refer to nodes
that no longer exist but are retained in the database as potential
nodes for reasons such as not being properly cleaned up.</p>
<p> </p>
<p>Leave your comments and feedback.</p>
<p>If you need some more information, please PM me or find me at
the Liferay community slack.</p>
<p> </p>
<p> <strong>Danie Dias</strong> <br> Head of Liferay Business
Unit at PDMFC<br> daniel.dias@pdmfc.com</p>Daniel Dias2024-01-31T19:05:00ZLiferay Permission PropagatorVitaliy Koshelenko/es/c/blogs/rss&entryId=1223711102024-01-31T12:01:52Z2024-01-31T09:55:00Z<h1>Introduction</h1>
<p>Roles and Permissions is one of Liferay foundation features that
allows to develop secure and reliable applications. It's crucial to
organize them in a structured, transparent and easy-manageable way.
Sometimes it can be hard, especially when it comes to hierarchical
entities. Fortunately, Liferay has a dedicated feature for this case -
Permissions Propagator.</p>
<h1>Permission Propagator Overview</h1>
<p>Permission Propagator is a Liferay feature that allows to
propagate permissions from a parent entity to child ones. <a
href="https://github.com/liferay/liferay-portal/blob/master/portal-kernel/src/com/liferay/portal/kernel/security/permission/propagator/PermissionPropagator.java"
target="_blank" rel="noopener noreferrer">PermissionPropagator</a>
interface declares a method to be implemented for permissions propagation:</p>
<p> <code>public void propagateRolePermissions(<br>
ActionRequest actionRequest, String className, String primKey,<br>
long[] roleIds)<br> throws
PortalException;</code> </p>
<p>Also, there is a <a
href="https://github.com/liferay/liferay-portal/blob/master/portal-kernel/src/com/liferay/portal/kernel/security/permission/propagator/BasePermissionPropagator.java"
target="_blank"
rel="noopener noreferrer">BasePermissionPropagator</a> class that
provides basic functionality for permissions propagation. </p>
<p>Currently Liferay has two implementation for PermissionPropagator:</p>
<ul> <li> <a
href="https://github.com/liferay/liferay-portal/blob/master/modules/apps/message-boards/message-boards-web/src/main/java/com/liferay/message/boards/web/internal/security/permission/MBPermissionPropagatorImpl.java"
target="_blank"
rel="noopener noreferrer">MBPermissionPropagatorImpl </a> - to
propagate permissions for child MB Categories and Threads from a
parent category;</li> <li> <a
href="https://github.com/liferay/liferay-portal/blob/master/modules/apps/wiki/wiki-web/src/main/java/com/liferay/wiki/web/internal/security/permission/WikiPermissionPropagatorImpl.java"
target="_blank"
rel="noopener noreferrer">WikiPermissionPropagatorImpl </a>- to
propagate permissions from a parent WikiNode to child
WikiPages.</li> </ul>
<p>By default, PermissionPropagator is disabled. To enable it - you
need to add the following property to portal-ext.propeties:</p>
<pre>
permissions.propagation.enabled=true</pre>
<p>Once you enable it - you can check it "in action".
Create a parent MB Category, a child MB Category and a child MB
Thread: once you define permissions to parent MB Category - they
should be automatically propagated to child categories and threads.</p>
<h1>Custom Permission Propagator</h1>
<p>What if you need a custom Permissions Propagator, e.g. to
propagate DL Folder permissions from a parent folder to child ones?
If you check one of the implementations listed above - you'll find
that it's just an OSGi component for PermissionPropagator service,
registered for specific portlet(s) by
specifying <code>javax.portlet.name</code> property:</p>
<pre>
<code>@Component(
property = {
"javax.portlet.name=" + MBPortletKeys.MESSAGE_BOARDS,
"javax.portlet.name=" + MBPortletKeys.MESSAGE_BOARDS_ADMIN
},
service = PermissionPropagator.class
)
public class MBPermissionPropagatorImpl extends BasePermissionPropagator {</code></pre>
<p>In the same way you can create a PermissionPropagator component
for Document Library portlets:</p>
<pre>
@Component(
property = {
"javax.portlet.name=" + DLPortletKeys.DOCUMENT_LIBRARY,
"javax.portlet.name=" + DLPortletKeys.DOCUMENT_LIBRARY_ADMIN
},
service = PermissionPropagator.class
)
public class DLPermissionPropagatorImpl extends BasePermissionPropagator {</pre>
<p>and implement the <code>propagateRolePermissions</code> method. </p>
<p>In the method implementation you can fetch the list of child
folders based on a current one and
invoke <code>propagateRolePermissions</code> method from base
implementation for each of them. The complete implementation (a bit
simplified) is listed below:</p>
<pre>
@Component(
property = {
"javax.portlet.name=" + DLPortletKeys.DOCUMENT_LIBRARY,
"javax.portlet.name=" + DLPortletKeys.DOCUMENT_LIBRARY_ADMIN
},
service = PermissionPropagator.class
)
public class DLPermissionPropagatorImpl extends BasePermissionPropagator {
@Override
public void propagateRolePermissions(ActionRequest actionRequest, String className,
String primKey, long[] roleIds) throws PortalException {
if (!className.equals(DLFolder.class.getName())) {
return;
}
long dlFolderId = GetterUtil.getLong(primKey);
DLFolder dlFolder = dlFolderLocalService.fetchDLFolder(dlFolderId);
List<DLFolder> childFolders = getChildFolders(dlFolder);
for (DLFolder childFolder: childFolders) {
for (long roleId : roleIds) {
propagateRolePermissions(
actionRequest, roleId, DLFolder.class.getName(), dlFolderId,
DLFolder.class.getName(), childFolder.getFolderId());
}
}
}
private List<DLFolder> getChildFolders(DLFolder parentFolder) {
List<DLFolder> childFolders = dlFolderLocalService.getFolders(parentFolder.getGroupId(), parentFolder.getFolderId());
List<DLFolder> allChildFolders = new ArrayList<>(childFolders);
for (DLFolder childFolder: childFolders) {
allChildFolders.addAll(getChildFolders(childFolder));
}
return allChildFolders;
}
@Reference
private DLFolderLocalService dlFolderLocalService;
}</pre>
<p>The getChildFolders method returns a list of all child folders
recursively, and then permissions are propagated for each of them.</p>
<p>PermissionPropagator is invoked automatically when permissions
are updated in Permissions Configuration. </p>
<p>Deploy module with DLPermissionPropagatorImpl component and
check it: now when you define permissions for a parent DL Folder -
they should be propagated to all child ones.</p>
<div class="overflow-auto portlet-msg-info"> <em>
<strong>Note</strong>: make sure permission propagation is
enabled <strong>permissions.propagation.enabled=true</strong> </em> </div>
<p>You can find complete sources for this example here: <a
href="https://github.com/vitaliy-koshelenko/lifedev-portal/tree/permissions-propagator/modules/lifedev-dl-permisson-propagator"
target="_blank" rel="noopener noreferrer">https://github.com/vitaliy-koshelenko/lifedev-portal/tree/permissions-propagator/modules/lifedev-dl-permisson-propagator</a></p>
<p>In a similar way you can implement permission propagation for
any other hierarchical entities, even custom ones.</p>
<h1>Conclusion</h1>
<p>PermissionPropagator is out-of-the-box Lifeary feature, that is
disabled by default and implemented only for Message Boards and Wiki
Pages. But it can be enabled in portal properties, if permission
propagation is required. Also, custom PermissionPropagator compo