107
I Use This!
Activity Not Available

News

Analyzed 5 months ago. based on code collected 5 months ago.
Posted almost 4 years ago by David H Nebinger
I recently was helping a client with an upgrade process and that had run into a little problem... A few of the model hints were not updated on some of the columns in their Service Builder services and, as we all know, Service Builder will not ensure ... [More] the columns are changed in a production environment (well, actually any environment where the schema.module.build.auto.upgrade property is not set to true). For these environments, Liferay recommends using an upgrade process to handle making the changes in those environments and even provides very helpful documentation to build one of these here: https://help.liferay.com/hc/en-us/articles/360031165751-Creating-Upgrade-Processes-for-Modules The team had followed the instructions, step by step, and everything was working fine locally. The next step was to send it to the single-node development environment, and there it also worked swimingly. The next step was to promote it to the two-node UAT environment, and here it crashed and burned hard. Both of the UAT nodes started to process the upgrade at the same time. Each node found that the current version from the Release table was older, found the upgrade step to get to the next version, and both tried to execute the commands to alter the columns but they couldn't both do it. Long story short, the upgrade failed but the Release and ServiceComponent tables thought it had and neither node could start up with the services because version match errors... We finally had to take the update out and restore the database to get back to a known working state. If the team had been able to do a staggered deployment, only sending the upgrade process to a single node in the cluster, then later on send it to the second node, all would have been fine. Only one node would have processed the upgrade, the nodes wouldn't step on each other, and when deployed to the 2nd node the Release table would have already been at the later version so it would have no reason to try and run the upgrade... Sometimes (as in their case) a staggered deployment isn't possible, so this begs the question, is there anything that can be done to deal with this scenario? I think there is, and I'm here to present that solution today - Cluster-Aware Upgrade Processes The challenge is how to run something, such as an upgrade process, on only a single node in the cluster and avoid having multiple nodes try and step on each other. With a few small changes to the UpgradeStepRegistrator and injection of some Cluster Executor classes, you can define upgrade processes that will only register steps on the cluster leader, and non-leaders will not get the steps and not run the upgrade processes. Let's start by modifying Liferay's example for registering the upgrade steps: package com.liferay.mycustommodule.upgrade; import com.liferay.portal.upgrade.registry.UpgradeStepRegistrator; import org.osgi.service.component.annotations.Component; @Component(immediate = true, service = UpgradeStepRegistrator.class) public class MyCustomModuleUpgrade implements UpgradeStepRegistrator { @Override public void register(Registry registry) { registry.register( "com.liferay.mycustommodule", "0.0.0", "2.0.0", new DummyUpgradeStep()); registry.register( "com.liferay.mycustommodule", "1.0.0", "1.1.0", new com.liferay.mycustommodule.upgrade.v1_1_0.UpgradeFoo()); registry.register( "com.liferay.mycustommodule", "1.1.0", "2.0.0", new com.liferay.mycustommodule.upgrade.v2_0_0.UpgradeFoo(), new UpgradeBar()); } } UpgradeFoo the class which is actually altering table column sizes, and this is not something that we want every node in the cluster to try. In the example above, we can see that UpgradeFoo is used in both the 1.0.0 -> 1.1.0 step as well as the 1.1.0 -> 2.0.0 steps, so we want to protect those steps from running on all nodes. We'll start by adding a couple of imports: import com.liferay.portal.kernel.cluster.ClusterExecutor; import com.liferay.portal.kernel.cluster.ClusterMasterExecutor; These imports will provide the services we'll need to determine if we're on the cluster leader or not. Our MyCustomModuleUpgrade class will also get some new dependencies: @Reference private volatile ClusterMasterExecutor _clusterMasterExecutor; @Reference private volatile ClusterExecutor _clusterExecutor; I apologize for having to use names here that some might find offensive. I've tried like the dickens to convince Liferay that there are suitable alternative names that could be used and avoid problematic ones, but there is a great hesitancy (that I also understand) to change working code that many community members and customers might rely on and impose a significant, detrimental impact to those environments. Since we're @Referencing these guys in, we know that our upgrade step registrar is not going to be able to run until those services are available by Liferay and are ready to inject into our instance. Now, we can use these in the register() method to only register if the node is the cluster leader: boolean clusterLeader = false; // we will only be executing this step if (_clusterExecutor.isEnabled()) { clusterLeader = _clusterMasterExecutor.isMaster(); } else { // not in a cluster, so this is the leader clusterLeader = true; } // we will only register this step on the cluster leader if (clusterLeader) { ... We use the two Liferay services to determine if the node is the leader and, if it is, we're then okay to register the upgrade steps. Our final class ends up looking like: package com.liferay.mycustommodule.upgrade; import com.liferay.portal.kernel.cluster.ClusterExecutor; import com.liferay.portal.kernel.cluster.ClusterMasterExecutor; import com.liferay.portal.upgrade.registry.UpgradeStepRegistrator; import org.osgi.service.component.annotations.Component; @Component(immediate = true, service = UpgradeStepRegistrator.class) public class MyCustomModuleUpgrade implements UpgradeStepRegistrator { @Override public void register(Registry registry) { registry.register( "com.liferay.mycustommodule", "0.0.0", "2.0.0", new DummyUpgradeStep()); boolean clusterLeader = false; // we will only be executing this step if (_clusterExecutor.isEnabled()) { clusterLeader = _clusterMasterExecutor.isMaster(); } else { // not in a cluster, so this is the leader clusterLeader = true; } // we will only register this step on the cluster leader if (clusterLeader) { registry.register( "com.liferay.mycustommodule", "1.0.0", "1.1.0", new com.liferay.mycustommodule.upgrade.v1_1_0.UpgradeFoo()); registry.register( "com.liferay.mycustommodule", "1.1.0", "2.0.0", new com.liferay.mycustommodule.upgrade.v2_0_0.UpgradeFoo(), new UpgradeBar()); } } @Reference private volatile ClusterMasterExecutor _clusterMasterExecutor; @Reference private volatile ClusterExecutor _clusterExecutor; } And the Minions? So this code will work to run the upgrade steps on the cluster leader, but what about the other nodes? As is, the nodes would do nothing. They would start up, they would not have any registered upgrade steps to execute, so they'd be ready to start serving traffic. This is important to keep in mind in certain scenarios... Imagine if you have updated your Service Builder service to use a new column, and you followed Liferay's guidance to extend the UpgradeProcess to create the new column, plus you wrote some code to pre-populate the columns of existing rows so your data model will be consistent... Let's say your upgrade process fails on the cluster leader; the other nodes aren't going to know that it failed. The other nodes might expect that the column was already added, that the data was already populated, and the code might not be able to handle the case when either or both of these aren't actually met. You can actually guard against this in some cases. For example, you can add an @Reference for a Release instance like: @Reference( target = "(&(release.bundle.symbolic.name=com.example.mycustom.service)(& release.schema.version>=2.0.0)))" ) private Release _release; This will actually prevent the @Component from starting unless version 2.0.0 or greater is available. Note, however, that the cluster leader is not going to send out notifications that it has finished the upgrade so these @References can be resolved, it will take a node restart for them to resolve. A great place to add this, well as long as your upgrade step registrar (and therefore upgrade steps) are not dependent upon it, is you XxxLocalServiceImpl class. As in our case above, if UpgradeBar class is adding the column and populating it, not allowing XxxLocalServiceImpl to start would prevent any other component dependent upon the service from starting. But, if your upgrade step registrar and/or the upgrade steps are dependent upon the service, the service would be blocked because the upgrade steps didn't run, and the upgrade steps wouldn't run because they are dependent upon the service which hasn't started yet. So this is by no means an ideal solution, but it can be a preventative solution to avoid messy situations where your upgrade is not ready and your code would fail badly as a result. In this case, not starting is going to be better than starting, failing and having to clean up data later on... Conclusion So with this relatively minor change to our upgrade step registration, we receive the following benefits: We don't have to do anything special for cluster startups. Sensitive upgrade steps can run on a single node. We prevent parallel execution of upgrade steps on all nodes in the cluster. If you build a lot of Upgrade Steps (like I often do), keep this trick in mind to avoid cluster upgrade step issues. [Less]
Posted almost 4 years ago by Anne Durey
En aquel momento era diciembre de 2020 y la pandemia ya nos había dejado casi un año sin encuentros presenciales de nuestra comunidad en todo el mundo. En esa ocasión realizamos el primer encuentro de desarrolladores Liferay en español en LATAM en ... [More] línea, pensando que las restricciones se encontraban cerca de su fin y que pronto podríamos encontrarnos presencialmente en alguna parte de la región. Poco más de ocho meses después, continuamos haciendo todo en línea. Sin embargo, pudimos aprovechar la conectividad para empezar un nuevo capítulo sin fronteras de la comunidad Liferay hispanohablante en Latinoamérica.  ¡Nos complace anunciar que ya está disponible el grupo de usuarios Liferay (Liferay User Group, LUG) LATAM! El grupo tiene como objetivo conectar a usuarios, desarrolladores, administradores de sistemas y entusiastas de productos y proyectos de Liferay. Únete al grupo en https://www.meetup.com/pt-BR/liferay-latam-user-group/  Un puente sobre el Atlántico LUG LATAM también es otro espacio para conectar a la comunidad hispanohablante. En este sentido, el papel de LUG España fue fundamental para la creación del grupo, además de ser un gran patrocinador de la idea. Gracias a su fuerza, hoy la comunidad hispanohablante crece año tras año. Y ahora ambos lados del Atlántico pueden juntar energía. Por lo menos en Slack, las distancias de huso horario no importan. Ya estamos todos juntos en el grupo #lug-spanish que recibe a todos los hispanohablantes de la comunidad Liferay en el mundo. Accede aquí para ser parte. Un grupo para todo LATAM Desafortunadamente, aún seguimos con restricciones por la pandemia. Pero, aunque no podamos realizar encuentros presenciales, vamos a aprovechar el digital para unir a los 20 países de habla hispana de la región.  Y ya que estamos hablando de lo que viene, pronto vamos a divulgar la fecha del primer meetup oficial de LUG LATAM. Mientras esperas, te invito a sugerir temas que te gustaría ver u ofrecerte para compartir contenidos en este formulario: https://forms.gle/4Ar82TnRN4wbk5PN8  Si te gustaría participar cómo voluntario del grupo, estamos en búsqueda de co-organizadores, seguidores, entusiastas, ponentes, lo que prefieras. ¡Ayuda a LUG LATAM a expandirse mucho más!  [Less]
Posted almost 4 years ago by Ashley Yuan
The new installers and IDE 3.9.4 ga5 has been made available.  Community Download https://liferay.dev/-/ide-installation-instructions Customers Download ... [More] https://customer.liferay.com/downloads/-/download/liferay-workspace-with-developer-studio-3-9-4 https://customer.liferay.com/downloads/-/download/liferay-workspace-installer-2021-08-09 Release highlights Installers Improvements: Support installation on Mac M1 Improvements on windows installation Update default liferay 7.3 version to 7.3 GA8 Update default liferay 7.4 version to 7.4 GA3 Development Improvements: Add support for liferay docker bundle  Create maven projects using specific target platform version Improve deployment support for Liferay 7.x Integration of Blade CLI 4.0.9 Support for Liferay Workspace Gradle 3.4.12 Support Liferay 7.4 GA3 tomcat bundle Improvements on Liferay upgrade plan Configure workspace product key Update workspace plugin version Switch to use release api dependency Bug fixes About using docker A new docker image and container will be created once users right clicking on the liferay gradle workspace project and select init portal docker bundle, as well as a liferay docker server in servers view.   Same as starting a liferay server, users can click the icons to start the docker server in normal mode or in debug mode. Right clicking on the project and selecting liferay > Docker Deploy to deploy a project to the docker server. Users can check the related messages in console once the project is deployed, they also can open gogo shell through right clicking on the docker server and select open gogo shell option.   Feedback If you run into any issues or have any suggestions please come find us on our community forums or report them on JIRA (IDE project), we are always around to try to help you out. Good luck! [Less]
Posted almost 4 years ago by Roselaine Marques
For big and complex applications and take advantage from some parameter and configuration as a Liferay Application, the best way to have total control over your application is by transforming manually  a native Angular Application to a Liferay ... [More] Angular Widget. You need your Angular application and a new Angular widget generated by liferay-js from scratch. Requirements: Node, Yeoman and NPM Installed 1. First, it is necessary to install liferay-js by running this command:               npm install -g generator-liferay-js   2. Run the generator inside your “liferay-workspace/modules” or some other project folder which you are using:                           yo liferay-js   3. Choose the “Angular Widget” type and fill in the rest of the information needed:   4. Open the src of your Angular Application and copy all HTML files to Angular Widget’s assets/app.  5. Now copy all CSS files to Angular Widget’s assets/app/css.  6. If you have some folder with static files like images, fonts, etc., take them out from these folders and copy them to Angular Widget’s assets. 7. Copy all folders and .ts files (services, model, routing*, components) from Angular Application’s over to the Angular Widget’s src/app/ folder (exceptions are app.module.ts and app.component.ts files). You shouldn’t override the files app.component.ts' generated by Liferay because we will leverage some of the code generated there. There are some examples of how to get context information about your Angular Widget to use to reference static files, like images. About app.modules.ts, the same principle explained above applies: you shouldn’t override or delete it because we are creating a Liferay Angular Widget, and there are some differences to an ordinary Angular App to take account. For example “don't bootstrap any component statically”.  8. Open the Angular Widget’s app.component.ts file and pay attention to the @Component There is a setting: the “templateUrl” which has the reference to the theme used plus project plus html path. Following this sample, you will need to do the same for all components to reference its “html” files. 9. Now open Angular Widget’s app.component.ts file.             a. Here the most important point is the templateUrl which has the reference to  the project as explained before.          b. The second important thing to highlight is the LiferayParams as displayed on the sample code. Through LiferayParams you have access to configuration, portlet-namespace, context-path (useful to compose the static files URLs) and the portlet-element-id.        c. The last thing to pay attention to is the Liferay JS Object, with a lot of utils to use in a Liferay Environment. In this sample you can see how to use the internationalization.  So, after knowing these points you will be able to do the changes from Angular Application’s app.component.ts for Angular Widget’s app.component.ts. Do it! 10. Open Angular Widget’s app.module.ts file. There are several comments about the restriction, so knowing these copy the code from Angular Application’s app.module.ts adapting for Angular Widget’s app.module.ts.        a. In @NgModule, all services should be added to the AppModule providers array. 11. Install the missed “dependencies” or copy and paste from Application’s package.json to Angular Widget’s package.json (Don’t delete the existing dependencies, unless you know what you are doing). Ok. So now we have all files imported for Angular Widget, close the Angular Application project… All attention for the final adjustments to get ready the application migrated.  12. Come back to CSS. If the components migrated are using styleUrl, delete it from all .ts files. As explained before, all “CSS” files were copied to the assets/css folder. Inside this folder, there is a style.css file, importing all “CSS” files..             a. The style.css file is referenced in the package.json inside “portlet” properties, and it is the only css file loaded by your Widget.    13. As a final step before implementing your Liferay Widget, you will need to add the @Inject decorator for service injection to all constructor classes. Remember: every time you need to inject a provider service you need to use @Inject.   14. After we will deploy it both project, the consumer and the provider, for that run inside each other project:                                   npm run deploy   Routing Angular Widget in Liferay For routing an Angular Widget in Liferay we need to adjust some things.  1. Add “{provide: APP_BASE_HREF, useValue: '/'}” to the app.module.ts to configure the root app injector with a base href value from which the router will compose navigation URLs.  import {APP_BASE_HREF} from '@angular/common'; @NgModule({   providers: [{provide: APP_BASE_HREF, useValue: '/'}] }) class AppModule { ngDoBootstrap() {} }   2. Now to prevent the client-side parameters being sent to backend Liferay DXP we need to set a router option to use a hash (i.e. /#/{route}). Open your app-routing.module.ts file and in your @NgModule decorator set it like below: import { Routes, RouterModule } from '@angular/router'; @NgModule({   imports: [RouterModule.forRoot(routes,{useHash: true, enableTracing: false})],   exports: [RouterModule] }) export class AppRoutingModule { }   3. After that, deploy and test it. npm run deploy   Inside your Liferay DXP when you call some route from your Angular Application you will see in the browser the ${domain}/#/{route}. [Less]
Posted almost 4 years ago by David H Nebinger
A really quick post to share something important with you React + Clay developers out there... A recent update to Clay had a breaking internal change. If after deploying your React portlet and you try to place it on the page and you get errors in ... [More] the JS console like: liferay-amd-loader | Errors returned from server for require( ["[email protected]"] ): "Missing dependency '@my-module$clayui/provider' of '@my-module$clayui/[email protected]/lib/Portal'" "Missing version constraints for '@my-module$clayui/provider' in package.json of '@my-module$clayui/[email protected]' (required from its 'lib/Portal' module)" this means that you're using a version since the breaking change was introduced. It's really easy to solve, though. Just npm install @clayui/provider or yarn add @clayui/provider and then build and deploy your module again. If after doing this you still get the same error, try deleting the build and dist folders and do a clean build and that should take care of it. Good Luck!   [Less]
Posted about 4 years ago by Jamie Sammons
Download options 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. Docker image To use ... [More] Liferay Portal 7.4 GA3 and Liferay Commerce 4.0 GA3 run the following: docker run -it -p 8080:8080 liferay/portal:7.4.2-ga3 For more information on using the official Liferay docker image see the liferay/portal repo on Docker Hub. Bundles and other download options For a binary release, please find the Liferay Portal 7.4 GA3 and Liferay Commerce 4.0 GA3 release on the download page.  For additional files (for example, the source code, or dependency libraries), visit the release page. Dependency Management For development using the Liferay Platform, update Liferay Workspace to use the  latest dependencies, by adding the following line to the build.gradle file: dependencies { compileOnly group: "com.liferay.portal", name: "release.portal.api" } All portal dependencies are now defined with a single declaration. When using an IDE such as Eclipse or IntelliJ all apis are immediately available in autocomplete for immediate use.   By setting a product info key property it will be possible to update all dependencies to a new version by updating the liferay.workspace.product property in the liferay workspace projects gradle.property file: liferay.workspace.product = portal-7.4-ga3 When using an IDE such as Eclipse or IntelliJ all apis are immediately available in autocomplete for immediate use. Liferay Commerce Features Display Page Preferences Admins now have full control over display pages for different categories. Set the default page or opt out and choose which pages you want to use for specific products or categories. Map Product Information Into Fragments Just like with content, documents, and other assets, practitioners can now map and insert product information into page fragments.  Product Visibility Management Now use either the admin interface or API to configure product visibility rules. Liferay Portal Features Experience Management Integration with automatic translation service for web content In order to help translators in their translation process, we have integrated with Google Translator service for web contents. Now translators will be able to take advantage of this service to accelerate the translation process so they can focus on reviewing the result provided. Translators can perform a full auto-translation of all the web content fields or translate just specific fields, for example in case of needing to update just part of it. LPS-117996 Select asset library vocabularies defined in the Category Navigation Widget Now it is possible to select vocabularies and categories stored in Asset Libraries within the Category Navigation Widget so users can later on select them to filter the content shown. LPS-132363 Page Audit based on PageSpeed Content authors are concerned about their content to have adequate visibility in search engines, so they can reach the right audiences. To ensure that a page is optimized for search engine indexing and visibility, it is necessary to check the page's compliance with best practices in terms of accessibility, search engine metadata and SEO. Page audit allows users to make a quick check of that compliance and get warnings and tips to improve each one of the categories. To set up the feature, it is only needed an API Key from Google PageSpeed Insights that can be obtained for free here. Once the API Key is configured, users can launch a Page Audit of any public page on their sites by opening the panel and hitting “Launch Audit” or the reload icon. Each language version of the page can be audited separately. The checkpoints encompass Accessibility and SEO issues. The checked issues include the following: Low contrast ratio, Missing img-alt attributes, Missing input-alt attributes, Incorrect image aspect ratios, Missing meta-description, Link texts, Page blocked from indexing, Invalid hreflang, Invalid canonical URL, Illegible font sizes, Small tap targets, Missing element, Uncrawlable links All the issues have a detail view with the description, tips to fix and direct links to the places where issues can be fixed, if possible. Private pages, local-run sites pages and under-login pages cannot be audited, as they’re not crawl-able.   LPS-111803, LPS-128382, LPS-128081  Digital Operations Enabling Automated Live Chat Systems  Liferay now offers integration with a number of live support chat platforms.   Click to chat adds a familiar chat window to sites on Liferay’s instance and can be used to improve support and site experience for users. For more information on how to access and use Click to Chat access Enabling Automated Live Chat Systems’s Documentation. Enabling DocuSign Digital Signature  Liferay now offers integration between DocuSign and Liferay’s portal.  DocuSign is a software that manages documents to be signed electronically, it helps the management and collection of signatures on documents.  For more information access Enabling Docusign Digital Signature’s Documentation.   Forms Extension point for conditions in Forms Rules When adding custom conditions for form rules, they will now appear as OOTB ones, to allow users to normally build rules with custom conditions and have a consistent experience. Story: As a developer, I want to see my custom conditions in the rule builder   ​​​​​​​ Extension point for validations When adding custom validations for form fields, they will now appear as OOTB ones, to allow users to use them in the form builder and have a consistent experience. Story: As a developer, I would like to create an extension to customize the validations of the fields to facilitate the inclusion of new rules    New hide field property To allow users to autofill or calculate via rules, or bring data from other systems via customization, to a field that the user filling the form user won't see to trigger rules in their forms easily. Story: As an form creator, I want to have a hidden field in my form to trigger rules    New field setting to customize required error message Making it easier to customize the error message for required fields to allow form builders to deliver a more consistent experience with their messaging style to their customers, this property is localizable. Story: As a developer, I want to customize the error message of my required fields    Masks for integer numeric types To improve the user experience while filling forms with long integer numeric fields such as ID number, Zip Code, phone number and others, this property is localizable. Stories: As a form creator, I want to create masks for my integer numeric type fields and As a form user, I want to see the masks created for integer numeric type fields  Platform Improvements Application Security Core Infra Util HashMapBuilder & HashMapDictionaryBuilder API to putAll a dictionary (as a Developer) This feature exists to spare devs from converting dictionaries to/from map. In our code base, we have quite a few places that use legacy java dictionaries because of OSGi, many of them require conversion to/from java map. Creating an unified api would help reduce manual conversion burden. Devs would simply call the api to generate Map from dictionary, or dictionary from map. Story: https://issues.liferay.com/browse/LPS-132251 OpenID Connect improvements OpenID Connect requests go through the configured proxy The OpenID Connect requests did not go through proxy which was a product limitation. We implemented that by using the configured proxy system properties. Story: https://issues.liferay.com/browse/LPS-134144 There is no special documentation needed. See the description of the story for usage. SAML improvements (DXP-only) Making it possible to configure UUID to sync users with an IdP (as an Instance Administrator). Additional features: SAML assertion attribute mapping to screenName even if it differs from NameID (as an Instance Administrator) Better configuration of the SAML adapter to create and update users in the portal user database using info from SAML assertion (as an Instance Administrator)   This feature exists to: Embrace the SAML spec's intention for the various NameId Formats. Especially "persistent". Allow the IDP to send different NameIDs / formats at different times, for the same SP user. The alternative would be to fail the SSO attempt, because the SAML spec does not state that a user should only be identifiable by one NameId! Maintain such identifiers in the context of the SP + IDP relationship. This will hereafter be described as a "binding". Allow to nominate a SAML assertion attribute to be used for matching users more globally. This means the admin can configure the name of the expected attribute and what SP user related field it should be matched with to log in a user. Maintain full backwards compatibility for DXP customers. Meaning the existing behavior is maintained until they decide to re-configure their SAML integrations, and we do not hijack the SP user's UUID field for a different purpose than what they may currently have for it. Control better the model the scope & tracking of identifiers used for matching SP users. Improve this situation by "binding" a NameId to a SP user after the "emailAddress" is matched at least once. It was a recurrent feedback that when "emailAddress" is used, then when the user's email address gets changed on the IDP, this breaks SSO for the user. Now the bindings are always preferred/checked before user matching is conducted, so the user will keep its ability to log in. Additionally the “emailAddress2 can then be automatically corrected via SAML attribute mappings (if desirable). Stories: https://issues.liferay.com/browse/LPS-123218​​​​​​​ https://issues.liferay.com/browse/LPS-105170 ​​​​​​​https://issues.liferay.com/browse/LPS-105169 ​​​​​​​https://issues.liferay.com/browse/LPS-125272 Get rid of saml-hook (as an Instance Administrator) We want to remove legacy frameworks from saml modules. This particular one is about removing saml-hook. Story: https://issues.liferay.com/browse/LPS-88439 Audit log improvements (DXP-only) Audit info in the regular log entries and Configure that if the audit info is written to the regular log entries (as a System Administrator) In SaaS environment, the current log framework can not differentiate logs of one instance from those of others. In fact, even if we have only one instance of Liferay, it gets difficult to tell logs of one request from those of others if there are multiple requests incoming in a short amount of period. With these feature, it should be easy to tell logs of certain request/instance. It is needed for Portal admins and developers who need to debug. It is solve the difficulty to tell logs. Stories: https://issues.liferay.com/browse/LPS-133646 https://issues.liferay.com/browse/LPS-133653​​​​​​​ Headless APIs Expose versions and create drafts of structured contents from APIs Now it is possible to access the different versions of a structured content as well as delete specific versions. Additionally, we have added the possibility to create draft versions with a dedicated endpoint. LPS-98647 Documentation All documentation for Liferay Portal and Liferay Commerce can now be found on our documentation site: learn.liferay.com.  For more information on upgrading to Liferay Portal 7.4 GA3 see refer to the Upgrade Overview. Compatibility Matrix 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).  Liferay Portal 7.4 GA3 and Liferay Commerce 4.0 GA3 were tested extensively for use with the following Application/Database Servers:  Application Server Tomcat 9.0 Wildfly 17.0 Database HSQLDB 2 (only for demonstration, development, and testing) MySQL 5.7, 8.0 MariaDB 10.2, 10.4 PostgreSQL 12.x, 13.x JDK IBM J9 JDK 8 Oracle JDK 8 Oracle JDK 11 All Java Technical Compatibility Kit (TCK) compliant builds of Java 11 and Java 8 Source Code Source is available as a zip archive on the release page, or on its home on GitHub. If you're interested in contributing, take a look at our contribution page. Bug Reporting If you believe you have encountered a bug in the new release you can report your issue by following the bug reporting instructions. Getting Support Support is provided by our awesome community. Please visit helping a developer page for more details on how you can receive support. Fixes and Known Issues Fixes List of known issues [Less]
Posted about 4 years ago by Roselaine Marques
NOTICE: This blog  post has Spanish and English version. Cuando empieces a crear un sitio web de comercio electrónico con Liferay Commerce, es posible que te enfrentes a un requisito muy común del cliente: la personalización del "Mini Carrito de ... [More] compra", AKA el mini-cart taglib. Para empezar a hablar sobre el tema, por si aún no lo conocías, existe un Widget de Mini-Cart y una de las posibilidades para personalizarlo es utilizar una Widget Display Template para crear su propio estilo personalizado completo.  Pero supón que el requisito de cambio es un cambio pequeño, porque gusta la estructura que tiene la taglib del Commerce y solo se quiere añadir o quitar algún pequeño detalle. En este caso tener que crear una estructura y un estilo completos con Freemarker es mucho trabajo para el poco cambio que hay que hacer en el mini-cart OOTB. Así que tú, como buen desarrollador que eres, comienzas una búsqueda para ver cómo quitar alguna parte del carrito existente en Liferay Commerce (por supuesto, porque no queremos reinventar la rueda, sólo queremos agregar una pequeña pieza u ocultar alguna pieza existente en este carrito).  Pero para tu sorpresa, no existe una "manera estándar de Liferay" de la cual estamos acostumbrados, para customizarlo. No es una JSP con la cual nos sintamos cómodos y sepamos cómo manejar. Es un Taglib construido con archivos React ... Bien..que no cunda el pánico, aquí tengo la receta paso a paso de cómo hacerlo. En primer lugar… Abre en un navegador tu Liferay Portal y haz clic en > Panel de control > Sitios > haz clic para agregar un nuevo sitio y seleccione una plantilla SpeedWell, para crear un nuevo sitio de comercio electrónico para nuestra prueba con el mini carrito. Ve a la página del Catálogo y añade algunos productos al carrito. En la esquina superior derecha puede ver el mini carrito con el nuevo producto agregado. Ahí está la pieza con la que aprenderás a personalizar en esta publicación de blog. Iniciando nuestra jornada, conozcamos un poco el código. Es muy útil conocer y comprender los taglibs de Liferay Commerce, aquí hay un enlace para ayudarlo:: https://github.com/liferay/liferay-portal/blob/7.4.1-ga2/modules/apps/commerce/commerce-frontend-taglib/src/main/resources/META-INF/resources Es bueno saber: cuando veas el código de la taglib del mini-cart, notarás que se puede hacer la llamada a un componente de React en tu Módulo Liferay, simplemente usando   Estos taglibs fueron construidos con React Components, y aquí tenemos los Componentes que son y pueden ser utilizados: https://github.com/liferay/liferay-portal/tree/7.4.1-ga2/modules/apps/commerce/commerce-frontend-js/src/main/resources/META-INF/resources/components   Así que, comencemos a crear nuestro taglib que sobreescribirá el mini-cart OOTB. 1. Como siempre me gusta utilizar blade y comenzar con liferay-workspace nuevo:      blade init -v portal-7.4-ga2 liferay-workspace-7-4 2. Para este tutorial, con blade, crea un módulo con el template “api” dentro del ${liferay-workspace-}/modules, con el siguiente comando:      blade create -t api commerce-frontend-taglib-custom   3. Dentro de tu módulo, elimina el paquete creado por defecto y crea estas dos clases Java para la taglib:      a. com.liferay.commerce.frontend.taglib.custom.internal.servlet.ServletContextUtil              * Dentro de tu clase creada, copia y pega el siguiente código: package com.liferay.commerce.frontend.taglib.custom.internal.servlet; import org.osgi.service.component.annotations.Reference; import javax.servlet.ServletContext; import org.osgi.service.component.annotations.Component; /** * @author Roselaine Marques */ @Component(immediate = true, service = {}) public class ServletContextUtil {   private static ServletContext _servletContext;   @Reference(           target = "(osgi.web.symbolicname=commerce.frontend.taglib.custom)", unbind = "-"   )   protected void setServletContext(ServletContext servletContext) {       _servletContext = servletContext;   }   public static ServletContext getServletContext() {       return _servletContext;   } }   b. com.liferay.commerce.frontend.taglib.custom.CommerceFrontendTaglibCustom        *Dentro de tu clase creada, copia y pega el siguiente código: package com.liferay.commerce.frontend.taglib.custom; import com.liferay.taglib.util.IncludeTag; import com.liferay.commerce.frontend.taglib.custom.internal.servlet.ServletContextUtil; import javax.servlet.jsp.PageContext; /** * @author Roselaine Marques */ public class CommerceFrontendTaglibCustom extends IncludeTag {   @Override   public void setPageContext(PageContext pageContext) {       super.setPageContext(pageContext);       setServletContext(ServletContextUtil.getServletContext());   }   @Override   protected String getPage() {       return _PAGE;   }   private static final String _PAGE = "/mini-cart/page.jsp"; }   4. Abre el fichero bnd.bnd y añade la siguiente configuración: Bundle-Name: Commerce Frontend Taglib Custom Bundle-SymbolicName: commerce.frontend.taglib.custom Bundle-Version: 1.0.0 Export-Package: com.liferay.commerce.frontend.taglib.custom Provide-Capability:\   osgi.extender;\     osgi.extender="jsp.taglib";\     uri="http://liferay.com/tld/commerce-ui-custom";\     version:Version="${Bundle-Version}" Web-ContextPath: /commerce-frontend-taglib-custom   NOTA: El objetivo principal aquí es saber cómo personalizar una taglib OOTB del Commerce hecha con React. Estos dos pasos anteriores son solo sobre cómo crear un taglib simple y común, y como no quiero ser aburrida, en lo que se trata del taglib no entraré en demasiados detalles. 5) Ahora en “commerce-frontend-taglib-custom/src/main/resources”, elimina todo lo que hay dentro y crea la siguiente estructura de directorios y archivos:              └── META-INF                 ├── liferay-commerce-ui-custom.tld                 ├── resources                 │   ├── init.jsp                 │   ├── js                 │   │   └── OverriddenCartItem.js                 │   └── mini-cart                 │       └── page.jsp                 └── taglib-mappings.properties       a. En el fichero liferay-commerce-ui-custom.tld agrega el siguiente código:       version="2.1"       xmlns="http://java.sun.com/xml/ns/javaee"       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"       xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-jsptaglibrary_2_1.xsd" >   Provides the Liferay Commerce MiniCart component tags, prefixed with commerce-ui-custom:]]>.   1.0   liferay-commerce-ui-custom   http://liferay.com/tld/commerce-ui-custom         mini-cart       com.liferay.commerce.frontend.taglib.custom.CommerceFrontendTaglibCustom       JSP          b. En el fichero init.jsp agrega el siguiente código: <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> <%@ taglib uri="http://java.sun.com/portlet_2_0" prefix="portlet" %> <%@ taglib uri="http://liferay.com/tld/aui" prefix="aui" %> <%@ taglib uri="http://liferay.com/tld/frontend" prefix="liferay-frontend" %> <%@ taglib uri="http://liferay.com/tld/theme" prefix="liferay-theme" %>       c. En el fichero taglib-mappings.properties agrega el siguiente código: liferay_commerce_ui_custom=/META-INF/liferay-commerce-ui-custom.tld   NOTA: Ahora llegamos al punto principal de este tutorial. Veamos cómo "sobreescribir" una Taglib del Liferay Commerce hecha con React. En este caso, queremos personalizar el mini-carrito, por lo que en el archivo commerce-frontend-js/src/main/resources/META-INF/resources/components/mini_cart/util/views.js es donde se encuentran los archivos de View para reemplazar, del componente original:     6. Como ejemplo, reemplazamos el componente CartItem original con nuestro OverriddenCartItem.js. Ve a CartItem.js, copia todo el código dentro, pégalo en tu archivo OverriddenCartItem.js y guárdalo.      a. Ahora, cambia la ruta de los import relativa a absoluta, como en el siguiente código: import {PRODUCT_REMOVED_FROM_CART} from 'commerce-frontend-js/utilities/eventsDefinitions'; import Price from 'commerce-frontend-js/components/price/Price'; import QuantitySelector from 'commerce-frontend-js/components/quantity_selector/QuantitySelector'; import ItemInfoView from 'commerce-frontend-js/components/mini_cart/CartItemViews/ItemInfoView'; import MiniCartContext from 'commerce-frontend-js/components/mini_cart/MiniCartContext'; import {   INITIAL_ITEM_STATE,   REMOVAL_CANCELING_TIMEOUT,   REMOVAL_ERRORS_TIMEOUT,   REMOVAL_TIMEOUT, } from 'commerce-frontend-js/components/mini_cart/util/constants'; import {parseOptions} from 'commerce-frontend-js/components/mini_cart/util/index';         b. Busca este bloque de código y cámbialo por un hardcode, para probarlo si está obteniendo nuestro componente JS, como se muestra a continuación:   childItems={childItems}   name={name}   options={options}   sku="[SKU hardcoded]" />       c. Finalmente, busca en el código el nombre "CartItem" y reemplaza todo por "OverriddenCartItem". Ojo: Aquí escribí "sobreescribir" entre comillas, porque de hecho no estamos sobreescribiendo el archivo. En realidad, estamos enviando otro archivo para una "posición" de destino. Quedará completamente claro al final de este tutorial de blog. Como mencioné anteriormente, "sobreescribiremos un archivo" de la taglib original de Liferay Commerce. Ten en cuenta que tendremos que declarar dicha taglib en un page.jsp pasando el archivo OverriddenCartItem.js para reemplazar el componente CartItem.js. 7. Abre tu archivo page.jsp desde su módulo y agrega el siguiente código: <%@ include file="../init.jsp" %> <%@ page import="com.liferay.portal.kernel.util.HashMapBuilder" %> <%@ taglib prefix="liferay-commerce-ui" uri="http://liferay.com/tld/commerce-ui" %> " />   NOTA: Como se puede ver arriba, estamos declarando un taglib como de costumbre, la única diferencia aquí es el parámetro "views" declarado. Aquí es donde tenemos un punto de extensión entre el archivo taglib original y nuestro archivo. Agregamos la clave “Item” porque queremos reemplazar este componente por el nuevo (como he enseñado arriba en view.js del mini_cart, el componente CartItem está asociado con la clave Item). Agregamos el valor “[email protected]/js/OverriddenCartItem” que será nuestro archivo personalizado. Ojo: Este valor agregado es la ruta del archivo después de compilado, compuesto por: “bundle-name” + “version-of-module” + “folder-with-js-file” + “your-file” 8. En la carpeta raíz de su proyecto, crea más tres archivos:    a) .npmbundlerrc Aquí declaramos las dependencias de importación necesarias para nuestro componente, así que copia y pega el siguiente código: {   "config": {     "imports": {         "commerce-frontend-js": {           "/": ">=4.0.0"         },         "frontend-js-web": {           "/": ">=3.0.0"         },         "frontend-taglib-clay": {           "@clayui/icon": ">=3.1.0",               "@clayui/button": ">=3.6.0"         },         "@liferay/frontend-js-react-web": {                 "/": ">=1.0.0",                 "classnames": ">=2.2.6",                 "formik": ">=1.4.3",                 "prop-types": ">=15.7.2",                 "react": ">=16.8.6",                 "react-dnd": ">=7.0.2",                 "react-dnd-html5-backend": ">=7.0.2",                 "react-dom": ">=16.8.6"         }     }   } }     b) Package.json con el siguiente código (el "nombre" aquí debe ser el mismo que el nombre del módulo de su proyecto): { "dependencies": {}, "devDependencies": {   "@babel/cli": "^7.7.5",   "@babel/core": "^7.7.5",   "@babel/preset-env": "^7.7.6",   "@babel/preset-react": "^7.7.4",   "liferay-npm-bridge-generator": "2.26.0",   "liferay-npm-bundler": "2.24.3" }, "name": "commerce-frontend-taglib-custom", "scripts": {   "build": "babel --source-maps -d build/resources/main/META-INF/resources src/main/resources/META-INF/resources && liferay-npm-bundler && liferay-npm-bridge-generator --verbose" }, "version": "1.0.0" }      c) .babelrc con el siguiente código: { "presets": ["@babel/preset-env", "@babel/preset-react"] }   8. Ahora en tu tema podrás utilizar tu TagLib personalizada a partir de la macro liferay_commerce_ui_custom, según el ejemplo a continuación:   <@liferay_commerce_ui_custom["mini-cart"] />   Como resultado, deberías ver tu mini-cart con tu personalización, como en la imagen a continuación:   Ojo: Para probar la TagLib, hice una copia del tema SpeedWell de Liferay y lo asigne al sitio creado con el comercio. Puedes encontrar este código de ejemplo explicado aquí en mi repositorio.   Hay una known issue de cuando se vuelve a hacer el deploy la taglib y está siendo utilizada en un Freemarker con Liferay CE 7.4 GA2, que se puede resolver reiniciando el servidor. Por esta razón, si estás haciendo muchos cambios y desea evitar reiniciar el servidor, use el taglib en algún portlet como lo he creado aquí, y una vez que haya preparado su taglib, puede configurarlo en tu tema o WDT. [Less]
Posted about 4 years ago by Roselaine Marques
NOTICE: This blog  post has Spanish and English version. When you start to build an e-commerce website with Liferay Commerce, you might be faced with a requirement from a customer to personalize the “Mini Shopping Cart”.  You can use a Widget ... [More] Display Template to create your own entire custom style and apply it to the “Mini Cart” module. But suppose you want to make just a minor change , because you like the mini-car commerce taglib styles and you don’t want to have to create an entire structure and style with a freemarker. So... you start to look at how to override some part of the existing cart in Liferay Commerce (of course because we don’t want to reinvent the wheel, we only want to maybe add some small piece, or hide some existing part in this cart). But to your surprise, there is not a “standardized Liferay way” to override it. It is not a JSP which we are comfortable with and know how to handle. It is a Taglib built with React files…  Don't panic, here I have the step-by-step recipe on how to do it.  First of all…  Go to your Liferay Portal > Control Panel > Sites > click to add a new site and select a SpeedWell Template, to create a new e-commerce Site for our test with the mini cart.    Go to the Catalog page and add some products to the cart. At the top right hand corner you can see the mini cart with the new product added. Here is the piece with which you’ll learn how to customize in this blog post. So, let's get to know the code a little bit. It’s useful to know and understand the taglibs from Liferay Commerce, here is a link to help you: https://github.com/liferay/liferay-portal/blob/7.4.1-ga2/modules/apps/commerce/commerce-frontend-taglib/src/main/resources/META-INF/resources Good to know: when you see the mini-cart taglib code, you will notice that it is called a react component in your Liferay Module, simply using the   So, the taglibs we saw were built with React Components, and here we have the Components used: https://github.com/liferay/liferay-portal/tree/7.4.1-ga2/modules/apps/commerce/commerce-frontend-js/src/main/resources/META-INF/resources/components   So lets start to create our taglib overriding the mini shopping cart OOTB.  1. I always like to use the blade and starting with a liferay-workspace:      blade init -v portal-7.4-ga2 liferay-workspace-7-4 2. For this test create a API module inside your liferay-workspace, with the following command:      blade create -t api commerce-frontend-taglib-custom   3. Inside your module, delete the package created by default and create these two java classes for our taglib:      a. com.liferay.commerce.frontend.taglib.custom.internal.servlet.ServletContextUtil              * Inside copy and paste the following code package com.liferay.commerce.frontend.taglib.custom.internal.servlet; import org.osgi.service.component.annotations.Reference; import javax.servlet.ServletContext; import org.osgi.service.component.annotations.Component; /** * @author Roselaine Marques */ @Component(immediate = true, service = {}) public class ServletContextUtil {   private static ServletContext _servletContext;   @Reference(           target = "(osgi.web.symbolicname=commerce.frontend.taglib.custom)", unbind = "-"   )   protected void setServletContext(ServletContext servletContext) {       _servletContext = servletContext;   }   public static ServletContext getServletContext() {       return _servletContext;   } }   b. com.liferay.commerce.frontend.taglib.custom.CommerceFrontendTaglibCustom        *Inside copy and paste the following code package com.liferay.commerce.frontend.taglib.custom; import com.liferay.taglib.util.IncludeTag; import com.liferay.commerce.frontend.taglib.custom.internal.servlet.ServletContextUtil; import javax.servlet.jsp.PageContext; /** * @author Roselaine Marques */ public class CommerceFrontendTaglibCustom extends IncludeTag {   @Override   public void setPageContext(PageContext pageContext) {       super.setPageContext(pageContext);       setServletContext(ServletContextUtil.getServletContext());   }   @Override   protected String getPage() {       return _PAGE;   }   private static final String _PAGE = "/mini-cart/page.jsp"; }   4. Open the bnd.bnd file and add the following configuration Bundle-Name: Commerce Frontend Taglib Custom Bundle-SymbolicName: commerce.frontend.taglib.custom Bundle-Version: 1.0.0 Export-Package: com.liferay.commerce.frontend.taglib.custom Provide-Capability:\   osgi.extender;\     osgi.extender="jsp.taglib";\     uri="http://liferay.com/tld/commerce-ui-custom";\     version:Version="${Bundle-Version}" Web-ContextPath: /commerce-frontend-taglib-custom   NOTE: The main target here is to know how to customize a React taglib from commerce OOTB. These two previous steps are only about how to create a simple and common taglib, I don’t want to bore you by going into too much detail. 5) Now in “commerce-frontend-taglib-custom/src/main/resources”, eliminate everything inside and create the following structure of directories and files:                └── META-INF                 ├── liferay-commerce-ui-custom.tld                 ├── resources                 │   ├── init.jsp                 │   ├── js                 │   │   └── OverriddenCartItem.js                 │   └── mini-cart                 │       └── page.jsp                 └── taglib-mappings.properties       a. In the liferay-commerce-ui-custom.tld add the following code:       version="2.1"       xmlns="http://java.sun.com/xml/ns/javaee"       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"       xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-jsptaglibrary_2_1.xsd" >   Provides the Liferay Commerce MiniCart component tags, prefixed with commerce-ui-custom:]]>.   1.0   liferay-commerce-ui-custom   http://liferay.com/tld/commerce-ui-custom         mini-cart       com.liferay.commerce.frontend.taglib.custom.CommerceFrontendTaglibCustom       JSP          b. In the init.jsp add the following code: <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> <%@ taglib uri="http://java.sun.com/portlet_2_0" prefix="portlet" %> <%@ taglib uri="http://liferay.com/tld/aui" prefix="aui" %> <%@ taglib uri="http://liferay.com/tld/frontend" prefix="liferay-frontend" %> <%@ taglib uri="http://liferay.com/tld/theme" prefix="liferay-theme" %>       c. In the taglib-mappings.properties file add the following code: liferay_commerce_ui_custom=/META-INF/liferay-commerce-ui-custom.tld   NOTE: Now we arrive at the main point of this blog post. Let's see how to “override” a React Commerce Taglib in Liferay. We want to customize the mini-cart, so in commerce-frontend-js/src/main/resources/META-INF/resources/components/mini_cart/util/views.js file is where the View files to replace from the original component are:      6. As an example, let's replace the original CartItem component with our custom OverriddenCartItem.js. Go to the CartItem.js copy all the code inside and paste it in your OverriddenCartItem.js file and save it.      a. Now, change the relative imports path to absolute, as in the code below: import {PRODUCT_REMOVED_FROM_CART} from 'commerce-frontend-js/utilities/eventsDefinitions'; import Price from 'commerce-frontend-js/components/price/Price'; import QuantitySelector from 'commerce-frontend-js/components/quantity_selector/QuantitySelector'; import ItemInfoView from 'commerce-frontend-js/components/mini_cart/CartItemViews/ItemInfoView'; import MiniCartContext from 'commerce-frontend-js/components/mini_cart/MiniCartContext'; import {   INITIAL_ITEM_STATE,   REMOVAL_CANCELING_TIMEOUT,   REMOVAL_ERRORS_TIMEOUT,   REMOVAL_TIMEOUT, } from 'commerce-frontend-js/components/mini_cart/util/constants'; import {parseOptions} from 'commerce-frontend-js/components/mini_cart/util/index';         b. Search for this block code and change it for a hardcode to test it if is getting our JS override file, as below:   childItems={childItems}   name={name}   options={options}   sku="[SKU hardcoded]" />       c. Finally, search in the code for the name “CartItem” and replace everything to “OverriddenCartItem”. PS.: Here I wrote “override” in quotation marks, because in fact you are not overriding the file, you are sending another file for one target “position”. It will be completely clear at the end of this blog tutorial.  As mentioned before, we will “override one file” from the original commerce taglib. Keep in mind that you will declare this taglib in the page.jsp passing the OverriddenCartItem.js file to replace the CartItem.js component. 7. Open your page.jsp file from your module and add the following code: <%@ include file="../init.jsp" %> <%@ page import="com.liferay.portal.kernel.util.HashMapBuilder" %> <%@ taglib prefix="liferay-commerce-ui" uri="http://liferay.com/tld/commerce-ui" %> " />   NOTE: As you can see above, we are declaring a taglib as usual, the only difference here is the parameter “views” delecared. Here is where we have a point of extension between the original taglib file to our view component file. We add the key “Item” because we want to replace this component for the new one (as I show above in the view.js from the mini_cart the CartItem is associated with the key Item).  We add the value “[email protected]/js/OverriddenCartItem” which will be our custom file. PS: This value added is the path of the file after compiled, compose by:  “bundle-name” + “version-of-module” + “folder-with-js-file” + “your-file” 8. In your root project folder, create three more files:    a) .npmbundlerrc Here we declare the import dependencies needed for our component, so copy and paste the following code: {   "config": {     "imports": {         "commerce-frontend-js": {           "/": ">=4.0.0"         },         "frontend-js-web": {           "/": ">=3.0.0"         },         "frontend-taglib-clay": {           "@clayui/icon": ">=3.1.0",               "@clayui/button": ">=3.6.0"         },         "@liferay/frontend-js-react-web": {                 "/": ">=1.0.0",                 "classnames": ">=2.2.6",                 "formik": ">=1.4.3",                 "prop-types": ">=15.7.2",                 "react": ">=16.8.6",                 "react-dnd": ">=7.0.2",                 "react-dnd-html5-backend": ">=7.0.2",                 "react-dom": ">=16.8.6"         }     }   } }     b) Package.json with the following code (the “name” here should be the same as your project module name): { "dependencies": {}, "devDependencies": {   "@babel/cli": "^7.7.5",   "@babel/core": "^7.7.5",   "@babel/preset-env": "^7.7.6",   "@babel/preset-react": "^7.7.4",   "liferay-npm-bridge-generator": "2.26.0",   "liferay-npm-bundler": "2.24.3" }, "name": "commerce-frontend-taglib-custom", "scripts": {   "build": "babel --source-maps -d build/resources/main/META-INF/resources src/main/resources/META-INF/resources && liferay-npm-bundler && liferay-npm-bridge-generator --verbose" }, "version": "1.0.0" }      c) .babelrc with the following code: { "presets": ["@babel/preset-env", "@babel/preset-react"] }   8. Now in your theme you can use your custom TagLib using a macro liferay_commerce_ui_custom. See example below:   <@liferay_commerce_ui_custom["mini-cart"] />   As the result, you should see your mini-cart overridden, as in the image below:   PS: To test the TagLib, I made a copy of the SpeedWell theme from Liferay and assigned it  to the site created with commerce. You can find this example code explained here in my repo.   There is a known issue when you redeploy your taglib and it’s used in a Freemarker with the Liferay CE 7.4 GA2, can be solved by restarting the server. For this reason If you are doing a lot of changes and want to avoid restarting the server, use the taglib in some portlet as I’ve created here, and after you get ready your taglib you can setup in your Theme or Widget Display Template.    [Less]
Posted about 4 years ago by David H Nebinger
Introduction A client asked me to build a React portlet for them and I was like, "Oh Yeah!" I mean, it's been a while since I built a React portlet and tooling has really changed a lot since then, so I was really looking forward to this new task. ... [More] So without further ado, I'm just going to dive right in... Creating the Module Okay, so this is still Liferay and still OSGi, so we're going to be building a module. You have basically two options: 1. Use the Yeoman generator-liferay-js generator. This is the path to take when you are not already using a Liferay Gradle Workspace. After you install it, use the yo liferay-js command to start the generator. If this is the route you want to take, first install yo and the generator using the command npm install -g yo generator-liferay-js and then you can use the yo liferay-js command. 2. Use Blade to create a liferay-js module. This actually uses the same Yeoman generator, but it will create your module with the necessary stuff to allow the Gradle workspace to build your module. Command to create the module is blade create -t js-widget my-module-name. If you have Blade installed, you don't need anything more. There's actually two other legacy options that I'll mention but not go further with. The first is another Yeoman generator, the generator-liferay-bundle. This was prevalent at some point (that I think I missed entirely), but now it is deprecated. Also, in Blade there is the npm-react-portlet module type which actually builds a larger Java portlet around the launch of the React code. This can be handy if you need something like defined portlet permissions. I feel like this one is going to be a larger by-hand responsibility for building the React code than using the standard React tooling, but I don't know this for certain. Deciding between which of the options to go with comes down to an answer to a simple question: Are you also using a Liferay Gradle Workspace? If yes, go with #2 as you can include your React portlet build in with your other customizations. If no, go with #1 and you can build your project separately. And if you're a Maven person? Well, um, yeah... For you I'd probably try to do option #1 on its own, if possible. Maven is capable of invoking npm to do a build, though, so it should be possible to use #1 to create a module in your Maven workspace and then add some pom magic to complete the build, but I'm afraid you're on your own there. If someone wants to share a pom in the comments, that would be awesome! Invoking the liferay-js Generator Regardless which option you pick, it will always end up running the liferay-js generator. The generator is going to prompt you a number of times for values, this is what you'll see: ? What type of project do you want to create? React Widget ? What name shall I give to the folder hosting your project? modules/my-module ? What is the human readable description of your project? My React Portlet ? Do you want to add localization support? Yes ? Do you want to add configuration support? Needs Liferay DXP/Portal CE 7.1 with JS Portlet Extender 1.1.0 or Liferay DXP/Portal CE 7.2+. Yes ? Under which category should your widget be listed? category.react ? Do you have a local installation of Liferay for development? Yes ? Where is your local installation of Liferay placed? ~/liferay/react/bundles ? Do you want to generate sample code? Yes The first prompt is for the JS portlet type. You can pick JavaScript, Angular, React, Vue.js or Metal.js. Obviously I chose React for my portlet. Next is the folder where your code will be placed. If outside of a workspace, you can pick anything, but in the workspace it will use the modules directory and the my-module name which you specified on the blade command line. The human readable description is really just your friendly portlet name. You have the option of enabling Localization for your portlet. Most of the time I opt to support localization. The next option is to enable configuration support. I was a bit thrown when I saw this prompt the first time because you answer underneath the note, I didn't realize it was connected to the question above. I like configuration options, so I chose yes. The note just tells you the minimum requirements for config support, so you can't use this in 7.0 or 7.1 without the JS portlet extender. The next prompt is for the category where your portlet will appear in the widgets menu. "category.name" is standard and can then be handled with localization. Next two questions are about your local dev bundle. If you are not using the workspace, you'll need to pick your path. If you are using the workspace, it assumes you're going to use the workspace bundle, but you can change it if necessary. Finally, you can generate the sample code in your new portlet. Actually I'd suggest always saying yes to this prompt as it starts your localization and config for you, and basically lays a solid foundation for your portlet without introducing any real cruft. After this last question, the generator will create your new module folder. Anatomy of the Liferay JS React Portlet After the code has been generated, you'll have this structure: $ tree my-module my-module ├── README.md ├── assets │ └── css │ └── styles.css ├── build.gradle ├── config.json ├── features │ ├── configuration.json │ └── localization │ └── Language.properties ├── package.json └── src ├── AppComponent.js └── index.js 5 directories, 8 files This is even with the sample data thrown in! The README.MD is just what you think it is. It starts out being blank, but I love to use this to add documentation about my react portlet. For example, the one I did for the client had some SB remote services thrown in, so in the README I included the code for calling the right services and the kinds of JSON I got back as a result. It can be a handy place to keep notes like this. The assets folder will include hard assets like css files, images, etc. build.gradle I got because I'm in a workspace, but it is empty if you check it. config.json is configuration for the module itself. If you check it you'll find it includes module and build details. The features folder holds the config and/or localization stuff. Localization is the standard Liferay resource bundle stuff, just know that it will be copied into the right spot at build time to make all of your localization strings work as expected. The configuration.json I'm going to cover in more detail in a coming section. The package.json is the file every node developer recognizes. There are some things in here that are Liferay-specific, including all of the portlet properties we know from the portlet.xml and liferay-portlet.xml from the past or Java portlet @Component annotations from the present. I wouldn't tamper with sections in here you don't understand, otherwise you could foul your build. And finally, the src folder is where your React source is going to go. Configuration.json File I wanted to call this one out because honestly I couldn't find details on it anywhere else... Javascript portlets like the React one I'm building can now access configuration properties from the Liferay system scope (i.e. global System Settings control panel) or portlet instance scope (the configuration panel from a typical Liferay portlet). Pretty cool, huh? By defining your configuration in the configuration.json file, the Liferay UI will present your configuration dialog in the two places (System Settings or the portlet configuration dialog) for gathering config details, plus it will pass those details off to the portlet so you can leverage the configuration values in the portlet. The file is defined by a JS schema, specifically this one: https://raw.githubusercontent.com/liferay/liferay-js-toolkit/master/resources/schemas/configuration.schema.json. It is kind of hard to read if you haven't seen one before, so I'll break it down here... There is a useful reference page here: https://github.com/liferay/liferay-frontend-projects/blob/master/maintenance/projects/js-toolkit/docs/configuration.json-file-reference.md There are two primary objects, the system object and the portletInstance object. Each contain the config items for the target scopes, either the System Settings or the portlet configuration dialog. For the system object, there are two attributes, category and name which will be the category and name you'll see in the System Settings page. The portletInstance object does not need these attributes. The fields object is part of both sections and contains the configuration key/definition pairs. The key is the configuration field name, it is what you'll reference in your JS code to get the value of the config item. The definition is an object that contains a lot of items: type is the configuration type and can be one of number, float, string or boolean for both system and portletInstance items, but system can have an additional password type that portletInstance cannot. Before you ask, the answer is No, there is no way to add additional types to the configuration. Name and description are either the human readable values or the language bundle keys for the labels that will appear in the UI. default is the default value for the config item in case it has not been set. Be sure to use a value consistent with the type. options is an object of key/label pairs used when displaying a pick list. The key will be what the value of the configuration item is, and the label is what the user will see (either human readable or language bundle key work here). You only want to use the options when you are limiting to a list of items for the user to pick from; if you leave it off, they will see a basic text entry field. Your React components will have access to the configuration through the props.configuration object. So the sample data gives you two configuration fields, one "fruit" and one "drink". You could initialize your state using these values: state = { favoriteFruit: props.configuration.system.fruit, favoriteBeverage: props.configuration.portletInstance.drink }; You can also reference them in the props argument on your constructor if you have one. Using Clay Components We all love a good component library, right? For Liferay, one component library you might want to pick is ClayUI. Why? Well consistency is a good reason, right? Liferay as a whole is leveraging Clay all over the place, so if you use the Clay components, your portlet should be visually consistent with the rest of Liferay. ClayUI is Liferay's component suite that is used for all of the out of the box portlets and can also be used in your React portlets. See the whole suite here: https://clayui.com/ So how do you add the Clay components to your project? It's pretty easy actually... First, you want to find the list of components here: https://clayui.com/docs/components/index.html. When you find the component you want, have npm install it like I've done here for the ClayUI Table component: $ npm install @clayui/table Now if you see a warning (like you would for table) about not finding the peer ClayUI CSS component, you can just ignore that. And don't try to install the ClayUI CSS component either. The ClayUI CSS is already going to be loaded in as part of the theme. Because ClayUI CSS is part of the theme, this can pose a problem when rendering ClayUI components. The install command I've provided is going to try to use the latest version of the ClayUI component, but there could be something missing because the theme would have an older version of the ClayUI CSS component. If you find this happening with a ClayUI component you're using, try to use an older version until you get one that is inline with the ClayUI CSS in the theme. You can selectively add ClayUI components to your module, or you can just add them all using the following: $ npm install @clayui/alert @clayui/autocomplete @clayui/badge @clayui/breadcrumb \ @clayui/button @clayui/card @clayui/charts @clayui/color-picker \ @clayui/data-provider @clayui/date-picker @clayui/drop-down @clayui/empty-state \ @clayui/form @clayui/icon @clayui/label @clayui/link @clayui/list \ @clayui/loading-indicator @clayui/management-toolbar @clayui/modal \ @clayui/multi-select @clayui/multi-step-nav @clayui/nav @clayui/navigation-bar \ @clayui/pagination @clayui/pagination-bar @clayui/panel @clayui/popover \ @clayui/progress-bar @clayui/shared @clayui/slider @clayui/sticker @clayui/table \ @clayui/tabs @clayui/time-picker @clayui/tooltip @clayui/upper-toolbar With these pulled in, you can then start importing components into your React files: import ClayTable from "@clayui/table"; import Checkbox from "@clayui/form"; And also using them in your render() methods: render() { return ( ... The ClayUI site should have all of the details you need to leverage the components effectively in your React portlets. Liferay JavaScript Object Liferay has a suite of Javascript objects and services at your disposal, even if you are using React. I spoke of some remote SB services I had created and used as part of one of my projects, for example. The way to invoke these kinds of services (and you'll find examples like this scattered throughout the Liferay OOTB portlets), is via code like: _getEntries(callback) { var instance = this; Liferay.Service( '/assettag/get-groups-tags', { groupIds: instance.get('groupIds'), }, callback ); } You can still do this in your React code, too! But wait, you might ask, why aren't you using OAuth2 Dave? Honestly, because I don't have to. I'm already authenticated into Liferay, the Liferay.Service() call is going to add my p_auth parameter so Liferay will verify I'm already logged in, plus I find I can easily code up a remote SB service easy enough. If I were doing a React SPA or exposing services for some other outside consumer, the new RESTBuilder stuff is the way to go. Liferay has lots of other JS available such as fire() and on() for InterPortlet Communication (IPC), that works even with React code. You can access Session object to extend the session, you can access the themeDisplay object, ... Anything that's there, you can call it just by using the Liferay container. Routing If you have a really simple React portlet, routing may not be necessary. Most of the time, though, you probably want to have routing so you can navigate within your portlet. When you do, you can't just use any Router implementation. When running under Liferay, Liferay owns the address bar, not your React portlet. Avoid BrowserRouter at all costs. Instead, opt for the HashRouter (puts your routing stuff after a hash # on the url, Liferay will ignore that) or even better the MemoryRouter. I always recommend the MemoryRouter because it is the best option since (a) Liferay owns the address bar and (b) two React portlets on the same page will trip over each other if they both use the HashRouter. The Liferay documentation, however, leans towards the HashRouter as it is the only address bar modification that Liferay supports. The caveat here is that you cannot have multiple JS-based portlets on the page each trying to use hash-based routing, they will clobber each other and break the routing for all. Using MemoryRouter is pretty simple, just start with the import: import React from 'react'; import { NavLink, Redirect, Route, MemoryRouter as Router, Switch } from 'react-router-dom'; With that in, you can build out your app with your navigation routes defined: import React from 'react'; import { NavLink, Redirect, Route, MemoryRouter as Router, Switch } from 'react-router-dom'; import EditValues from './pages/page1/Edit'; import Home from './pages/index'; import Page1 from './pages/page1/index'; import Page2 from './pages/page2/index'; import Page2Create from './pages/page2/Create'; class App extends React.Component { render() { return ( Home Page1 Page2 ); } } export default App; Just follow the regular React rules for routing and you're good to go. Unmounting the React Portlet This is an addition from information provided by my friend Julien Mourad: If you do not have SPA disabled (you'll know if you did disable SPA), it is important that you unmount your React portlet. With SPA enabled, Liferay does not do a full page refresh, it's doing AJAX calls back to Liferay and performing partial page updates, even when you navigate from page to page. With SPA enabled, your React portlet will never get unmounted automatically as the user navigates around on pages, you have to handle the unmounting manually. Fortunately that's pretty easy to do. In the index.js of your portlet, you're going to add 3 lines of code to the main() function. Here's how it looks for the sample code generated when the module was created: export default function main({portletNamespace, contextPath,   portletElementId, configuration}) { ReactDOM.render( , document.getElementById(portletElementId) ); // Liferay will issue the destroyPortlet event when the // portlet should unmount. Liferay.once('destroyPortlet', () => { ReactDOM.unmountComponentAtNode(container); }); } The last three lines are the ones that were added to trigger the unmount() call. Although these lines are not needed when SPA is disabled (the full page refresh will always ensure the browser memory has been reset on every page render), I would actually recommend just adding them to all of your React portlets. After all, you don't really know if an admin will come along and either enable or disable SPA without telling the developers. Adding these three lines will ensure that you're unmounted whether SPA is enabled or not. Building the React Portlet Building your React-based portlet couldn't be easier. If you're using the standalone yo liferay-js option, well you have an NPM project ready to create the portlet jar and you just use the npm run build command. If you're using the Liferay Gradle Workspace, just do your regular gradlew build and it does the rest. In both cases, you'll have two new folders in the root of your project. First is the build dir where all of the raw artifacts are in the appropriate locations. The second is the dist folder. In here, you'll find your React portlet jar file, ready to be plucked and sent off to a remote Liferay server for deployment. For your own development, you can use npm run deploy or gradlew deploy and your React portlet will be deployed to the local Tomcat bundle you pointed at when creating the module using the generator. Conclusion So this is it, really. I know, I know, there's not a lot of React in here, but there doesn't need to be. The info here gives you the framework to build the next great React portlet, but that's gotta come from you. Using the liferay-js generator you have a solid foundation, you just need to add inspiration and some React code. Thats all I have. If you have any React portlet tips you'd like to share, please add a comment below. If you have a question or comment, add those below too. If you find a mistake in what I wrote, keep it to yourself... Just kidding, I'd love your feedback below and will fix the content. Or of course you can find me in the community Slack and chat with me directly. [Less]
Posted about 4 years ago by David H Nebinger
Introduction So I was recently asked to help build a custom Liferay docker image for a client and there were some specific requirements such as: Should not contain hard-coded credentials, those will be available as environment variables. Should ... [More] be a single image that can be used in every environment, DEV, UAT and PROD. Now these kinds of things can be challenging to do if you were to say, pull down a Jenkins image and want to have the same kind of flexibility... But the Liferay Docker Base Images actually have a lot of functionality to them which I thought I'd share with you all... JVM Choice Yes, that's right, you do have a JVM choice for your image. You can use JDK 8 or 11, both are supported and also included in the image. You'll default to Zulu JDK 8, but if you use the environment variable JAVA_VERSION=zulu11 your environment will use Zulu JDK 11 instead. Portal-ext.properties If you've been using Liferay for a long time like I have, you know the portal-ext.properties file and how important it is for configuring Liferay. But did you know that there is an alternative to creating a portal-ext.properties file? There is, and it is based on environment variables. Liferay supports setting any property that you would define in portal-ext.properties via an environmental property. The format kind of predictable; the environment var must start with the LIFERAY_ prefix, and then you take the property name, capitalize it and replace any periods with _PERIOD_. So, for example, locales.enabled becomes LIFERAY_LOCALES_PERIOD_ENABLED and jdbc.default.username becomes LIFERAY_JDBC_PERIOD_DEFAULT_PERIOD_USERNAME. In cases where a property name has mixed case, then things differ a little. The uppercase character is replaced with _UPPERCASEX_ where X is the character. So the jdbc.default.driverClassName property with the uppercase C and N will become the LIFERAY_JDBC_PERIOD_DEFAULT_PERIOD_DRIVER_UPPERCASEC_LASS_UPPERCASEN_AME environment variable. If you have your own properties that you've been adding to portal-ext.properties and referencing them via PrefsUtil, well you can use this same technique to reference an environment variable as a replacement for setting the values in portal-ext.properties. Me, I prefer to mix the technique. I want to provide a portal-ext.properties file that has common settings for all of my environments, then leave the environment vars for specific values. So I will normally have the jdbc.default.driverClassName in my portal-ext.properties because every environment is going to use Postgres, for example, and I may even set jdbc.default.username too if that is also going to be the same. But I'll leave the jdbc.default.url and jbdc.default.password for environment variables. This way my environment variables control what a specific environment has, but I'm slimming the list to just what is necessary. And it also allows me to satisfy a requirement of having one image that can be used in all environments. Volume Mapping In the coming sections, I'm going to be referring to special directories that are in the image under the /mnt/liferay directory. If you are building a custom image, you could easily populate this directory in your dockerfile and copy external resources in place and Liferay will use them correctly. Alternatively, you could mount the /mnt/liferay directory from a host volume using the -v option. So if I put my stuff in the /opt/liferay/image/testing directory, I could use the command docker run ... -v /opt/liferay/image/testing:/mnt/liferay ... so the image will use my local filesystem when looking for the special files. Note that if you do use the -v option this way, the host volume completely replaces the /mnt/liferay folder in the image, it does not "merge" them. If the image has an /mnt/liferay/data folder but there is no /opt/liferay/image/testing/data folder, as far as the container is concerned there will not be an /mnt/liferay/data folder and any attempt to access it would fail. Overriding Files The image of course is going to contain a number of files for Liferay, Tomcat, and other things. Sometimes you may want to overwrite a file from the image with your own copy. For example, you might want to replace the default Tomcat bin/setenv.sh file with one of your own. The Liferay image supports this using the /mnt/liferay/files directory. Any files/folders here will be overlayed into the image at /opt/liferay before Liferay starts. So for the setenv.sh script override, I would just need to make it available as /mnt/liferay/files/tomcat/bin/setenv.sh and at runtime it will be copied to /opt/liferay/tomcat/bin, replacing the current setenv.sh file there, and using it as the startup. You could also do this with your portal-ext.properties file. Create it as /mnt/liferay/files/portal-ext.properties and it will be copied to /opt/liferay/portal-ext.properties before Liferay starts. This technique can be used along with the volume mapping in the previous section to move the portal-ext.properties file out of the image altogether, pulling it from the host OS when the image is starting. Same deal for your activation key xml file (if you have one). Using /mnt/liferay/files/osgi/modules/activation-key-...xml, it would be copied into /opt/liferay/osgi/modules before Liferay starts effectively dropping your key where it needs to go. Again this is moving an environment-specific key (i.e. prod vs non-prod) outside of the image, so the image can be used as-is in any environment; you just need to control what -v source you use for the mounting. Shell Scripts The scripts are really the fun part, but I haven't really seen much recommendations on how to handle them and the kinds of things you might do with them, so I wanted to touch on them here. Basically any script that is in the /mnt/liferay/scripts directory are executed before Liferay starts. A note about script naming... I like to have some control over the order of script execution. I accomplish this by prefixing all of the scripts I write with a number, such as 01_do_first.sh and 02_do_next.sh and 99_do_last.sh. When the scripts are processed, they'll be executed in numerical order... I like to use scripts to combine all of the previous techniques a powerful and flexible mechanism to prepare the runtime container... For example, I prefer to use JNDI definitions for the database connections as this will ensure that the connection details and credentials are not exposed to Liferay and not subject to Liferay to reveal. To do this, I will need to overwrite the /opt/liferay/tomcat/conf/Catalina/localhost/ROOT.xml (because I also like to keep the context bound to the app and not make it global). Parts of this will be the same in every environ such as the database driver (if not already available). I'll drop the db driver and the Hikari jars into /mnt/liferay/files/tomcat/lib/ext so they will be copied to /opt/liferay/tomcat/lib/ext and available to Tomcat (you've seen https://liferay.dev/blogs/-/blogs/tomcat-hikaricp, right?). My ROOT.xml file, well I guess I could put it in /mnt/liferay/files/tomcat/conf/Catalina/localhost and let the image copy it in, but that would mean I'd have to have the passwords in the file and may make it impossible to change from an environment variable perspective. What I really want to have is environment variables in the ROOT.xml so I can define creds and URL in the container startup, but Tomcat doesn't really support live replacements in its configuration files. Initially I used a /mnt/liferay/templates directory where I put ROOT.xml with placeholder strings instead of actual values with a script responsible for replacing placeholders and moving to final location. So my JNDI fragment would be something like: With a file like this, you can easily handle the replacements with a simple sed command such as: sed 's|%%JDBC_USER%%|$LIFERAY_DB_USERNAME|g' \ /mnt/liferay/templates/ROOT.xml > \ /opt/liferay/tomcat/conf/Catalina/localhost/ROOT.xml This sed command is going to replace the %%JDBC_USER%% marker in the source file with the value of the LIFERAY_DB_USERNAME environment variable, and the output will be redirected to the ROOT.xml file where Tomcat expects to find it. You're going to want to test this out. I found out the hard way that you can't put an unescaped URL into an XML file like this because odd failures will occur. Since I have multiple replacements to make, I could use a chain of sed commands to apply each replacement. Another alternative, one that I use now, is to do the changes in-place. We could put the ROOT.xml file with the placeholders in /mnt/liferay/files/tomcat/conf/Catalina/localhost, then we could run the sed command with -i so it changes the file directly: sed -i 's|%%JDBC_USER%%|$LIFERAY_DB_USERNAME|g' \ /opt/liferay/tomcat/conf/Catalina/localhost/ROOT.xml For the scripting aspect, we can define the /mnt/liferay/scripts/01-update-jndi.sh script with the following: #!/bin/bash # Declare an associative array for our replacements declare -a jndi jndi=( [%%JDBC_URL%%]=$LIFERAY_DB_URL [%%JDBC_USER%%]=$LIFERAY_DB_USERNAME [%%JDBC_PSWD%%]=$LIFERAY_DB_PASSWORD ) # Use a function to facilitate the replacements updateJndi() { # Loop through the array for key in "${!jndi[@]}" do # Extract the value value=${jndi[$key]} # perform the in-place replacement sed -i "s|${key}|${value}|g" /opt/liferay/tomcat/conf/Catalina/localhost/ROOT.xml done } # run the function updateJndi Perhaps this is more complicated than it needs to be, but hopefully it gives you some ideas. Note that the sed -i command actually leaves a file behind. In the /opt/liferay/tomcat/conf/Catalina/localhost directory you'll still have the ROOT.xml file, but you'll also have a ._ROOT.xml hidden file. And boy, does this file cause Tomcat a heap of trouble. You'll get context startup failures and you think it's pointing at the ROOT.xml file, but it's not, it's referring to the hidden field. Now, in my scripts, if I am doing sed -i on a file, I'm going to add a step to remove the hidden files. I don't need it and don't want them causing any problems... So now we have a template file and that file is updated before Liferay starts, replacing the placeholders with values from environment variables. As another example, consider the simple case of having your activation key files in /mnt/liferay/keys. You need one copied to /opt/liferay/osgi/modules, but you want to control the one using an environment variable. You could leverage a script like: #!/bin/bash # Use the LIFERAY_ENV environment variable to copy in the right activation key... case $LIFERAY_ENV in DEV) cp /mnt/liferay/keys/activation-...dev.xml /opt/liferay/osgi/modules ;; UAT) cp /mnt/liferay/keys/activation-...uat.xml /opt/liferay/osgi/modules ;; PROD) cp /mnt/liferay/keys/activation-...prod.xml /opt/liferay/osgi/modules ;; *) echo ERROR! Invalid environment $LIFERAY_ENV value. ;; esac In this way we'd get the right activation key based on the environment variable even though we don't really know the contents of the key. We're also grabbing it from /mnt/liferay/keys, so if we're using the volume mount trick our volume can have all of the keys and it will be separate from the image. It should be clear now that the scripts directory can contain a shell script that does whatever you need it to do. You have access to a basic linux shell and a basic command set, so you could leverage curl commands to download files to the image with environment variables for added flexibility. The world is practically your oyster in regards to setting up the runtime container image. Deployments Another directory you can leverage is the /mnt/liferay/deploy folder. Any files that you drop in here are going to be copied to the /opt/liferay/deploy folder and processed as normal deployments when Liferay starts. This works out well if you just don't want to build your own docker image in the Liferay workspace, opting instead to use an official docker image along with this "just in time" deployment. Note that you will get errors if you do not have an /mnt/liferay/deploy folder, even if you have nothing to deploy. I think this is a bug, the Liferay image should be able to wrap an if [ -e /mnt/liferay/deploy ] around the processing of the deployments and skip it if it is not there, but until it changes you must create this directory. Docker Environment Variables File So I don't know about you, but I can't see myself typing out docker run -e this -e that -e the_other every time that I want to fire up my docker container. I mean, after all just in this blog I've mentioned at least 10 different environment variables to set and that doesn't even cover the many portal properties I'd probably also want to override. A great solution is to use an environment list file. The file is close to a properties file format, although there are a couple of differences: # This is a comment USER ENV=thevalue The oddball here is the USER value. Actually this will pass the USER environment from your command shell, and the value, into your docker environment. This would be the same as adding -e USER=${USER} to your docker run command. Once you have this file, then you can proceed to use the command docker run --env-file myfile.list ... and this file will be used to set the environment variables passed into the docker container. Plus, you can reuse the file every time you need to, so forget about typing in all of those env vars going forward... Revisting Custom Docker Images So back in https://liferay.dev/blogs/-/blogs/building-customized-liferay-docker-images, I presented what (at the time) was the workspace-supported way to create a custom docker image. If you follow the same path as outlined there, the Dockerfile used for the latest version of the image is: FROM liferay/dxp:7.3.10-dxp-1 ENV LIFERAY_WORKSPACE_ENVIRONMENT=local COPY --chown=liferay:liferay deploy /mnt/liferay/deploy COPY --chown=liferay:liferay patching /mnt/liferay/patching COPY --chown=liferay:liferay scripts /mnt/liferay/scripts COPY --chown=liferay:liferay configs /home/liferay/configs COPY --chown=liferay:liferay 100_liferay_image_setup.sh \   /usr/local/liferay/scripts/pre-configure/100_liferay_image_setup.sh # Any instructions here will be appended to the end of the Dockerfile # created by `createDockerfile`. It's still effectively the same Dockerfile as presented in the older blog, just additional capability for supporting patches and the scripts and configs that I've covered here. The comment at the end? That comes from the project root Dockerfile.ext file and is used in the workspace to add custom stuff to the end of your Docker image. The image that you end up with, all of your modules and wars will be in the deploy directory. So, when this image starts, Liferay/Tomcat will start up and will end up deploying all of your modules, etc. This works of course, it is used by many a project. Alternatively, you could create your own complete image that has your artifacts fully deployed. On a recent project, I did just that... You can use either the distBundleZip or distBundleTar tasks to get the Liferay bundle prepped (the version of Liferay from your gradle.properties, the custom modules and wars moved into the right directories, all good to go). You'll find in the build/dist directory is your expanded bundle. From here we need to change the folder name from build/dist/tomcat-9.0.xx to build/dist/tomcat (you might want to create a soft link from tomcat to the old tomcat-9.0.xx just in case). With this minor change, we can do a docker build using the following Dockerfile: # Use the Liferay base image, has java 8 and java 11, necessary liferay tools, etc. FROM liferay/base:latest # Define the args we support ARG LABEL_BUILD_DATE ARG LABEL_NAME ARG LABEL_VERSION ARG LABEL_VCS_REF ARG LABEL_BUILD_ID RUN apk --no-cache add busybox-extras # Copy the dist folder in as the new /opt/liferay root COPY --chown=liferay:liferay build/dist /opt/liferay # Soft-link files back to the home mount RUN ln -fs /opt/liferay/* /home/liferay # Set up the /mnt/liferay folder in case someone forgets the -v ENV LIFERAY_WORKSPACE_ENVIRONMENT=docker RUN install -d -m 0755 -p liferay -g liferay /mnt/liferay/deploy /mnt/liferay/patching /mnt/liferay/scripts COPY --chown=liferay:liferay configs /home/liferay/configs COPY --chown=liferay:liferay 100_liferay_image_setup.sh /usr/local/liferay/scripts/pre-configure/100_liferay_image_setup.sh # Define the entry point as a script from the base ENTRYPOINT /usr/local/bin/liferay_entrypoint.sh # Liferay/Tomcat basics ENV JPDA_ADDRESS=8000 ENV LIFERAY_DISABLE_TRIAL_LICENSE=false ENV LIFERAY_HOME=/opt/liferay ENV LIFERAY_JPDA_ENABLED=false ENV LIFERAY_JVM_OPTS= ENV LIFERAY_PRODUCT_NAME="${LABEL_NAME}" # Set up some defaults in case overrides are skipped ENV LIFERAY_MODULE_PERIOD_FRAMEWORK_PERIOD_PROPERTIES_PERIOD_OSGI_PERIOD_CONSOLE=0.0.0.0:11311 ENV LIFERAY_SETUP_PERIOD_WIZARD_PERIOD_ADD_PERIOD_SAMPLE_PERIOD_DATA=false ENV LIFERAY_SETUP_PERIOD_WIZARD_PERIOD_ENABLED=false ENV LIFERAY_TERMS_PERIOD_OF_PERIOD_USE_PERIOD_REQUIRED=false ENV LIFERAY_USERS_PERIOD_REMINDER_PERIOD_QUERIES_PERIOD_ENABLED=false # These are the publicly exposed ports EXPOSE 8000 8009 8080 11311 # This health check is the same used with DXPC HEALTHCHECK \ --interval=1m \ --start-period=1m \ --timeout=1m \ CMD curl -fsS "http://localhost:8080/c/portal/layout" || exit 1 # Define some labels on the image LABEL org.label-schema.build-date="${LABEL_BUILD_DATE}" LABEL org.label-schema.name="${LABEL_NAME}" LABEL org.label-schema.schema-version="1.0" LABEL org.label-schema.vendor="Vantage DC" LABEL org.label-schema.version="${LABEL_VERSION}" LABEL org.label-schema.vcs-ref="${LABEL_VCS_REF}" LABEL org.label-schema.build-ref="${LABEL_BUILD_ID}" # Switch to the liferay user for the run time USER liferay:liferay # Set the working dir WORKDIR /opt/liferay # Docker will now launch the entry point script... Lest you think I created something new here, I really didn't. If you check the official Liferay Dockerfile used to create the normal Liferay images, you'll see that the bulk of it is from there. I did add the content from the workspace's Dockerfile; after all, I want to ensure that the Liferay entrypoint script is going to work for this custom bundle as it does for the regular Liferay bundle. Now, I'm not getting into how invoke Docker to build your image with this. Me, I was using Jenkins so I leveraged facilities there to build my image and then push to the local repo. You could also build it by hand on the command line or leverage one of the Gradle plugins to build your image. I kind of like this concept because my image already has my artifacts deployed, so it's not really a container startup function anymore. Conclusion Some points in conclusion that perhaps weren't made clear... First, precedence... For the /mnt/liferay directory, the file copies happen before the script executions, not after. So avoid scripts that change files in /mnt/liferay/files because they won't have the effect you expect. And both of these actions occur before the processing of the /mnt/liferay/deploy folder. Second, persistence. The files copied from /mnt/liferay/files and the changes imposed by scripts in /mnt/liferay/scripts are not persistent. They will only be applied within the running container. If the container is shut down, the changes are lost. When the container is started again, /mnt/liferay/files are re-copied and scripts in /mnt/liferay/scripts are re-executed. This is important to understand, especially if you are using a mount for /mnt/liferay as any changes in the host filesystem would be reflected in the next container launch. The persistence aspect also applies to the /mnt/liferay/deploy folder; basically every time the docker container starts, it will be redeploying the artifacts. We can build our own images still, either by using the Liferay Workspace way or, alternatively, using our own Dockerfile, so we can get the image we want or need.   [Less]