|
|
Welcome to the JButler CRUD Tutorial. In this tutorial, we start from scratch and give step-by-step instructions on how to create a CRUD using standard [Jakarta EE](https://jakarta.ee/) technology (JSF, CDI, EJB, JPA) and taking advantage of utility classes already present in [JButler](https://gitlab.labes.inf.ufes.br/labes/jbutler/).
|
|
|
|
|
|
As a running example for this tutorial, imagine an information system called Oldenburg, which helps professors teaching Research Methodology courses to simulate a workshop in which the students will submit papers and perform peer review. The application is named after [Henry Oldenburg](https://en.wikipedia.org/wiki/Henry_Oldenburg) which, according to Wikipedia, is the creator of scientific peer review. If you're using this tutorial to bootstrap your own Web application, you should replace all references to _Oldenburg_ with your application's name throughout the tutorial.
|
|
|
|
|
|
|
|
|
|
|
|
## Tool installation and configuration
|
|
|
|
|
|
The following tools and versions were used in the making of this tutorial (newer versions of these tools might also work the same way):
|
|
|
|
|
|
Technology | Tool | Version | Download
|
|
|
------------------- | ---------------------- | ------- | -----------------------------------------------------------------
|
|
|
Java SDK | OpenJDK | 17 | [jdk.java.net](https://jdk.java.net/17/)
|
|
|
Build Tool | Maven | 3.8 | [maven.apache.org](https://maven.apache.org/download.cgi)
|
|
|
IDE | Visual Studio Code | 1.6 | [code.visualstudio.com](https://code.visualstudio.com/Download)
|
|
|
Database | MySQL Community Server | 8.0 | [dev.mysql.com](https://dev.mysql.com/downloads/mysql/)
|
|
|
Database front-end | MySQL Workbench | 8.0 | [dev.mysql.com](http://dev.mysql.com/downloads/tools/workbench/)
|
|
|
Jakarta EE Server | WildFly Preview EE 9.1 | 26.0 | [wildfly.org](https://wildfly.org/downloads/)
|
|
|
|
|
|
Installation instructions vary according to the operating system (Linux, MacOS or Windows), therefore we have not included detailed, step-by-step instructions on how to install these tools. For WildFly it's enough to unpack it somewhere in your hard drive. We'll refer to the folder where you unpacked WildFly as `$WILDFLY_HOME`. The other tools could be installed using an install wizard downloaded from their website or through a package manager (e.g.: [Homebrew](https://brew.sh/) in MacOS, `apt-get` in [Ubuntu Linux](http://www.ubuntulinux.com) or any other [Debian](http://www.debian.org)-based distributions).
|
|
|
|
|
|
|
|
|
## Deploy the JButler Base Project
|
|
|
|
|
|
The [JButler Base Project](https://gitlab.labes.inf.ufes.br/labes/jbutler-base-project) is a generic information system called `mysystem` built with JButler with a CRUD for objects that are instances of a `MyObject` class. To test that everything is OK with your tool installation and to give a sense of JButler, use the following steps to try the base project in your computer (some instructions are for Unix-like systems like Linux and MacOS, Windows users may need to adapt, feel free to contribute with Windows-specific instructions to the tutorial):
|
|
|
|
|
|
1. Make sure that MySQL is running, open MySQL Workbench, connect to your server and create a new, empty schema (use the appropriate button in the toolbar) called `mysystem`, with character set `utf8mb4`;
|
|
|
|
|
|
2. Still under MySQL Workbench, create a new user called `labes`, limit to hosts matching `localhost` and password `labes`. After the user is created, add "full" schema privileges (use the **Select "ALL"** button) to the newly created `mysystem` schema. See [the MySLQ Workbench documentation on Users and Privileges](https://dev.mysql.com/doc/workbench/en/wb-mysql-connections-navigator-management-users-and-privileges.html) if you need assistance;
|
|
|
|
|
|
3. Using Git, clone the [JButler Base Project](https://gitlab.labes.inf.ufes.br/labes/jbutler-base-project) to your computer: `git clone https://gitlab.labes.inf.ufes.br/labes/jbutler-base-project.git` (you can also use the SSH URL if you are from LabES);
|
|
|
|
|
|
4. Using a terminal, go to the `jbutler-base-project` folder you just cloned and build the application for deployment by running `mvn package`;
|
|
|
|
|
|
5. Using (another) terminal, go to the `$WILDFLY_HOME/bin` folder and execute WildFly by running `./standalone.sh`;
|
|
|
|
|
|
6. Once WildFly is running, go back to the `jbutler-base-project` folder and copy the application package `target/jbutler-base-project.war` to the `$WILDFLY_HOME/standalone/deployments` folder. WildFly should start deploying. If everything goes well, you should see this message: `Deployed "jbutler-base-project.war"`;
|
|
|
|
|
|
7. Open http://localhost:8080/jbutler-base-project in your browser and try some of the features;
|
|
|
|
|
|
8. When you want to undeploy the application, go to the `$WILDFLY_HOME/standalone/deployments` folder and rename the `jbutler-base-project.war.deployed` file to `jbutler-base-project.war.undeploy`. You can then delete the `jbutler-base-project.*` files from that folder and stop WildFly by pressing Ctrl+C in the terminal in which it's open.
|
|
|
|
|
|
If you want, you can use the [JButler Base Project](https://gitlab.labes.inf.ufes.br/labes/jbutler-base-project) source code to kickstart your system (in this case, you should be aware of its [license](https://gitlab.labes.inf.ufes.br/labes/jbutler-base-project/-/blob/main/LICENSE.txt)). Or you can just use it as an example and start your project from scratch. In the following section, this is what we will do.
|
|
|
|
|
|
|
|
|
|
|
|
## Create a new project with JButler
|
|
|
|
|
|
To create a project from scratch, we will use a Maven archetype mentioned in [a Jakarta EE 9 Hello World example](https://blog.payara.fish/getting-started-with-jakarta-ee-9-hello-world). A similar process (but without JButler) is described in the [JavaHostel example repository](https://github.com/dwws-ufes/javahostel/tree/main/jakartaee9), with detailed explanation of the steps, if you need them. It's a good tutorial to do before this one, in order to learn about Jakarta EE 9 before learning about JButler. Here, I will just describe the steps without detailed explanations.
|
|
|
|
|
|
Open a terminal, change to the directory where you want the Oldenburg project folder to be created, run the following command and provide the values for the project properties as in the example below (the `$` denotes the prompt and is not part of the command):
|
|
|
|
|
|
```console
|
|
|
$ mvn archetype:generate -DarchetypeGroupId=org.apache.maven.archetypes -DarchetypeArtifactId=maven-archetype-quickstart
|
|
|
...
|
|
|
[INFO] Generating project in Interactive mode
|
|
|
[INFO] Archetype [org.apache.maven.archetypes:maven-archetype-quickstart:1.4] found in catalog remote
|
|
|
Define value for property 'groupId': br.ufes.inf.labes
|
|
|
Define value for property 'artifactId': oldenburg
|
|
|
Define value for property 'version' 1.0-SNAPSHOT: :
|
|
|
Define value for property 'package' br.ufes.inf.labes: : br.ufes.inf.labes.oldenburg
|
|
|
```
|
|
|
|
|
|
This archetype doesn't create the full structure of a Jakarta EE project in Maven. To complete it, go into the project folder and create some new subfolders, plus you can delete the two sample classes that are created, like below:
|
|
|
|
|
|
```console
|
|
|
$ cd oldenburg
|
|
|
$ mkdir src/main/resources
|
|
|
$ mkdir src/main/webapp
|
|
|
$ mkdir src/test/resources
|
|
|
$ rm src/main/java/br/ufes/inf/labes/oldenburg/App.java
|
|
|
$ rm src/test/java/br/ufes/inf/labes/oldenburg/AppTest.java
|
|
|
```
|
|
|
|
|
|
Open the `oldenburg` folder on Visual Studio Code to finish the project setup and add JButler. Open the `pom.xml` file and change it so it looks like the one below:
|
|
|
|
|
|
```xml
|
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
|
|
|
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
|
|
<modelVersion>4.0.0</modelVersion>
|
|
|
|
|
|
<groupId>br.ufes.inf.labes</groupId>
|
|
|
<artifactId>oldenburg</artifactId>
|
|
|
<version>1.0-SNAPSHOT</version>
|
|
|
<packaging>war</packaging>
|
|
|
|
|
|
<name>oldenburg</name>
|
|
|
<url>https://gitlab.labes.inf.ufes.br/labes/oldenburg/</url>
|
|
|
|
|
|
<properties>
|
|
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
|
|
<maven.compiler.release>17</maven.compiler.release>
|
|
|
<failOnMissingWebXml>false</failOnMissingWebXml>
|
|
|
</properties>
|
|
|
|
|
|
<repositories>
|
|
|
<repository>
|
|
|
<id>br.ufes.inf.labes</id>
|
|
|
<name>LabES/UFES Maven Repository</name>
|
|
|
<url>https://labes.inf.ufes.br/maven2</url>
|
|
|
<layout>default</layout>
|
|
|
</repository>
|
|
|
</repositories>
|
|
|
|
|
|
<dependencies>
|
|
|
<dependency>
|
|
|
<groupId>br.ufes.inf.labes</groupId>
|
|
|
<artifactId>jbutler</artifactId>
|
|
|
<version>2.0</version>
|
|
|
</dependency>
|
|
|
<dependency>
|
|
|
<groupId>org.webjars</groupId>
|
|
|
<artifactId>font-awesome</artifactId>
|
|
|
<version>6.1.0</version>
|
|
|
</dependency>
|
|
|
<dependency>
|
|
|
<groupId>com.github.adminfaces</groupId>
|
|
|
<artifactId>admin-template</artifactId>
|
|
|
<version>1.3.1-jakarta</version>
|
|
|
</dependency>
|
|
|
<dependency>
|
|
|
<groupId>mysql</groupId>
|
|
|
<artifactId>mysql-connector-java</artifactId>
|
|
|
<version>8.0.27</version>
|
|
|
<scope>runtime</scope>
|
|
|
</dependency>
|
|
|
</dependencies>
|
|
|
|
|
|
<build>
|
|
|
<finalName>oldenburg</finalName>
|
|
|
<pluginManagement>
|
|
|
<plugins>
|
|
|
<!-- clean lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#clean_Lifecycle -->
|
|
|
<plugin>
|
|
|
<artifactId>maven-clean-plugin</artifactId>
|
|
|
<version>3.1.0</version>
|
|
|
</plugin>
|
|
|
<!-- default lifecycle, jar packaging: see https://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_jar_packaging -->
|
|
|
<plugin>
|
|
|
<artifactId>maven-resources-plugin</artifactId>
|
|
|
<version>3.0.2</version>
|
|
|
</plugin>
|
|
|
<plugin>
|
|
|
<artifactId>maven-compiler-plugin</artifactId>
|
|
|
<version>3.8.0</version>
|
|
|
</plugin>
|
|
|
<plugin>
|
|
|
<artifactId>maven-surefire-plugin</artifactId>
|
|
|
<version>2.22.1</version>
|
|
|
</plugin>
|
|
|
<plugin>
|
|
|
<artifactId>maven-jar-plugin</artifactId>
|
|
|
<version>3.0.2</version>
|
|
|
</plugin>
|
|
|
<plugin>
|
|
|
<artifactId>maven-install-plugin</artifactId>
|
|
|
<version>2.5.2</version>
|
|
|
</plugin>
|
|
|
<plugin>
|
|
|
<artifactId>maven-deploy-plugin</artifactId>
|
|
|
<version>2.8.2</version>
|
|
|
</plugin>
|
|
|
<!-- site lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#site_Lifecycle -->
|
|
|
<plugin>
|
|
|
<artifactId>maven-site-plugin</artifactId>
|
|
|
<version>3.7.1</version>
|
|
|
</plugin>
|
|
|
<plugin>
|
|
|
<artifactId>maven-project-info-reports-plugin</artifactId>
|
|
|
<version>3.0.0</version>
|
|
|
</plugin>
|
|
|
<plugin>
|
|
|
<artifactId>maven-war-plugin</artifactId>
|
|
|
<version>3.3.2</version>
|
|
|
</plugin>
|
|
|
</plugins>
|
|
|
</pluginManagement>
|
|
|
</build>
|
|
|
</project>
|
|
|
```
|
|
|
|
|
|
Create the Web application configuration file `src/main/webapp/WEB-INF/web.xml` with the following contents:
|
|
|
|
|
|
```xml
|
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
|
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd" version="5.0">
|
|
|
<display-name>Oldenburg Workshop Simulator</display-name>
|
|
|
|
|
|
<!-- Files to look for when one is not specified. -->
|
|
|
<welcome-file-list>
|
|
|
<welcome-file>index.xhtml</welcome-file>
|
|
|
</welcome-file-list>
|
|
|
|
|
|
<!-- Delegates rendering of XHTML pages to JSF. -->
|
|
|
<servlet>
|
|
|
<servlet-name>Faces Servlet</servlet-name>
|
|
|
<servlet-class>jakarta.faces.webapp.FacesServlet</servlet-class>
|
|
|
<load-on-startup>1</load-on-startup>
|
|
|
</servlet>
|
|
|
<servlet-mapping>
|
|
|
<servlet-name>Faces Servlet</servlet-name>
|
|
|
<url-pattern>*.xhtml</url-pattern>
|
|
|
</servlet-mapping>
|
|
|
|
|
|
<!-- JSF and PrimeFaces configuration. -->
|
|
|
<context-param>
|
|
|
<param-name>jakarta.faces.DATETIMECONVERTER_DEFAULT_TIMEZONE_IS_SYSTEM_TIMEZONE</param-name>
|
|
|
<param-value>true</param-value>
|
|
|
</context-param>
|
|
|
<context-param>
|
|
|
<param-name>jakarta.faces.PROJECT_STAGE</param-name>
|
|
|
<param-value>Development</param-value>
|
|
|
</context-param>
|
|
|
<context-param>
|
|
|
<param-name>jakarta.faces.INTERPRET_EMPTY_STRING_SUBMITTED_VALUES_AS_NULL</param-name>
|
|
|
<param-value>true</param-value>
|
|
|
</context-param>
|
|
|
<context-param>
|
|
|
<param-name>jakarta.faces.FACELETS_SKIP_COMMENTS</param-name>
|
|
|
<param-value>true</param-value>
|
|
|
</context-param>
|
|
|
|
|
|
<!-- Session timeout in minutes. -->
|
|
|
<session-config>
|
|
|
<session-timeout>30</session-timeout>
|
|
|
</session-config>
|
|
|
|
|
|
<!-- Datasource configuration. -->
|
|
|
<data-source>
|
|
|
<name>java:app/datasources/oldenburg</name>
|
|
|
<class-name>com.mysql.cj.jdbc.MysqlDataSource</class-name>
|
|
|
<server-name>localhost</server-name>
|
|
|
<port-number>3306</port-number>
|
|
|
<database-name>oldenburg</database-name>
|
|
|
<user>labes</user>
|
|
|
<password><![CDATA[labes]]></password>
|
|
|
</data-source>
|
|
|
</web-app>
|
|
|
```
|
|
|
|
|
|
Create an empty CDI configuration file `src/main/webapp/WEB-INF/beans.xml` with the following contents:
|
|
|
|
|
|
```xml
|
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
|
<beans xmlns="https://jakarta.ee/xml/ns/jakartaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/beans_3_0.xsd" version="3.0" bean-discovery-mode="annotated">
|
|
|
</beans>
|
|
|
```
|
|
|
|
|
|
Create a JSF configuration file `src/main/webapp/WEB-INF/faces-config.xml` with the following contents:
|
|
|
|
|
|
```xml
|
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
|
<faces-config xmlns="https://jakarta.ee/xml/ns/jakartaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-facesconfig_3_0.xsd" version="3.0">
|
|
|
<application>
|
|
|
<!-- Defines the resource bundle that contains the standard JSF messages
|
|
|
(overriding the ones provided). -->
|
|
|
<message-bundle>br.ufes.inf.labes.oldenburg.faces</message-bundle>
|
|
|
|
|
|
<!-- Loads resource bundles for i18n messages and assigns names to them. -->
|
|
|
<resource-bundle>
|
|
|
<base-name>br.ufes.inf.labes.oldenburg.messages</base-name>
|
|
|
<var>msgs</var>
|
|
|
</resource-bundle>
|
|
|
</application>
|
|
|
</faces-config>
|
|
|
```
|
|
|
|
|
|
Notice that the JSF configuration points to a resource bundle for JSF and one for Oldenburg (which will also contain some text for JButler use). These bundles are useful if you want to translate your system to other languages later on. Create the former (the JSF bundle) in `src/main/resources/br/ufes/inf/labes/oldenburg/faces.properties` with the following contents:
|
|
|
|
|
|
```sh
|
|
|
##
|
|
|
## Standard localized error and informational messages from the JSF 3.0 specification (https://jakarta.ee/specifications/faces/3.0/jakarta-faces-3.0.html#a584)
|
|
|
## Language: American English
|
|
|
##
|
|
|
|
|
|
jakarta.faces.component.UIInput.CONVERSION={0}: Conversion error occurred
|
|
|
jakarta.faces.component.UIInput.REQUIRED={0}: Validation Error: Value is required
|
|
|
jakarta.faces.component.UIInput.UPDATE={0}: An error occurred when processing your submitted information
|
|
|
jakarta.faces.component.UISelectOne.INVALID={0}: Validation Error: Value is not valid
|
|
|
jakarta.faces.component.UISelectMany.INVALID={0}: Validation Error: Value is not valid
|
|
|
jakarta.faces.converter.BigDecimalConverter.DECIMAL={2}: ''{0}'' must be a signed decimal number.
|
|
|
jakarta.faces.converter.BigDecimalConverter.DECIMAL_detail={2}: ''{0}'' must be a signed decimal number consisting of zero or more digits, that may be followed by a decimal point and fraction. Example: {1}
|
|
|
jakarta.faces.converter.BigIntegerConverter.BIGINTEGER={2}: ''{0}'' must be a number consisting of one or more digits.
|
|
|
jakarta.faces.converter.BigIntegerConverter.BIGINTEGER_detail={2}: ''{0}'' must be a number consisting of one or more digits. Example: {1}
|
|
|
jakarta.faces.converter.BooleanConverter.BOOLEAN={1}: ''{0}'' must be 'true' or 'false'.
|
|
|
jakarta.faces.converter.BooleanConverter.BOOLEAN_detail={1}: ''{0}'' must be 'true' or 'false'. Any value other than 'true' will evaluate to 'false'.
|
|
|
jakarta.faces.converter.ByteConverter.BYTE={2}: ''{0}'' must be a number between -128 and 127.
|
|
|
jakarta.faces.converter.ByteConverter.BYTE_detail={2}: ''{0}'' must be a number between -128 and 127. Example: {1}
|
|
|
jakarta.faces.converter.CharacterConverter.CHARACTER={1}: ''{0}'' must be a valid character.
|
|
|
jakarta.faces.converter.CharacterConverter.CHARACTER_detail={1}: ''{0}'' must be a valid ASCII character.
|
|
|
jakarta.faces.converter.DateTimeConverter.DATE={2}: ''{0}'' could not be understood as a date.
|
|
|
jakarta.faces.converter.DateTimeConverter.DATE_detail={2}: ''{0}'' could not be understood as a date. Example: {1}
|
|
|
jakarta.faces.converter.DateTimeConverter.TIME={2}: ''{0}'' could not be understood as a time.
|
|
|
jakarta.faces.converter.DateTimeConverter.TIME_detail={2}: ''{0}'' could not be understood as a time. Example: {1}
|
|
|
jakarta.faces.converter.DateTimeConverter.DATETIME={2}: ''{0}'' could not be understood as a date and time.
|
|
|
jakarta.faces.converter.DateTimeConverter.DATETIME_detail={2}: ''{0}'' could not be understood as a date and time. Example: {1}
|
|
|
jakarta.faces.converter.DateTimeConverter.PATTERN_TYPE={1}: A 'pattern' or 'type' attribute must be specified to convert the value ''{0}''.
|
|
|
jakarta.faces.converter.DoubleConverter.DOUBLE={2}: ''{0}'' must be a number consisting of one or more digits.
|
|
|
jakarta.faces.converter.DoubleConverter.DOUBLE_detail={2}: ''{0}'' must be a number between 4.9E-324 and 1.7976931348623157E308 Example: {1}
|
|
|
jakarta.faces.converter.EnumConverter.ENUM={2}: ''{0}'' must be convertible to an enum.
|
|
|
jakarta.faces.converter.EnumConverter.ENUM_detail={2}: ''{0}'' must be convertible to an enum from the enum that contains the constant ''{1}''.
|
|
|
jakarta.faces.converter.EnumConverter.ENUM_NO_CLASS={1}: ''{0}'' must be convertible to an enum from the enum, but no enum class provided.
|
|
|
jakarta.faces.converter.EnumConverter.ENUM_NO_CLASS_detail={1}: ''{0}'' must be convertible to an enum from the enum, but no enum class provided.
|
|
|
jakarta.faces.converter.FloatConverter.FLOAT={2}: ''{0}'' must be a number consisting of one or more digits.
|
|
|
jakarta.faces.converter.FloatConverter.FLOAT_detail={2}: ''{0}'' must be a number between 1.4E-45 and 3.4028235E38 Example: {1}
|
|
|
jakarta.faces.converter.IntegerConverter.INTEGER={2}: ''{0}'' must be a number consisting of one or more digits.
|
|
|
jakarta.faces.converter.IntegerConverter.INTEGER_detail={2}: ''{0}'' must be a number between -2147483648 and 2147483647 Example: {1}
|
|
|
jakarta.faces.converter.LongConverter.LONG={2}: ''{0}'' must be a number consisting of one or more digits.
|
|
|
jakarta.faces.converter.LongConverter.LONG_detail={2}: ''{0}'' must be a number between -9223372036854775808 to 9223372036854775807 Example: {1}
|
|
|
jakarta.faces.converter.NumberConverter.CURRENCY={2}: ''{0}'' could not be understood as a currency value.
|
|
|
jakarta.faces.converter.NumberConverter.CURRENCY_detail={2}: ''{0}'' could not be understood as a currency value. Example: {1}
|
|
|
jakarta.faces.converter.NumberConverter.PERCENT={2}: ''{0}'' could not be understood as a percentage.
|
|
|
jakarta.faces.converter.NumberConverter.PERCENT_detail={2}: ''{0}'' could not be understood as a percentage. Example: {1}
|
|
|
jakarta.faces.converter.NumberConverter.NUMBER={2}: ''{0}'' is not a number.
|
|
|
jakarta.faces.converter.NumberConverter.NUMBER_detail={2}: ''{0}'' is not a number. Example: {1}
|
|
|
jakarta.faces.converter.NumberConverter.PATTERN={2}: ''{0}'' is not a number pattern.
|
|
|
jakarta.faces.converter.NumberConverter.PATTERN_detail={2}: ''{0}'' is not a number pattern. Example: {1}
|
|
|
jakarta.faces.converter.ShortConverter.SHORT={2}: ''{0}'' must be a number consisting of one or more digits.
|
|
|
jakarta.faces.converter.ShortConverter.SHORT_detail={2}: ''{0}'' must be a number between -32768 and 32767 Example: {1}
|
|
|
jakarta.faces.converter.STRING={1}: Could not convert ''{0}'' to a string.
|
|
|
jakarta.faces.validator.BeanValidator.MESSAGE={0}
|
|
|
jakarta.faces.validator.DoubleRangeValidator.MAXIMUM={1}: Validation Error: Value is greater than allowable maximum of ‘’{0}’’
|
|
|
jakarta.faces.validator.DoubleRangeValidator.MINIMUM={1}: Validation Error: Value is less than allowable minimum of ‘’{0}’’
|
|
|
jakarta.faces.validator.DoubleRangeValidator.NOT_IN_RANGE={2}: Validation Error: Specified attribute is not between the expected values of {0} and {1}.
|
|
|
jakarta.faces.validator.DoubleRangeValidator.TYPE={0}: Validation Error: Value is not of the correct type
|
|
|
jakarta.faces.validator.LengthValidator.MAXIMUM={1}: Validation Error: Length is greater than allowable maximum of ‘’{0}’’
|
|
|
jakarta.faces.validator.LengthValidator.MINIMUM={1}: Validation Error: Length is less than allowable minimum of ‘’{0}’’
|
|
|
jakarta.faces.validator.LongRangeValidator.MAXIMUM={1}: Validation Error: Value is greater than allowable maximum of ‘’{0}’’
|
|
|
jakarta.faces.validator.LongRangeValidator.MINIMUM={1}: Validation Error Value is less than allowable minimum of ‘’{0}’’
|
|
|
jakarta.faces.validator.LongRangeValidator.NOT_IN_RANGE={2}: Validation Error: Specified attribute is not between the expected values of {0} and {1}.
|
|
|
jakarta.faces.validator.LongRangeValidator.TYPE={0}: Validation Error: Value is not of the correct type
|
|
|
```
|
|
|
|
|
|
Finally, create the latter (the Oldenburg bundle) in `src/main/resources/br/ufes/inf/labes/oldenburg/messages.properties` with the following contents:
|
|
|
|
|
|
```sh
|
|
|
##
|
|
|
## Global resource bundle for a JButler project.
|
|
|
## Language: American English
|
|
|
##
|
|
|
|
|
|
# Messages for PrimeFaces components.
|
|
|
primefaces.password.prompt = Please enter a password
|
|
|
primefaces.password.weak = Weak
|
|
|
primefaces.password.good = Good
|
|
|
primefaces.password.strong = Strong
|
|
|
|
|
|
# For JButler's base project decorator:
|
|
|
jbutler.text.ajax.loading = Loading data, please wait...
|
|
|
jbutler.text.ajax.error = There has been an error communicating with the server.
|
|
|
jbutler.text.ajax.complete = Last communication with the server successful.
|
|
|
|
|
|
# Generic messages for JButler's CRUD web pages:
|
|
|
jbutler.crud.button.add = Add
|
|
|
jbutler.crud.button.filter = Filter
|
|
|
jbutler.crud.button.cancelFilter = Cancel
|
|
|
jbutler.crud.button.clearFilter = Clear
|
|
|
jbutler.crud.button.create = New
|
|
|
jbutler.crud.button.retrieve = View
|
|
|
jbutler.crud.button.update = Modify
|
|
|
jbutler.crud.button.delete = Delete
|
|
|
jbutler.crud.button.cancelDeletion = Cancel
|
|
|
jbutler.crud.button.confirmDeletion = Confirm deletion
|
|
|
jbutler.crud.button.cancel = Cancel
|
|
|
jbutler.crud.button.save = Save
|
|
|
jbutler.crud.button.back = Back
|
|
|
jbutler.crud.button.yes = Yes
|
|
|
jbutler.crud.button.no = No
|
|
|
jbutler.crud.help.hotkeys.filterFocus = focus the filter field
|
|
|
jbutler.crud.help.hotkeys.clearFilter = clear current filter
|
|
|
jbutler.crud.help.hotkeys.create = open the form for a new entry
|
|
|
jbutler.crud.help.hotkeys.retrieve = open the details of the selected item
|
|
|
jbutler.crud.help.hotkeys.update = open the form for modifying the selected item
|
|
|
jbutler.crud.help.hotkeys.delete = add the selected item to the trash bin for deletion
|
|
|
jbutler.crud.help.hotkeys.cancelDeletion = cancel deletion and restore the items in the trash bin
|
|
|
jbutler.crud.help.hotkeys.confirmDeletion = confirm the deletion of the items in the trash bin
|
|
|
jbutler.crud.help.hotkeys.focusFirstField = focus the first form field
|
|
|
jbutler.crud.help.hotkeys.backToList = go back to the listing
|
|
|
jbutler.crud.help.hotkeys.save = save changes and go back to the listing
|
|
|
jbutler.crud.help.hotkeys.cancel = cancel the operation and go back
|
|
|
jbutler.crud.help.hotkeys.back = go back
|
|
|
jbutler.crud.hotkey.filterFocus = f
|
|
|
jbutler.crud.hotkey.clearFilter = x
|
|
|
jbutler.crud.hotkey.create = c
|
|
|
jbutler.crud.hotkey.retrieve = r
|
|
|
jbutler.crud.hotkey.update = u
|
|
|
jbutler.crud.hotkey.delete = d
|
|
|
jbutler.crud.hotkey.cancelDeletion = esc
|
|
|
jbutler.crud.hotkey.confirmDeletion = y
|
|
|
jbutler.crud.hotkey.focusFirstField = f
|
|
|
jbutler.crud.hotkey.backToList = esc
|
|
|
jbutler.crud.hotkey.cancel = esc
|
|
|
jbutler.crud.hotkey.back = esc
|
|
|
jbutler.crud.text.search = Search
|
|
|
jbutler.crud.text.trashHeader = Items to delete:
|
|
|
jbutler.crud.title.confirmation = Confirmation
|
|
|
|
|
|
# JButler default formats:
|
|
|
jbutler.format.date.java=dd/MM/yyyy
|
|
|
jbutler.format.date.primefaces=99/99/9999
|
|
|
jbutler.format.date.label=dd/mm/aaaa
|
|
|
jbutler.format.datetime.java=dd/MM/yyyy HH:mm:ss
|
|
|
jbutler.format.taxCode.primefaces=999.999.999-99
|
|
|
jbutler.format.taxCode.label=\#\#\#.\#\#\#.\#\#\#-\#\#
|
|
|
jbutler.format.zipCode.primefaces=99999-999
|
|
|
jbutler.format.zipCode.label=\#\#\#\#\#-\#\#\#
|
|
|
|
|
|
# JButler regular expressions and validator messages.
|
|
|
jbutler.regex.email=([^.@]+)(\\.[^.@]+)*@([^.@]+\\.)+([^.@]+)
|
|
|
jbutler.regex.confirmationCode=[\\w\\-]\{36\}
|
|
|
jbutler.regex.email.message = This is not a valid email address.
|
|
|
jbutler.regex.confirmationCode.message = This confirmation code given is not valid.
|
|
|
|
|
|
# Global messages for the Web application.
|
|
|
menu.home = Home
|
|
|
text.backToIndex = Back to the start
|
|
|
text.developedBy = Developed by <a href="http://www.inf.ufes.br/~vitorsouza/">prof. Vítor E. Silva Souza</a> from <a href="http://labes.inf.ufes.br/">LabES/UFES</a>.
|
|
|
text.home.about01 = This system aims to help professors teaching Research Methodology courses to simulate a workshop in which the students will submit papers and perform peer review.
|
|
|
text.home.about02 = The application is named after <a href="https://en.wikipedia.org/wiki/Henry_Oldenburg">Henry Oldenburg</a> which, according to Wikipedia, is the creator of scientific peer review.
|
|
|
title.home = Home
|
|
|
title.home.description = Welcome to the Oldenburg Workshop Simulator!
|
|
|
```
|
|
|
|
|
|
If you want, change `text.developedBy` and add your name, as you are developing Oldenburg now. :)
|
|
|
|
|
|
|
|
|
|
|
|
## Set up AdminFaces, a ready-to-use front-end
|
|
|
|
|
|
When we don't have front-end designers working on our teams, we can use some generic ones, such as [AdminFaces](https://github.com/adminfaces), which is based on [AdminLTE](https://almsaeedstudio.com/themes/AdminLTE/index2.html) and [Bootstrap](http://getbootstrap.com/), but for JSF.
|
|
|
|
|
|
A dependency for `admin-template` was already included in our `pom.xml`, so to set up AdminFaces, create its configuration file `src/main/resources/admin-config.properties` with the following contents:
|
|
|
|
|
|
```sh
|
|
|
# Configures the AdminFilter to allow non-authenticated users in certain pages.
|
|
|
admin.ignoredResources = /public,/resources
|
|
|
|
|
|
# Disables the AJAX status bar from being rendered in every AJAX request.
|
|
|
admin.renderAjaxStatus = false
|
|
|
|
|
|
# Disable the menu search field.
|
|
|
admin.renderMenuSearch = false
|
|
|
```
|
|
|
|
|
|
Create the resource bundle (again, useful if you want to translate your system to other languages later on) for AdminFaces in `src/main/resources/admin.properties` with the following contents:
|
|
|
|
|
|
```sh
|
|
|
##
|
|
|
## Resource bundle for AdminFaces.
|
|
|
## Language: American English
|
|
|
##
|
|
|
|
|
|
#general
|
|
|
admin.version=${project.version}
|
|
|
label.home=Home
|
|
|
label.go-back=Go back to
|
|
|
label.or-to=or to
|
|
|
label.previous-page=previous page
|
|
|
|
|
|
#403
|
|
|
label.403.header=403
|
|
|
label.403.message=Access denied! You do not have access to the requested page.
|
|
|
|
|
|
#404
|
|
|
label.404.header=404
|
|
|
label.404.message=Oops! Page not found
|
|
|
|
|
|
#500
|
|
|
label.500.header=500
|
|
|
label.500.message=Oops! Something went wrong
|
|
|
label.500.title=Unexpected error
|
|
|
label.500.detail=Details
|
|
|
|
|
|
#expired
|
|
|
label.expired.title=View expired
|
|
|
label.expired.message= The requested page could not be recovered.
|
|
|
label.expired.click-here= Click here to reload the page.
|
|
|
|
|
|
#optimistic
|
|
|
label.optimistic.title=Record already updated
|
|
|
label.optimistic.message= The requested record has been already updated by another user.
|
|
|
label.optimistic.click-here= Click here to reload the updated record from database.
|
|
|
|
|
|
#controlsidebar
|
|
|
controlsidebar.header=Layout Options
|
|
|
controlsidebar.label.restore-defaults=Restore defaults
|
|
|
controlsidebar.label.menu-horientation=Left menu layout
|
|
|
controlsidebar.txt.menu-horientation=Toggle menu orientation between <b class\="sidebar-bold">left</b> and <b class\="sidebar-bold">top</b> menu.
|
|
|
controlsidebar.label.fixed-layout=Fixed Layout
|
|
|
controlsidebar.txt.fixed-layout=Activate the fixed layout, if checked the top bar will be fixed on the page.
|
|
|
controlsidebar.label.boxed-layout=Boxed Layout
|
|
|
controlsidebar.txt.boxed-layout=Activate the boxed layout.
|
|
|
controlsidebar.label.sidebar-collapsed=Collapsed Sidebar
|
|
|
controlsidebar.txt.sidebar-collapsed=If checked the sidebar menu will be collapsed.
|
|
|
controlsidebar.label.sidebar-expand-hover=Sidebar Expand on Hover
|
|
|
controlsidebar.txt.sidebar-expand-hover=If checked the left sidebar will expand on hover.
|
|
|
controlsidebar.label.sidebar-slide=Control Sidebar fixed
|
|
|
controlsidebar.txt.sidebar-slide=If checked control sidebar will be fixed on the page.
|
|
|
controlsidebar.label.sidebar-skin=Dark Sidebar Skin
|
|
|
controlsidebar.txt.sidebar-skin=If checked <b class\="sidebar-bold">dark</b> skin will be used for control sidebar, otherwise <b class\="sidebar-bold">light</b> skin will be used.
|
|
|
controlsidebar.header.skins=Skins
|
|
|
|
|
|
#menu search
|
|
|
menu.search.placeholder=Search menu items...
|
|
|
```
|
|
|
|
|
|
Next, we create a Facelets template based on AdminFaces to customize the appearance of Oldenburg a little bit. Create the file `src/main/webapp/WEB-INF/templates/template.xhtml` with the following contents:
|
|
|
|
|
|
```xml
|
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
|
<ui:composition
|
|
|
xmlns="http://www.w3.org/1999/xhtml"
|
|
|
xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
|
|
|
xmlns:f="http://xmlns.jcp.org/jsf/core"
|
|
|
xmlns:h="http://xmlns.jcp.org/jsf/html"
|
|
|
xmlns:jsf="http://xmlns.jcp.org/jsf"
|
|
|
xmlns:pt="http://xmlns.jcp.org/jsf/passthrough"
|
|
|
xmlns:p="http://primefaces.org/ui"
|
|
|
template="/admin.xhtml">
|
|
|
|
|
|
<ui:define name="head">
|
|
|
<title><h:outputText value="Oldenburg Workshop Simulator :: " /><ui:insert name="title" /></title>
|
|
|
<h:outputStylesheet library="webjars" name="font-awesome/6.1.0/css/all.min-jsf.css" />
|
|
|
<h:outputStylesheet library="webjars" name="font-awesome/6.1.0/css/v4-shims.min-jsf.css" />
|
|
|
</ui:define>
|
|
|
|
|
|
<ui:define name="logo-lg">
|
|
|
<span class="logo-lg"><b>Oldenburg</b></span>
|
|
|
</ui:define>
|
|
|
|
|
|
<ui:define name="logo-mini">
|
|
|
OWS
|
|
|
</ui:define>
|
|
|
|
|
|
<ui:define name="menu">
|
|
|
<ul class="sidebar-menu">
|
|
|
|
|
|
<!-- Menu entries. -->
|
|
|
<li><p:link outcome="/index">
|
|
|
<i class="fa fa-home"></i>
|
|
|
<span><h:outputText value="#{msgs['menu.home']}" /></span>
|
|
|
</p:link></li>
|
|
|
</ul>
|
|
|
</ui:define>
|
|
|
|
|
|
<ui:define name="footer">
|
|
|
<h:outputText value="#{msgs['text.developedBy']}" escape="false" />
|
|
|
<div class="pull-right hidden-xs" style="color: gray">
|
|
|
<i>1.0.0</i>
|
|
|
</div>
|
|
|
|
|
|
<!-- Customized JButler style. -->
|
|
|
<style type="text/css">
|
|
|
.ui-datatable td, th {
|
|
|
padding: 4px 10px !important;
|
|
|
}
|
|
|
</style>
|
|
|
</ui:define>
|
|
|
</ui:composition>
|
|
|
```
|
|
|
|
|
|
Finally, create the home page at `src/main/webapp/index.xhtml` referring to the template we just created, as below:
|
|
|
|
|
|
```xml
|
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
|
<ui:composition
|
|
|
xmlns="http://www.w3.org/1999/xhtml"
|
|
|
xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
|
|
|
xmlns:f="http://xmlns.jcp.org/jsf/core"
|
|
|
xmlns:h="http://xmlns.jcp.org/jsf/html"
|
|
|
xmlns:jsf="http://xmlns.jcp.org/jsf"
|
|
|
xmlns:pt="http://xmlns.jcp.org/jsf/passthrough"
|
|
|
xmlns:p="http://primefaces.org/ui"
|
|
|
xmlns:adm="http://github.com/adminfaces"
|
|
|
template="/WEB-INF/templates/template.xhtml">
|
|
|
|
|
|
<ui:define name="title">
|
|
|
<h:outputText value="#{msgs['title.home']}" />
|
|
|
</ui:define>
|
|
|
|
|
|
<ui:define name="description">
|
|
|
<h:outputText value="#{msgs['title.home.description']}" />
|
|
|
</ui:define>
|
|
|
|
|
|
<ui:define name="body">
|
|
|
<!-- Clear the breadcrumb. -->
|
|
|
<adm:breadcrumb title="" clear="true" />
|
|
|
|
|
|
<p><h:outputText value="#{msgs['text.home.about01']}" escape="false" /></p>
|
|
|
<p><h:outputText value="#{msgs['text.home.about02']}" escape="false" /></p>
|
|
|
</ui:define>
|
|
|
</ui:composition>
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## Deploy and check it out
|
|
|
|
|
|
At this point you can already deploy and see the home page using the AdminFaces template. You could do it just like before with the JButler Base Project, but for a project you're developing I suggest a slightly different process. On a terminal (you could use the one in VSCode, for instance), run `mvn package` to build the deploy and then the following commands:
|
|
|
|
|
|
```sh
|
|
|
$ export WILDFLY_HOME=/path/to/wildfly/in/you/computer
|
|
|
$ export WILDFLY_DEPLOY=$WILDFLY_HOME/standalone/deployments
|
|
|
$ ln -s `pwd`/target/oldenburg/ $WILDFLY_DEPLOY/oldenburg.war
|
|
|
$ touch $WILDFLY_DEPLOY/oldenburg.war.dodeploy
|
|
|
```
|
|
|
|
|
|
Before `mvn package` produces `target/oldenburg.war`, it assembles it in a folder called `target/oldenburg`, so the above commands create a link to this folder in WildFly's deployments folder. When a folder is detected, WildFly doesn't automatically deploy it, so you need to create the `oldenburg.war.dodeploy` file there as well.
|
|
|
|
|
|
The advantage in this case is that you can change something under `src/main/webapp` (in a web page, CSS, script, etc.) and update it on the server only with `mvn package` (no need to redeploy). If you change something under `src/main/java` or `src/main/resources`, though, you need to redeploy:
|
|
|
|
|
|
```sh
|
|
|
$ mv $WILDFLY_DEPLOY/oldenburg.war.deployed $WILDFLY_DEPLOY/oldenburg.war.dodeploy
|
|
|
```
|
|
|
|
|
|
Finally, to undeploy and remove Oldenburg from the server:
|
|
|
|
|
|
```sh
|
|
|
$ mv $WILDFLY_DEPLOY/oldenburg.war.deployed $WILDFLY_DEPLOY/oldenburg.war.undeploy
|
|
|
$ rm -r $WILDFLY_DEPLOY/oldenburg.*
|
|
|
```
|
|
|
|
|
|
> **Note:** IDEs usually do this for you. In earlier versions of this tutorial, I would start WildFly and deploy the application using Eclipse. When I migrated to VSCode, I couldn't find a WildFly plug-in that worked. Once I do, I can update this tutorial. It's always nice, though, to know how to do things by hand as well. ;)
|
|
|
>
|
|
|
> Also, there's a Maven plug-in that deploys on WildFly, the instructions in the [JavaHostel example repository](https://github.com/dwws-ufes/javahostel/tree/main/jakartaee9) explain how to use it. I find its deploy a bit slower, tough, and I didn't find a way to update only changes under `src/main/webapp` without a full redeploy.
|
|
|
>
|
|
|
> Speaking of updating only `src/main/webapp`, if you're making a lot of small changes there and testing often (e.g., tweaking with CSS or with PrimeFaces tags) and you are annoyed that you have to call `mvn package` every time, there's a trick: make your changes directly under `target/oldenburg` and just reload the page on the browser. Remember, however, that once you are satisfied you need to copy the result back to `src/main/webapp`, otherwise the next `mvn package` will override it!
|
|
|
|
|
|
|
|
|
|
|
|
## Implement the domain and persistence classes
|
|
|
|
|
|
As explained in [JButler Architecture](docs/JButler-Architecture), applications that use JButler should follow a specific division of packages to make integration with JButler easier. We will start with domain and persistence classes, which JButler helps you write even if you're not going to build a CRUD for them.
|
|
|
|
|
|
In Oldenburg, the first thing one needs to create is a workshop, specifying a name (e.g., _Research Methodology Workshop 2022_), an acronym (e.g., _RMW2022_), a year (a number) and submission and review deadlines (dates). Thus, under `src/main/java`, create the `br.ufes.inf.labes.oldenburg.core.domain.Workshop` class as follows:
|
|
|
|
|
|
```java
|
|
|
package br.ufes.inf.labes.oldenburg.core.domain;
|
|
|
|
|
|
import java.time.LocalDate;
|
|
|
import br.ufes.inf.labes.jbutler.ejb.persistence.PersistentObjectSupport;
|
|
|
import jakarta.persistence.Entity;
|
|
|
import jakarta.validation.constraints.NotNull;
|
|
|
import jakarta.validation.constraints.Size;
|
|
|
|
|
|
@Entity
|
|
|
public class Workshop extends PersistentObjectSupport implements Comparable<Workshop> {
|
|
|
@Size(max = 100)
|
|
|
private String name;
|
|
|
|
|
|
@Size(max = 10)
|
|
|
private String acronym;
|
|
|
|
|
|
@NotNull
|
|
|
private int year;
|
|
|
|
|
|
@NotNull
|
|
|
private LocalDate submissionDeadline;
|
|
|
|
|
|
@NotNull
|
|
|
private LocalDate reviewDeadline;
|
|
|
|
|
|
public String getName() {
|
|
|
return name;
|
|
|
}
|
|
|
|
|
|
public void setName(String name) {
|
|
|
this.name = name;
|
|
|
}
|
|
|
|
|
|
public String getAcronym() {
|
|
|
return acronym;
|
|
|
}
|
|
|
|
|
|
public void setAcronym(String acronym) {
|
|
|
this.acronym = acronym;
|
|
|
}
|
|
|
|
|
|
public int getYear() {
|
|
|
return year;
|
|
|
}
|
|
|
|
|
|
public void setYear(int year) {
|
|
|
this.year = year;
|
|
|
}
|
|
|
|
|
|
public LocalDate getSubmissionDeadline() {
|
|
|
return submissionDeadline;
|
|
|
}
|
|
|
|
|
|
public void setSubmissionDeadline(LocalDate submissionDeadline) {
|
|
|
this.submissionDeadline = submissionDeadline;
|
|
|
}
|
|
|
|
|
|
public LocalDate getReviewDeadline() {
|
|
|
return reviewDeadline;
|
|
|
}
|
|
|
|
|
|
public void setReviewDeadline(LocalDate reviewDeadline) {
|
|
|
this.reviewDeadline = reviewDeadline;
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
public int compareTo(Workshop o) {
|
|
|
return year - o.year;
|
|
|
}
|
|
|
}
|
|
|
```
|
|
|
|
|
|
By extending JButler's `PersistentObjectSupport`, our domain class inherits persistent ID and version attributes, plus implementations of `equals()` and `hashCode()` based on an `UUID`. This way we can focus on our application's business logic and our domain class is done.
|
|
|
|
|
|
In the persistence side, JButler works with [Data Access Objects (DAOs)](https://en.wikipedia.org/wiki/Data_access_object) implemented as stateless EJBs. Since there aren't any specific queries or persistence operations on Workshop objects in our business logic, we will create a very simple DAO that just inherits (almost) everything from JButler's utility classes. First, create a DAO interface `br.ufes.inf.labes.oldenburg.core.persistence.WorkshopDAO` (under `src/main/java` as usual) for Workshop objects:
|
|
|
|
|
|
```java
|
|
|
package br.ufes.inf.labes.oldenburg.core.persistence;
|
|
|
|
|
|
import br.ufes.inf.labes.jbutler.ejb.persistence.BaseDAO;
|
|
|
import br.ufes.inf.labes.oldenburg.core.domain.Workshop;
|
|
|
import jakarta.ejb.Local;
|
|
|
|
|
|
@Local
|
|
|
public interface WorkshopDAO extends BaseDAO<Workshop> {
|
|
|
}
|
|
|
```
|
|
|
|
|
|
Then, create the implementation (class) `br.ufes.inf.labes.oldenburg.core.persistence.WorkshopJPADAO` for the Workshop DAO:
|
|
|
|
|
|
```java
|
|
|
package br.ufes.inf.labes.oldenburg.core.persistence;
|
|
|
|
|
|
import br.ufes.inf.labes.jbutler.ejb.persistence.BaseJPADAO;
|
|
|
import br.ufes.inf.labes.oldenburg.core.domain.Workshop;
|
|
|
import jakarta.ejb.Stateless;
|
|
|
import jakarta.persistence.EntityManager;
|
|
|
import jakarta.persistence.PersistenceContext;
|
|
|
|
|
|
@Stateless
|
|
|
public class WorkshopJPADAO extends BaseJPADAO<Workshop> implements WorkshopDAO {
|
|
|
@PersistenceContext
|
|
|
private EntityManager entityManager;
|
|
|
|
|
|
@Override
|
|
|
protected EntityManager getEntityManager() {
|
|
|
return entityManager;
|
|
|
}
|
|
|
}
|
|
|
```
|
|
|
|
|
|
The only thing JButler requires for a basic DAO implementation is for it to provide the `EntityManager` that can only be injected in our object, can't do that in the JButler superclass. This class gets in return the following persistent operations on `Workshop` objects, already implemented by JButler:
|
|
|
|
|
|
- Count;
|
|
|
- Retrieve all;
|
|
|
- Retrieve some, with pagination;
|
|
|
- Retrieve one, given its ID;
|
|
|
- Retrieve one, given its UUID;
|
|
|
- Save;
|
|
|
- Delete.
|
|
|
|
|
|
For all this to work, we need to configure JPA by creating the file `src/main/resources/META-INF/persistence.xml` with the following contents:
|
|
|
|
|
|
```xml
|
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
|
<persistence xmlns="https://jakarta.ee/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd" version="3.0">
|
|
|
<persistence-unit name="oldenburg" transaction-type="JTA">
|
|
|
<provider>org.hibernate.ejb.HibernatePersistence</provider>
|
|
|
<jta-data-source>java:app/datasources/oldenburg</jta-data-source>
|
|
|
|
|
|
<properties>
|
|
|
<property name="hibernate.hbm2ddl.auto" value="update" />
|
|
|
<property name="hibernate.dialect" value="org.hibernate.dialect.MySQL8Dialect" />
|
|
|
</properties>
|
|
|
</persistence-unit>
|
|
|
</persistence>
|
|
|
```
|
|
|
|
|
|
Finally, we also need to create the `oldenburg` schema in MySQL and give the `labes` user "full" privileges on it, just like before with the `mysystem` schema for the [JButler Base Project](https://gitlab.labes.inf.ufes.br/labes/jbutler-base-project) deployment.
|
|
|
|
|
|
|
|
|
|
|
|
## Implement the CRUD (application, controller and view)
|
|
|
|
|
|
Finally, let's implement the CRUD, which will require a service class in the `application` package (which JButler implements as stateless EJBs, just like DAOs), a controller class in the `control` package, a new Web page and a link to that Web page in the template.
|
|
|
|
|
|
Starting with the service, create the EJB interface `br.ufes.inf.labes.oldenburg.core.application.ManageWorkshopsService`:
|
|
|
|
|
|
```java
|
|
|
package br.ufes.inf.labes.oldenburg.core.application;
|
|
|
|
|
|
import br.ufes.inf.labes.jbutler.ejb.application.CrudService;
|
|
|
import br.ufes.inf.labes.oldenburg.core.domain.Workshop;
|
|
|
import jakarta.ejb.Local;
|
|
|
|
|
|
@Local
|
|
|
public interface ManageWorkshopsService extends CrudService<Workshop> {
|
|
|
}
|
|
|
```
|
|
|
|
|
|
Then, the implementation (class) `br.ufes.inf.labes.oldenburg.core.application.ManageWorkshopsServiceBean`:
|
|
|
|
|
|
```java
|
|
|
package br.ufes.inf.labes.oldenburg.core.application;
|
|
|
|
|
|
import br.ufes.inf.labes.jbutler.ejb.application.CrudServiceImpl;
|
|
|
import br.ufes.inf.labes.jbutler.ejb.persistence.BaseDAO;
|
|
|
import br.ufes.inf.labes.oldenburg.core.domain.Workshop;
|
|
|
import br.ufes.inf.labes.oldenburg.core.persistence.WorkshopDAO;
|
|
|
import jakarta.annotation.security.PermitAll;
|
|
|
import jakarta.ejb.EJB;
|
|
|
import jakarta.ejb.Stateless;
|
|
|
|
|
|
@Stateless
|
|
|
@PermitAll
|
|
|
public class ManageWorkshopsServiceBean extends CrudServiceImpl<Workshop>
|
|
|
implements ManageWorkshopsService {
|
|
|
@EJB
|
|
|
private WorkshopDAO workshopDAO;
|
|
|
|
|
|
@Override
|
|
|
public BaseDAO<Workshop> getDAO() {
|
|
|
return workshopDAO;
|
|
|
}
|
|
|
}
|
|
|
```
|
|
|
|
|
|
Similar to the DAO, if our CRUD is very simple and requires nothing different than what JButler already provides, the EJB interface is empty and the implementation only needs to provide the DAO that gets injected (just like the `EntityManager` in the DAO). JButler does the rest, providing CRUD operations and support for validation of business logic (out of scope of this simple tutorial, see [JButler CRUD Validation](tutorials/JButler-CRUD-Validation)).
|
|
|
|
|
|
Next, create the controller class `br.ufes.inf.labes.oldenburg.core.control.ManageWorkshopsController`:
|
|
|
|
|
|
```java
|
|
|
package br.ufes.inf.labes.oldenburg.core.control;
|
|
|
|
|
|
import br.ufes.inf.labes.jbutler.ejb.application.CrudService;
|
|
|
import br.ufes.inf.labes.jbutler.ejb.controller.CrudController;
|
|
|
import br.ufes.inf.labes.oldenburg.core.application.ManageWorkshopsService;
|
|
|
import br.ufes.inf.labes.oldenburg.core.domain.Workshop;
|
|
|
import jakarta.ejb.EJB;
|
|
|
import jakarta.faces.view.ViewScoped;
|
|
|
import jakarta.inject.Named;
|
|
|
|
|
|
@Named
|
|
|
@ViewScoped
|
|
|
public class ManageWorkshopsController extends CrudController<Workshop> {
|
|
|
@EJB
|
|
|
private ManageWorkshopsService manageWorkshopsService;
|
|
|
|
|
|
@Override
|
|
|
protected CrudService<Workshop> getCrudService() {
|
|
|
return manageWorkshopsService;
|
|
|
}
|
|
|
}
|
|
|
```
|
|
|
|
|
|
Again, our CRUD doesn't have any particular features, so all it needs to provide is the CRUD service class and JButler does the rest, providing all the communication needed with the CRUD page.
|
|
|
|
|
|
The [JButler Base Project](https://gitlab.labes.inf.ufes.br/labes/jbutler-base-project) has different examples of views that can be used for a JButler CRUD. Based on the simplest one, create the CRUD view in the `src/main/webapp/core/manageWorkshops/index.xhtml` file, with the following contents:
|
|
|
|
|
|
```xml
|
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
|
<ui:composition
|
|
|
xmlns="http://www.w3.org/1999/xhtml"
|
|
|
xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
|
|
|
xmlns:f="http://xmlns.jcp.org/jsf/core"
|
|
|
xmlns:h="http://xmlns.jcp.org/jsf/html"
|
|
|
xmlns:jsf="http://xmlns.jcp.org/jsf"
|
|
|
xmlns:pt="http://xmlns.jcp.org/jsf/passthrough"
|
|
|
xmlns:p="http://primefaces.org/ui"
|
|
|
xmlns:adm="http://github.com/adminfaces"
|
|
|
template="/WEB-INF/templates/template.xhtml">
|
|
|
|
|
|
<ui:define name="title">
|
|
|
<h:outputText value="#{msgsCore['manageWorkshops.title']}" />
|
|
|
</ui:define>
|
|
|
|
|
|
<ui:define name="description">
|
|
|
<h:outputText value="#{msgsCore['manageWorkshops.title.description']}" />
|
|
|
</ui:define>
|
|
|
|
|
|
<ui:define name="body">
|
|
|
<adm:breadcrumb link="/core/manageWorkshops/index" title="#{msgsCore['manageWorkshops.title']}" />
|
|
|
|
|
|
<h:form id="form">
|
|
|
<!-- The listing. -->
|
|
|
<p:panel header="#{msgsCore['manageWorkshops.text.entities']}" styleClass="card no-border">
|
|
|
<p:dataTable id="dt-entities" widgetVar="dtEntities" var="entity" value="#{manageWorkshopsController.entities}" emptyMessage="#{msgsCore['manageWorkshops.text.emptyMessage']}" reflow="true" selection="#{manageWorkshopsController.selectedEntities}" rowKey="#{entity.id}" paginator="true" rows="10" rowSelectMode="add" paginatorPosition="bottom">
|
|
|
<p:ajax event="filter" ignoreAutoUpdate="true" />
|
|
|
<f:facet name="footer">
|
|
|
<!-- New and Delete buttons. -->
|
|
|
<div style="float: right; margin-top: -40px;">
|
|
|
<p:commandButton value="#{msgs['jbutler.crud.button.create']}" icon="pi pi-plus" actionListener="#{manageWorkshopsController.openNew}" update=":form:manage-entity-content" oncomplete="PF('formDialog').show()" style="margin-right: .5rem">
|
|
|
<p:resetInput target=":form:manage-entity-content" />
|
|
|
</p:commandButton>
|
|
|
<p:commandButton id="delete-entities-button" value="#{msgs['jbutler.crud.button.delete']}" icon="pi pi-trash" actionListener="#{manageWorkshopsController.deleteSelectedEntities}" disabled="#{!manageWorkshopsController.hasSelectedEntities()}" update="@this">
|
|
|
<p:confirm header="#{msgs['jbutler.crud.title.confirmation']}" message="#{msgsCore['manageWorkshops.text.deleteConfirmation']}" icon="pi pi-exclamation-triangle" />
|
|
|
</p:commandButton>
|
|
|
</div>
|
|
|
</f:facet>
|
|
|
|
|
|
<p:ajax event="rowSelect" update="delete-entities-button" />
|
|
|
<p:ajax event="rowUnselect" update="delete-entities-button" />
|
|
|
<p:ajax event="rowSelectCheckbox" update="delete-entities-button" />
|
|
|
<p:ajax event="rowUnselectCheckbox" update="delete-entities-button" />
|
|
|
<p:ajax event="toggleSelect" update="delete-entities-button" />
|
|
|
|
|
|
<p:column width="40" selectionMode="multiple" exportable="false"></p:column>
|
|
|
|
|
|
<!-- Workshop data. -->
|
|
|
<p:column headerText="#{msgsCore['manageWorkshops.field.acronym']}" sortBy="#{entity.acronym}" filterBy="#{entity.acronym}" filterStyle="display: none">
|
|
|
<h:outputText value="#{entity.acronym}" />
|
|
|
</p:column>
|
|
|
<p:column headerText="#{msgsCore['manageWorkshops.field.name']}" sortBy="#{entity.name}" filterBy="#{entity.name}" filterStyle="display: none">
|
|
|
<h:outputText value="#{entity.name}" />
|
|
|
</p:column>
|
|
|
<p:column headerText="#{msgsCore['manageWorkshops.field.year']}" sortBy="#{entity.year}" filterBy="#{entity.year}" filterStyle="display: none">
|
|
|
<h:outputText value="#{entity.acronym}" />
|
|
|
</p:column>
|
|
|
<p:column headerText="#{msgsCore['manageWorkshops.field.submissionDeadline']}" sortBy="#{entity.submissionDeadline}">
|
|
|
<h:outputText value="#{entity.submissionDeadline}">
|
|
|
<f:convertDateTime type="localDate" pattern="#{msgs['jbutler.format.date.java']}" />
|
|
|
</h:outputText>
|
|
|
</p:column>
|
|
|
<p:column headerText="#{msgsCore['manageWorkshops.field.reviewDeadline']}" sortBy="#{entity.reviewDeadline}">
|
|
|
<h:outputText value="#{entity.reviewDeadline}">
|
|
|
<f:convertDateTime type="localDate" pattern="#{msgs['jbutler.format.date.java']}" />
|
|
|
</h:outputText>
|
|
|
</p:column>
|
|
|
|
|
|
<p:column exportable="false" style="text-align: center;">
|
|
|
<f:facet name="header">
|
|
|
<i class="pi pi-search"></i>
|
|
|
<p:inputText id="globalFilter" onkeyup="PF('dtEntities').filter()" placeholder="#{msgs['jbutler.crud.text.search']}" style="margin-left: 10px;" />
|
|
|
</f:facet>
|
|
|
<p:commandButton icon="pi pi-pencil" update=":form:manage-entity-content" oncomplete="PF('formDialog').show()" process="@this">
|
|
|
<f:setPropertyActionListener value="#{entity}" target="#{manageWorkshopsController.selectedEntity}" />
|
|
|
<p:resetInput target=":form:manage-entity-content" />
|
|
|
</p:commandButton>
|
|
|
<p:commandButton icon="pi pi-trash" process="@this" oncomplete="PF('deleteEntityDialog').show()">
|
|
|
<f:setPropertyActionListener value="#{entity}" target="#{manageWorkshopsController.selectedEntity}" />
|
|
|
</p:commandButton>
|
|
|
</p:column>
|
|
|
</p:dataTable>
|
|
|
</p:panel>
|
|
|
|
|
|
<!-- Dialog to create new entities or update existing ones. -->
|
|
|
<p:dialog header="#{msgsCore['manageWorkshops.title.detail']}" showEffect="fade" widgetVar="formDialog" responsive="true" width="450" modal="true">
|
|
|
<p:outputPanel id="manage-entity-content">
|
|
|
<p:outputPanel rendered="#{not empty manageWorkshopsController.selectedEntity}">
|
|
|
<p:panelGrid columns="2" cellpadding="5" layout="grid" styleClass="ui-fluid card" columnClasses="ui-grid-col-4,ui-grid-col-8">
|
|
|
<p:outputLabel for="acronymField" value="#{msgsCore['manageWorkshops.field.acronym']}" />
|
|
|
<h:panelGroup id="acronymGroup">
|
|
|
<p:message for="acronymField" />
|
|
|
<p:inputText id="acronymField" value="#{manageWorkshopsController.selectedEntity.acronym}" required="true">
|
|
|
<p:ajax event="blur" process="@this" update="acronymGroup" />
|
|
|
</p:inputText>
|
|
|
</h:panelGroup>
|
|
|
|
|
|
<p:outputLabel for="nameField" value="#{msgsCore['manageWorkshops.field.name']}" />
|
|
|
<h:panelGroup id="nameGroup">
|
|
|
<p:message for="nameField" />
|
|
|
<p:inputText id="nameField" value="#{manageWorkshopsController.selectedEntity.name}" required="true">
|
|
|
<p:ajax event="blur" process="@this" update="nameGroup" />
|
|
|
</p:inputText>
|
|
|
</h:panelGroup>
|
|
|
|
|
|
<p:outputLabel for="yearField" value="#{msgsCore['manageWorkshops.field.year']}" />
|
|
|
<h:panelGroup id="yearGroup">
|
|
|
<p:message for="yearField" />
|
|
|
<p:inputNumber id="yearField" value="#{manageWorkshopsController.selectedEntity.year}" required="true" decimalPlaces="0">
|
|
|
<p:ajax event="blur" process="@this" update="yearGroup" />
|
|
|
</p:inputNumber>
|
|
|
</h:panelGroup>
|
|
|
|
|
|
<p:outputLabel for="submissionDeadlineField" value="#{msgsCore['manageWorkshops.field.submissionDeadline']}" />
|
|
|
<p:datePicker id="submissionDeadlineField" value="#{manageWorkshopsController.selectedEntity.submissionDeadline}" pattern="#{msgs['jbutler.format.date.java']}" showButtonBar="true" mask="true" />
|
|
|
|
|
|
<p:outputLabel for="reviewDeadlineField" value="#{msgsCore['manageWorkshops.field.reviewDeadline']}" />
|
|
|
<p:datePicker id="reviewDeadlineField" value="#{manageWorkshopsController.selectedEntity.reviewDeadline}" pattern="#{msgs['jbutler.format.date.java']}" showButtonBar="true" mask="true" />
|
|
|
</p:panelGrid>
|
|
|
</p:outputPanel>
|
|
|
</p:outputPanel>
|
|
|
|
|
|
<f:facet name="footer">
|
|
|
<p:commandButton value="#{msgs['jbutler.crud.button.save']}" icon="pi pi-check" actionListener="#{manageWorkshopsController.save}" update="manage-entity-content" process="manage-entity-content @this" oncomplete="PF('formDialog').hide()" />
|
|
|
<p:commandButton value="#{msgs['jbutler.crud.button.cancel']}" icon="pi pi-times" onclick="PF('formDialog').hide()" />
|
|
|
</f:facet>
|
|
|
</p:dialog>
|
|
|
|
|
|
<!-- Dialog to confirm deletion. -->
|
|
|
<p:confirmDialog widgetVar="deleteEntityDialog" showEffect="fade" width="300" message="#{msgsCore['manageWorkshops.text.deleteConfirmation']}" header="#{msgs['jbutler.crud.title.confirmation']}" severity="warn">
|
|
|
<p:commandButton value="#{msgs['jbutler.crud.button.yes']}" icon="pi pi-check" actionListener="#{manageWorkshopsController.delete}" process="@this" oncomplete="PF('deleteEntityDialog').hide()" />
|
|
|
<p:commandButton value="#{msgs['jbutler.crud.button.no']}" type="button" icon="pi pi-times" onclick="PF('deleteEntityDialog').hide()" />
|
|
|
</p:confirmDialog>
|
|
|
|
|
|
<!-- Global dialog used by p:confirm tags. -->
|
|
|
<p:confirmDialog global="true" showEffect="fade" width="300">
|
|
|
<p:commandButton value="#{msgs['jbutler.crud.button.no']}" type="button" icon="pi pi-times" styleClass="ui-confirmdialog-no" />
|
|
|
<p:commandButton value="#{msgs['jbutler.crud.button.yes']}" type="button" icon="pi pi-check" styleClass="ui-confirmdialog-yes" />
|
|
|
</p:confirmDialog>
|
|
|
</h:form>
|
|
|
</ui:define>
|
|
|
</ui:composition>
|
|
|
```
|
|
|
|
|
|
## TODO -- Explain the code below, test CRUD, fix bugs
|
|
|
|
|
|
`src/main/resources/br/ufes/inf/labes/oldenburg/core/view/messages.properties`
|
|
|
|
|
|
```sh
|
|
|
##
|
|
|
## Resource bundle for package: core
|
|
|
## Language: American English
|
|
|
##
|
|
|
|
|
|
# Menu labels for all functionalities of the package:
|
|
|
menu.core.manageWorkshops = Manage Workshops
|
|
|
|
|
|
# Text for use case "Manage Workshops":
|
|
|
manageWorkshops.field.acronym = Acronym
|
|
|
manageWorkshops.field.name = Name
|
|
|
manageWorkshops.field.reviewDeadline = Review deadline
|
|
|
manageWorkshops.field.submissionDeadline = Submission deadline
|
|
|
manageWorkshops.field.year = Year
|
|
|
manageWorkshops.title = Manage Workshops
|
|
|
manageWorkshops.title.description = Create, retrieve, update and delete data about workshops.
|
|
|
manageWorkshops.title.detail = Workshop Details
|
|
|
manageWorkshops.text.createSucceeded = Workshop created:
|
|
|
manageWorkshops.text.deleteConfirmation = Delete the selected workshops?
|
|
|
manageWorkshops.text.deleteSucceeded = Successfully deleted {0,choice,0#no workshops|1#one workshop|1<{0,number,integer} workshops}
|
|
|
manageWorkshops.text.emptyMessage = There are no Workshops in the system. Click New to create the first one!
|
|
|
manageWorkshops.text.entities = Workshops
|
|
|
manageWorkshops.text.updateSucceeded = Workshop updated:
|
|
|
```
|
|
|
|
|
|
Add to `faces-config.xml`:
|
|
|
|
|
|
```xml
|
|
|
<resource-bundle>
|
|
|
<base-name>br.ufes.inf.labes.oldenburg.core.view.messages</base-name>
|
|
|
<var>msgsCore</var>
|
|
|
</resource-bundle>
|
|
|
```
|
|
|
|
|
|
Add to `template.xhtml`:
|
|
|
|
|
|
```xml
|
|
|
<li><p:link outcome="/core/manageWorkshops/index">
|
|
|
<i class="fa fa-database"></i>
|
|
|
<span><h:outputText value="#{msgsCore['menu.core.manageWorkshops']}" /></span>
|
|
|
</p:link></li>
|
|
|
```
|
|
|
|