<dependencies>
<dependency>
<groupId>com.flowlogix</groupId>
<artifactId>flowlogix-jee</artifactId>
<version>9.0.7</version>
</dependency>
</dependencies>
Introduction and Features
FlowLogix provides a few remaining missing pieces in what is covered by Jakarta EE, PrimeFaces, OmniFaces, Apache Shiro and other very popular software.
FlowLogix Jakarta EE library fills in the last few gaps that are left over in PrimeFaces, OmniFaces and Jakarta EE itself, including:
-
Provides automatic Data Access for JPA with delegation and without inheritance
-
Adds Type-safe Native SQL query with JPA via generics
-
Declares Jakarta Faces PROJECT_STAGE development mode automatically
-
Automatically uses minified versions of assets with Jakarta Faces
-
Provides easy and automatic initialization of OmniFaces' UnmappedResourceHandler
-
Easier-to-use, Injected JPA Lazy Data Model for PrimeFaces DataTable that supports clustered sessions
-
Automatically includes PrimeFaces font mime types in your web applications, preventing warnings and extra entries in your web.xml
-
Convert strings to arbitrary types on a best-effort basis
-
Transforms names between javax and jakarta namespaces
-
Checks if objects are truly serializable by testing them
-
Easy Transform Streams to Strings
-
Simplify creation and manipulation of ShrinkWrap and Arquillian test archives including assets
Where does FlowLogix fit into your application architecture?
No. FlowLogix fits within the Jakarta EE design philosophy and works with MicroProfile, Jakarta EE, OmniFaces and PrimeFaces ecosystem. FlowLogix tries to be the least intrusive, automatic and with the fewest requirements possible.
Simplicity is the #1 principal. FlowLogix doesn’t make you inherit from base classes or interfaces, unless absolutely necessary. Annotations are also used sparingly, being careful not to introduce any unnecessary cognitive load and complexity.
No. Although the library itself uses Lombok to avoid boilerplate code, it is completely optional to the user, and is not required to be a dependency of user code. Having said that, even though Java itself currently has lots of features only available with Lombok in prior years, Lombok still has many features not available in Java today, and is very useful and highly recommended.
Installation and Compatibility
Compatibility
FlowLogix 5.x is compatible with Java 11+ and Java EE 8, Jakarta EE 8 and later. Jakarta EE 9 and later (jakarta.* namespace) is available via the jakarta
maven classifier.
FlowLogix 6.x and later is compatible with Java 17+ and Jakarta EE 9 and later. No classifier is required.
Installation
Artifacts are available in the Sonatype’s Maven Central repository. All snapshots are available in the Maven Central snapshot repository
<dependencies>
<dependency>
<groupId>com.flowlogix</groupId>
<artifactId>flowlogix-datamodel</artifactId>
<version>9.0.7</version>
</dependency>
</dependencies>
Flow Logix BOM
Flow Logix includes an easy way to declare all the required dependency versions, via the Maven Bill of Materials.
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.flowlogix</groupId>
<artifactId>flowlogix-bom</artifactId>
<version>9.0.7</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
See the Maven Documentation for more information on BOMs
Developer’s Guide
FlowLogix Jakarta EE Components
JPAFinder
for JPA: Delegation-based Composable Queries for Data Access Objects and business logic
findAll()
, findRange()
and count()
take an optional argument to enhance the queries, which can add additional JPA clauses, enhancements and hints. This parameter takes a form of QueryEnhancement
interface, which extends BiConsumer
interface. There are a number of accept
convenience methods in QueryEnhancement
interface that can be passed via a method reference. This enables composition of different QueryEnhancement
instances.
public record CountAndList(long count, List<UserEntity> list) { };
public CountAndList countAndList(String userName) {
// add "where fullName = 'userName'" clause
QueryEnhancement<UserEntity> enhancement = (partial, criteria) -> criteria
.where(partial.builder().equal(partial.root()
.get(UserEntity_.fullName), userName));
return new CountAndList(jpaFinder.count(enhancement::accept),
jpaFinder.findAll(enhancement::accept)
.setHint(QueryHints.BATCH_TYPE, BatchFetchType.IN)
.getResultList());
}
public CountAndList extractedCountAndList(String userName) {
// add "where fullName = 'userName'" clause
QueryEnhancement<UserEntity> enhancement = (partial, criteria) -> criteria
.where(partial.builder().equal(partial.root()
.get(UserEntity_.fullName), userName));
// descending order for queries
QueryEnhancement<UserEntity> orderBy = (partial, criteria) -> criteria
.orderBy(partial.builder().desc(partial.root().get(UserEntity_.fullName)));
return new CountAndList(jpaFinder.count(enhancement::accept),
jpaFinder.findAll(enhancement.andThen(orderBy)::accept)
.getResultList());
}
There are many ways to implement the Data-Access-Object pattern in Java. Most of them require to implement an interface, have some kind of bytecode generation magic or inherit from a base class. FlowLogix takes a different approach. The amount of magic is totally up to the developers. If none of the below approaches work, your DAO can simply inherit from InheritableDaoHelper
, and initialize the jpaFinder
protected field in your @PostConstruct
method.
You can leverage Project Lombok @Delegate annotation to transparently implement Data Access Objects without any specific requirements. Alternatively, developers can write manual forwarder methods, dynamic proxies, or
other bytecode generators to delegate to JPAFinder
from an interface.
Recommended approach is to use Lombok’s @Delegate
annotation. This introduces the fewest amount of magic, results in the least amount of code, and absolute minimum (if any) boilerplate.
JPAFinder
can also be @Inject
ed and it will infer the EntityManager
via CDI. If default CDI producer for the EntityManager is insufficient, @EntityManagerSelector
annotation can be used to specify qualifiers for non-default EntityManager
producer
JPAFinder
is Serializable and thus can be used inside @ViewScoped
beans, for example.
Fluent Builder pattern can be used to create DaoHelper
object, which requires EntityManager Supplier
and entity Class
.
DaoHelper
take Supplier
as a parameter instead of EntityManager
directly?That’s Because EntityManager
is not initialized when the object is created. Supplier
lets you delay the initialization of DaoHelper
until EntityManager
is actually needed at run-time and is already initialized. This is why InheritableDAOHelper
needs to be initialized in @PostConstruct
method instead of the constructor.
find()
and related methods?These methods are built into the EntityManager and can be accessed that way (see the "more complete" example below). There is no need for JPAFinder to provide them. If more functionality related to find()
is desired, Jakarta Data or Apache DeltaSpike project is a wonderful addition to your architecture and will satisfy those needs.
@Stateless
public class ExampleDAO {
@PersistenceContext(unitName = "demo-pu")
EntityManager em;
@Delegate
JPAFinder<UserEntity> jpaFinder = new DaoHelper<>(() -> em, UserEntity.class);
}
@Stateless
public class InjectedDAO {
@Inject
@Delegate
JPAFinder<UserEntity> jpaFinder;
}
@Qualifier
@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
public @interface NonDefault {
}
@Stateless
public class InjectedEntityManager {
@Inject
@NonDefault
EntityManager entityManager;
}
@Stateless
public class InjectedNonDefaultDAO {
@Inject
@Delegate
@EntityManagerSelector(NonDefault.class)
JPAFinder<UserEntity> jpaFinder;
}
EntityManager
methods@Stateless
public class ExampleDelegateDAO {
@Inject
@Delegate(excludes = EntityManagerExclusions.class)
EntityManager entityManager;
@Inject
@Delegate
JPAFinderHelper<UserEntity> jpaFinder;
}
@RequestScoped
public class EntityManagerProducer {
@Getter(onMethod = @__({@Produces, @NotDefault}))
@PersistenceContext(unitName = "nonDefault")
EntityManager entityManager;
}
Note
|
@NonDefault annotation can be removed to use it as a Default producer. If there is only one Persistence Unit, unitName can be also omitted.
|
@Stateless
@TransactionAttribute(SUPPORTS)
public class StatelessEntityManagerProducer {
@Getter(onMethod = @__({@Produces, @NonDefault}))
@PersistenceContext(unitName = "nonDefault")
EntityManager entityManager;
}
The example above works well if @RequestScope
is unavailable and may work better because of EJB bean pooling optimizations.
@Stateless
public class InheritedDAO extends InheritableDaoHelper<UserEntity> {
@Inject
EntityManager entityManager;
@PostConstruct
void init() {
jpaFinder = new DaoHelper<>(() -> entityManager, UserEntity.class);
}
}
JPA: Generics-based Type-safe native query
JPANativeQuery
interface has a convenience method createNativeQuery()
which will return TypedNativeQuery
object. This is a thin wrapper over JPA’s Query
object, however it’s getResult*()
methods return typed results via generics, which avoids casting and makes the results easier and safer to use.
public List<UserEntity> findByNative(String sql) {
return jpaFinder.createNativeQuery(sql, jpaFinder.getEntityClass()).getResultList();
}
Jakarta Faces: Automated PROJECT_STAGE configuration
Jakarta Faces runs in production mode by default. However, most applications set up development mode by modifiying web.xml
. Traditionally, it’s been difficult to set up environment-based switching from development to production mode. FlowLogix sets this up automatically via web-fragment.xml
and allows JNDI-based switch to production mode.
Note
|
Special entries in web.xml , glassfish-web.xml or any other container-specific configuration are no longer required and can be removed.
|
Below is an example of setting up production mode with Payara and GlassFish with Jakarta EE 9 or later:
$ asadmin create-custom-resource --resType=java.lang.String --factoryClass=org.glassfish.resources.custom.factory.PrimitivesAndStringFactory faces/ProjectStage
$ asadmin set resources.custom-resource.faces/ProjectStage.property.value=Production
With Jakarta EE or Java EE 8 and earlier, replace faces/
with jsf/
.
Jakarta Faces: Use minified assets automatically in production mode
Most front-end applications want to use minified versions of their assets, such as JavaScript and CSS files in production (i.e. any non-development) modes. FlowLogix allows this via MinimizedHandler
which will automatically insert min
prefix into the appropriate assets, for example resource.js → resource.min.js
and resource.css → resource.min.css
.
This is configurable via web.xml parameters com.flowlogix.MINIMIZED_PREFIX
and com.flowlogix.MINIMIZED_FILE_TYPES
MinimizedHandler
works with build tools that generate minified versions of resources automatically, such as maven minify plugin.
<application>
<resource-handler>com.flowlogix.ui.MinimizedHandler</resource-handler>
</application>
Any configured Jakarta Faces resources are resolved to their minified versions
myjavascript.min.js
<h:outputScript name="myjavascript.js"/>
other.min.css
@import url("#{resource['css/other.css']}");
By default, only resources with css and js extensions are resolved to their minified versions. The min
extension is inserted prior to their original extension.
.min
to .minimized
<context-param>
<param-name>com.flowlogix.MINIMIZED_PREFIX</param-name>
<param-value>minimized</param-value>
</context-param>
The above example changes resolution from mycss.css
→ mycss.min.css
to mycss.css
→ mycss.minimized.css
<!-- Optional, default is "css, js" -->
<context-param>
<param-name>com.flowlogix.MINIMIZED_FILE_TYPES</param-name>
<param-value>css, js, tsx, sass, scss, less</param-value>
</context-param>
OmniFaces: Automatic initialization of UnmappedResourceHandler
In order to initialize OmniFaces' UnmappedResourceHandler, both web.xml
servlet-mapping
and faces-config.xml
entries are ordinarily required. FlowLogix automates more complicated web.xml
requirements by configuring the servlet container to include all unmapped resources. This alleviates requirement to specify Faces Servlet
mapping in your web.xml
file at all. In order to enable this, add the below context parameter in web.xml
:
<context-param>
<param-name>com.flowlogix.add-unmapped-resources</param-name>
<param-value>true</param-value>
</context-param>
PrimeFaces: Font mime-types automatically included
PrimeFaces automatically includes fonts as part of the application. However, the file extensions of these fonts are not usually included in most servlet containers by default. FlowLogix adds those mime types automatically and prevents the warnings such as below from appearing in log files:
WARNING: JSF1091: No mime type could be found for file font1.woff2
This way, no extra mime-mapping
entries are required in web.xml
Type Converter, Serialization Tester and Integration Test Helper
Most classes that can be constructed from String
include valueOf(String)
method by convention.
TypeConverter
class uses this to generically convert a String to any type specified, thus dramatically reducing the code required.
If unable to convert the class, an exception is thrown.
TypeConverter
specifically does not support custom converters for simplicity.
If those are desired, other libraries do a great job of handling custom converters, including Jakarta Faces' Converters.
String
to any class, throws an exception if failedTT convertedValue = TypeConverter.valueOf(input, cls);
CheckedValue<TT> checkedValue = TypeConverter.checkAndConvert(input, cls);
if (checkedValue.isValid()) {
// get and operate on a value in a type-safe way
TT value = checkedValue.getValue();
}
FlowLogix provides a convenience method for converting strings from javax
to jakarta
namespace:
// returns "jakarta.servlet.Servlet" in Jakarta artifacts
String jakartaServlet = JakartaTransformerUtils.jakartify("javax.servlet.Servlet");
// returns "jakarta.faces.FacesException: message X" in Jakarta artifacts
String jakartaError = JakartaTransformerUtils.jakartify("javax.faces.FacesException: message X");
FlowLogix automatically detects which environment it’s in, and converts javax
-based names into jakarta
-based namespace accordingly. Keep in mind that the environment check occurs at run-time, and not compile time. This method works not only for classes, but also for error messages and other strings.
The jakartify()
utility is particularly useful in scenarios where maven shade
plugin is used to produce both javax
and jakarta
-based JAR artifacts. It allows to use same source code for both and dynamically produce correct strings without additional shade plugins or complex regular expressions.
Generic serializeAndDeserialize()
method can be used to check the true ability to serialize a class. It returns the result of the input going through serialization and deserialization, so the new object’s state can be checked for correctness.
TT output = SerializeTester.serializeAndDeserialize(input);
assertEquals(input, output);
FlowLogix provides an easy way to read a String from any input stream:
var stream = new ByteArrayInputStream(input.getBytes());
String string = Streams.readString(stream);
Note
|
The input stream is not closed by the readString method
|
ShrinkWrapManupulator
class has a few utility methods that make testing tricky behaviors easier.
Some tests require TLS/SSL to execute properly. Arquillian uses plain http by default. To facilitate tests that require TLS/SSL, toHttpsURL(url)
takes a URL and converts it to it’s https equivalent, taking the httpsPort
system property into account. Default TLS port is 8181.
A more complete method toHttpURL(url, sslPortPropertyName, defaultPort)
is available if the default system property and port are not suitable for your needs.
$ mvn verify -DhttpsPort=8282
@Deployment
public static WebArchive deployMaven() {
WebArchive archive = ShrinkWrapManipulator.createDeployment(WebArchive.class);
log.info("Archive Contents: %s", archive.toString(true));
return archive;
}
@Deployment
public static WebArchive deployMavenSuffix() {
WebArchive archive = ShrinkWrapManipulator.createDeployment(WebArchive.class, name -> name + "-prod");
log.info("Archive Contents: %s", archive.toString(true));
return archive;
}
webXmlXPath()
takes a List<Action>
and will manipulate archive’s web.xml
to achieve the desired test configuration. For example, if Jakarta Faces production mode is desired for a particular archive, web.xml
context-param
can be changed below:
@Deployment
public static WebArchive deployProductionMode() {
var archive = ShrinkWrapManipulator.createDeployment(WebArchive.class);
// add classes to the archive here
var productionList = List.of(new Action(getContextParamValue(
jakartify("javax.faces.PROJECT_STAGE")),
node -> node.setTextContent("Production")));
new ShrinkWrapManipulator().webXmlXPath(archive, productionList);
return archive;
}
getContextParamValue()
is a shorthand to produce XPath for web.xml
context parameter (<context-param>
) element:
//web-app/context-param[param-name = 'jakarta.faces.PROJECT_STAGE']/param-value
The second parameter is DOM Node
class Consumer
lambda, which allows for manipulation of the DOM element directly by the user.
Above, we also combine web.xml
manipulation with jakartify
to be compatible with both Jakarta EE 8 or 9, if desired.
ShrinkWrapManipulator
can also easily manipulate persistence.xml
file. Even though maven filtering can be used to resolve a versioned entities JAR in persistence.xml, ShrinkWrap will not do the filtering because it uses its own mechanism to create the archive. Another way is needed to create the substitution inside the deploy()
method in ShirkWrap:
<persistence>
<persistence-unit>
<jar-file>entities-${project.version}.jar</jar-file>
</persistence-unit>
</persistence>
persistence.xml
filepublic static WebArchive deployPersistence() {
var archive = ShrinkWrapManipulator.createDeployment(WebArchive.class);
// add classes to the archive here
String version = System.getProperty("project.version");
new ShrinkWrapManipulator().persistenceXmlXPath(archive,
List.of(new Action("//persistence/persistence-unit/jar-file",
node -> node.setTextContent(String.format("lib/entities-%s.jar", version)))));
return archive;
}
Developers can transform any XML file from the archive by using the generic manipulateXml()
method, which both webXmlXPath()
and persistenceXmlXPath()
methods utilize to do their job.
FlowLogix PrimeFaces DataTable Lazy Data Model backed by JPA
PrimeFaces provides a convenient wrapper for the Lazy DataModel. However, FlowLogix JPALazyDataModel
predates it and therefore has a big "head start" in ease of use, features and compactness. Biggest advantage is ability to @Inject
the model via CDI. The model utilizes JPAFinder
classes and methodology to make JPA lazy data model easier to use, with a lot less code and better design. The model not require inheritance and problems associated with it. In addition to its ease of use advantages, JPALazyDataModel
is fully serializable, making its use trivial in clustered sessions, like the ones in Payara and Hazelcast. To make it even easier to use and configure, model’s initialization lambdas do not have to be serializable and 'just work' out-of-the-box without paying any special attention to their context.
<p:dataTable lazy="true" value="#{userViewer.lazyModel}" var="user">
... specify columns as usual ...
</p:dataTable>
@Named
@ViewScoped
public class UserViewer implements Serializable {
@Inject
@Getter
// optional configuration annotation
@LazyModelConfig(caseInsensitive = true)
JPALazyDataModel<UserEntity> lazyModel;
}
Above we created a model with case-insensitive filtering. Model can be optionally initialized further inside @PostConstruct
using initialize()
method and builder pattern (see below)
@Named
@ViewScoped
public class UserViewer implements Serializable {
private final @Getter JPALazyDataModel<UserEntity> userModel =
JPALazyDataModel.create(builder -> builder.entityClass(UserEntity.class).build());
}
Above, we create the model without injection.
During direct creation, JPALazyDataModel
only requires entityClass
to work, everything else is optional:
-
entityManager: Provide entity manager
Supplier
. Default is injected via CDI -
entityManagerQualifiers: Provides a list of qualifier annotations to select the correct EntityManager for injection
-
caseSensitiveFilter: Specifies if filtering of String values are case-sensitive (boolean). Default is
true
. -
filterCaseConversion: either
UPPER
orLOWER
. Specifies whether to convert queries to upper or lower case during case-insensitive filter queries. Default isUPPER
-
wildcardSupport: Specifies whether wildcards are supported in
EXACT
and other String queries (boolean). Default is false. -
sorter: Apply additional or replacement sort criteria
-
filter: Apply additional or replacement filter criteria
-
optimizer: Apply additional customizations to queries, such as JPA hints, works together with
JPAFinder
-
resultEnricher: Apply transformation to the results of the query, such as adding or modifying resulting rows displayed by the model
-
converter:
Function
that converts String representation of a primary key into a primary key object. Needed only if the default is insufficient. -
keyConverter:
Function
that converts an entity object into it’s primary key inString
form. Needed only if the default is insufficient.
Let’s use custom criteria to add address to the default sort:
@Named
@ViewScoped
public class SortingDataModel implements Serializable {
@Inject
@Getter
JPALazyDataModel<UserEntity> userModel;
@PostConstruct
void initialize() {
// add an ascending zip code-based sort order
userModel.initialize(builder -> builder.sorter((sortData, cb, root) ->
sortData.applicationSort(UserEntity_.zipCode.getName(),
var -> cb.asc(root.get(UserEntity_.zipCode))))
.build());
}
}
Let’s use custom filter criteria using replaceFilter
convenience method. Here we make sure that only zip codes greater than that in the filter are returned:
@Named
@ViewScoped
public class FilteringDataModel implements Serializable {
@Inject
@Getter
JPALazyDataModel<UserEntity> userModel;
@PostConstruct
void initialize() {
// display only zip codes greater than the filter field
userModel.initialize(builder -> builder.filter((filters, cb, root) ->
filters.replaceFilter(UserEntity_.zipCode.getName(),
(Predicate predicate, Integer value) -> cb.greaterThan(root.get(UserEntity_.zipCode), value)))
.build());
}
}
Optimizer hints can be used to fetch dependent entity relationships in batches. The UnaryOperator
should return the same TypedQuery
instance it was passed in the Fluent manner:
IN
query@Named
@ViewScoped
public class OptimizedDataModel implements Serializable {
@Inject
@Getter
JPALazyDataModel<UserEntity> userModel;
@PostConstruct
void initialize() {
// optimize query by batching relationship fetching
userModel.initialize(builder -> builder.optimizer(query -> query
.setHint(QueryHints.BATCH, getResultField(UserEntity_.userSettings.getName()))
.setHint(QueryHints.BATCH, getResultField(UserEntity_.alternateEmails.getName()))
.setHint(QueryHints.BATCH_TYPE, BatchFetchType.IN))
.build());
}
}
Results Enricher can be used to post-process the results of the query:
@Named
@ViewScoped
public class EnrichedDataModel implements Serializable {
@Inject
@Getter
JPALazyDataModel<UserEntity> userModel;
@PostConstruct
void initialize() {
// enrich returned results from the model
userModel.initialize(builder -> builder.resultEnricher(EnrichedDataModel::addLastRow).build());
}
private static List<UserEntity> addLastRow(List<UserEntity> list) {
list.add(UserEntity.builder().userId("golden").fullName("Golden User").build());
return list;
}
}
Let’s select a specific entity manager for this particular data model, by using a CDI qualifier:
@LazyModelConfig(entityManagerSelector)
to use a specific persistence unit@Named
@ViewScoped
public class QualifiedDataModel implements Serializable {
@Inject
@Getter
@LazyModelConfig(entityManagerSelector = NonDefault.class)
JPALazyDataModel<UserEntity> userModel;
}
JPALazyDataModel
needs to convert a String representation of the entity’s primary key into the actual entity primary key object. This is a requirement for PrimeFaces' LazyDataModel.getRowKey(String)
method. Default converter is provided. However, if it’s insufficient, converter
builder method is provided to override the default.
JPALazyDataModel
needs to convert an entity instance to a String that represents its primary key. This is a requirement for PrimeFaces' LazyDataModel.getRowKey(TT)
method. Default converter is provided. However, if it’s insufficient, keyConverter
builder method is provided to override the default.
@Named
@ViewScoped
public class ConverterDataModel implements Serializable {
@Inject
@Getter
JPALazyDataModel<UserEntity> userModel;
@PostConstruct
void initialize() {
userModel.initialize(builder -> builder
// key is stored as binary string
.converter(binaryString -> new BigInteger(binaryString, 2).longValue())
.keyConverter(entity -> Long.toBinaryString(entity.getId()))
.build());
}
}
API Reference
FlowLogix features a full API references: