Skip to content

Tutorial04

Vítor E. Silva Souza edited this page Apr 18, 2022 · 6 revisions

JButler has moved!

The new version of JButler is now hosted at Labes/UFES and there is a new tutorial as well. This repository contains an older version, if you're interested.


JButler Tutorial, Part 04: implement a simple CRUD feature

In this step we develop a simple CRUD (Create, Retrieve, Update and Delete) functionality in the Oldenburg project using JButler's CRUD mini-framework. The feature to be developed is the Manage Workshops use case, which allows the administrator to create, retrieve, update and delete workshops from the system.

Before we implement the CRUD, it's important to understand the architecture on which JButler is based.

The WIS architecture

JButler's mini CRUD framework is built for use in WISs that follow a three-tier architecture on top of Java EE, as shown in the figure below.

Proposed architecture for Web-based Information Systems that use Java EE.

At the Presentation Tier, the View is composed of Web pages and related resources (scripts, stylesheets, images, etc.). Facelets applies a template to our pages, whereas Primefaces, a JSF component library that offers a large set of components, is used to assemble web forms and such. In the Oldenburg project, the View is mostly contained in Deployed Resources/webapp, but we also have view packages under Java Resources to place specific resource bundles (for i18n).

The Control package contains JSF Managed Beans, which are classes responsible for establishing the communication between "the Java world" and "the Web world": JSF Web pages at the View refer to attributes and methods of JSF Beans at the Control using an Expression Language (EL), allowing users to send and obtain data from the WIS using a Web-based user interface. JSF Managed Beans will be placed in a controller package under Java Resources.

At the Business Tier, the Application and Domain packages implement the business rules independently of the presentation and persistence technologies. In Domain we have the entity (persistent) classes that represent elements of the domain of the problem (in this case, workshops, authors, reviewers, submissions, etc.), whereas in Application there are Session EJBs which implement the use cases of the system (manage workshops, submit a paper, submit a review, etc.). This tier is divided in application and domain packages under Java Resources.

Finally, the Data Access Tier is composed of a single package, Persistence, which is where the Data Access Objects are. The DAOs are responsible for storing, retrieving and deleting data from the domain entities in the data storage (in our case, the MySQL database) and uses JPA to do it through Object/Relational Mapping. DAOs are placed in the persistence package under Java Resources.

The dependencies between the three tiers -- Control depends on Application to execute the system's use cases upon user request and, in its turn, Application depends on Persistence to store data in the database -- are satisfied by CDI, which, along with JSF, EJBs and JPA, is part of Java EE.

Next, we implement a CRUD feature using JButler's mini CRUD framework on top of the above architecture.

Add JButler as dependency

To implement the CRUD, we need JButler itself. To declare it as a dependency in our project, perform the following changes in pom.xml:

  1. In the <project></project> tag, add NEMO's Maven2 repository:

    <repositories>
    	<repository>
    		<releases>
    			<enabled>true</enabled>
    			<updatePolicy>always</updatePolicy>
    			<checksumPolicy>fail</checksumPolicy>
    		</releases>
    		<id>br.ufes.inf.nemo</id>
    		<name>Nemo Maven Repository</name>
    		<url>http://dev.nemo.inf.ufes.br/maven2</url>
    		<layout>default</layout>
    	</repository>
    </repositories>
  2. In the <dependencies></dependencies> tag, add JButler for Web Profile:

    <dependency>
        <groupId>br.ufes.inf.nemo</groupId>
        <artifactId>jbutler-wp</artifactId>
        <version>1.2.7</version>
    </dependency>
  3. Save pom.xml and check that the dependencies have been included in Java Resources/Libraries/Maven Dependencies.

JPA Configuration

To communicate with the database, we also need to set up JPA:

  1. Right-click the oldenburg project and select Properties;

  2. In the Project Facets page, check JPA;

  3. Click Further configuration available, in JPA implementation select Disable Library Configuration and in Persistent class management (at the bottom) make sure Discover annotated classes automatically is selected;

  4. Click Apply and Close, then open JPA Content/persistence.xml. Open the Source tab and include the following inside the <persistence-unit></persistence-unit> tag:

    <provider>org.hibernate.ejb.HibernatePersistence</provider>
    <jta-data-source>java:jboss/datasources/Oldenburg</jta-data-source>
    <class>br.ufes.inf.nemo.jbutler.ejb.persistence.PersistentObjectSupport</class>
    <properties>
        <property name="hibernate.hbm2ddl.auto" value="update" />
    </properties>

The above configuration tells our application to use Hibernate (which is provided by WildFly) as the JPA implementation, to connect to the Oldenburg datasource we created in the preamble of this tutorial and to ask Hibernate to automatically generate the database schema for us.

Moreover, the PersistentObjectSupport class, from JButler is explicitly included in the configuration due to a bug that makes Eclipse think that the entity classes from our project do not have an @Id attribute (a primary key), when they actually get it from this class. Explicitly including it in the persistence.xml file like this circumvents this problem. Again, I didn't check if this bug has been fixed in the latest versions of the tools used in this tutorial, but it doesn't hurt to have this configuration.

JButler's CRUD mini-framework guidelines

It's important to explain how JButler is dependant on the previously described architecture. If you want to have less trouble, you should follow these guidelines:

  1. Your packages should follow this naming convention: (organization).(system).(subsystem).(module). For instance, br.ufes.informatica.oldenburg.core.controller refers to the controller's module of the core subsystem of oldenburg, from the organization informatica.ufes.br;

  2. Your controller modules should be called controller (e.g. br.ufes.informatica.oldenburg.core.controller, br.ufes.informatica.oldenburg.review.controller, etc.);

  3. Your controller classes' names should end with Controller (e.g., ManageWorkshopsController, SubmitReviewController, etc.);

  4. Web pages for CRUD features should be called exactly index.xhtml and form.xhtml and be placed in a standard directory structure following this convention: (module)/(controller-base-name), where (controller-base-name) is the name of the controller class, without the Controller suffix and with first letter lower case. For instance, the web pages (index.xhtml and form.xhtml) for br.ufes.informatica.oldenburg.core.controller.ManageWorkshopsController should be in Deployed Resources/webapp/core/manageWorkshops/, whereas those of br.ufes.informatica.oldenburg.review.controller.SubmitReviewController should be in Deployed Resources/webapp/review/submitReview;

  5. Your i18n resource bundle should be registered in Deployed Resources/webapp/WEB-INF/faces-config.xml under a name following this convention: msgs(Subsystem), where (Subsystem) is the name of the subsystem with the first letter capitalized. For instance, msgsCore is the bundle name for the core subsystem and msgsReview is the bundle name for the review subsystem;

  6. Keys in your i18n resource bundle should be prefixed with (controller-base-name) (as above with the folder for the Web pages). For instance, i18n messages for ManageWorkshopsController should be prefixed with manageWorkshops., whereas those of SubmitReviewController should be prefixed with submitReview..

If you don't want to observe so many rules, you will have to override some methods in your controller classes, as they expect things to follow the above rules. This is what you need to override (you can do it for each controller or create a new superclass with your own conventions):

  • JSFController::getBundleName(): determines the name of the i18n resource bundle, based on rules #1, #2 and #5;
  • JSFController::getBundlePrefix(): determines the prefix for the keys in the i18n resource bundle, based on rules #3 and #6;
  • ListingController::getViewPath(): determines the folder in which the list.xhtml and form.xhtml Web pages can be found, based on rules #1, #2 and #4;
  • ListingController::getListingPageName(): determines the name of the listing (index) Web page, as per rule #4.
  • CrudController::getFormPageName(): determines the name of the form Web page, as per rule #4.

The CRUD implementation

Finally, to the simple CRUD feature! Here are the steps (again, explanations follow at the end):

  1. Right-click Java Resources/src/main/java and select New > Package in order to create the following packages:

    • br.ufes.informatica.oldenburg.core.application;
    • br.ufes.informatica.oldenburg.core.controller;
    • br.ufes.informatica.oldenburg.core.domain;
    • br.ufes.informatica.oldenburg.core.persistence;
    • br.ufes.informatica.oldenburg.core.view.
  2. Right-click the br.ufes.informatica.oldenburg.core.domain package and select New > Class to create the Workshop class, as follows:

    package br.ufes.informatica.oldenburg.core.domain;
    
    import java.util.Date;
    
    import javax.persistence.Entity;
    import javax.persistence.Temporal;
    import javax.persistence.TemporalType;
    import javax.validation.constraints.NotNull;
    import javax.validation.constraints.Size;
    
    import br.ufes.inf.nemo.jbutler.ejb.persistence.PersistentObjectSupport;
    
    @Entity
    public class Workshop extends PersistentObjectSupport implements Comparable<Workshop> {
    	private static final long serialVersionUID = 1L;
    	
    	@Size(max = 100)
    	private String name;
    	
    	@Size(max = 10)
    	private String acronym;
    	
    	@NotNull
    	private int year;
    	
    	@NotNull @Temporal(TemporalType.DATE)
    	private Date submissionDeadline;
    	
    	@NotNull @Temporal(TemporalType.DATE)
    	private Date 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 Date getSubmissionDeadline() {
    		return submissionDeadline;
    	}
    
    	public void setSubmissionDeadline(Date submissionDeadline) {
    		this.submissionDeadline = submissionDeadline;
    	}
    
    	public Date getReviewDeadline() {
    		return reviewDeadline;
    	}
    
    	public void setReviewDeadline(Date reviewDeadline) {
    		this.reviewDeadline = reviewDeadline;
    	}
    
    	@Override
    	public int compareTo(Workshop o) {
    		return year - o.year;
    	}
    }
  3. Right-click the br.ufes.informatica.oldenburg.core.persistence package and select New > Interface to create the WorkshopDAO interface, as follows:

    package br.ufes.informatica.oldenburg.core.persistence;
    
    import javax.ejb.Local;
    
    import br.ufes.inf.nemo.jbutler.ejb.persistence.BaseDAO;
    import br.ufes.informatica.oldenburg.core.domain.Workshop;
    
    @Local
    public interface WorkshopDAO extends BaseDAO<Workshop> {
    
    }
  4. Right-click the br.ufes.informatica.oldenburg.core.persistence package and select New > Class to create the WorkshopJPADAO class, as follows:

    package br.ufes.informatica.oldenburg.core.persistence;
    
    import javax.ejb.Stateless;
    import javax.persistence.EntityManager;
    import javax.persistence.PersistenceContext;
    
    import br.ufes.inf.nemo.jbutler.ejb.persistence.BaseJPADAO;
    import br.ufes.informatica.oldenburg.core.domain.Workshop;
    
    @Stateless
    public class WorkshopJPADAO extends BaseJPADAO<Workshop> implements WorkshopDAO {
    	private static final long serialVersionUID = 1L;
    	
    	@PersistenceContext
    	private EntityManager entityManager;
    	
    	@Override
    	protected EntityManager getEntityManager() {
    		return entityManager;
    	}
    }
  5. Right-click the br.ufes.informatica.oldenburg.core.application package and select New > Interface to create the ManageWorkshopsService interface, as follows:

    package br.ufes.informatica.oldenburg.core.application;
    
    import javax.ejb.Local;
    
    import br.ufes.inf.nemo.jbutler.ejb.application.CrudService;
    import br.ufes.informatica.oldenburg.core.domain.Workshop;
    
    @Local
    public interface ManageWorkshopsService extends CrudService<Workshop> {
    
    }
  6. Right-click the br.ufes.informatica.oldenburg.core.application package and select New > Class to create the ManageWorkshopsServiceBean class, as follows:

    package br.ufes.informatica.oldenburg.core.application;
    
    import javax.annotation.security.PermitAll;
    import javax.ejb.EJB;
    import javax.ejb.Stateless;
    
    import br.ufes.inf.nemo.jbutler.ejb.application.CrudServiceBean;
    import br.ufes.inf.nemo.jbutler.ejb.persistence.BaseDAO;
    import br.ufes.informatica.oldenburg.core.domain.Workshop;
    import br.ufes.informatica.oldenburg.core.persistence.WorkshopDAO;
    
    @Stateless @PermitAll
    public class ManageWorkshopsServiceBean extends CrudServiceBean<Workshop> implements ManageWorkshopsService {
    		private static final long serialVersionUID = 1L;
    		
    		@EJB
    		private WorkshopDAO workshopDAO;
    		
    		@Override
    		public BaseDAO<Workshop> getDAO() {
    				return workshopDAO;
    		}
    }
  7. Right-click the br.ufes.informatica.oldenburg.core.controller package and select New > Class to create the ManageWorkshopsController class, as follows:

    package br.ufes.informatica.oldenburg.core.controller;
    
    import javax.ejb.EJB;
    import javax.enterprise.context.SessionScoped;
    import javax.inject.Named;
    
    import br.ufes.inf.nemo.jbutler.ejb.application.CrudService;
    import br.ufes.inf.nemo.jbutler.ejb.controller.CrudController;
    import br.ufes.informatica.oldenburg.core.application.ManageWorkshopsService;
    import br.ufes.informatica.oldenburg.core.domain.Workshop;
    
    @Named @SessionScoped
    public class ManageWorkshopsController extends CrudController<Workshop> {
    	private static final long serialVersionUID = 1L;
    
    	@EJB
    	private ManageWorkshopsService manageWorkshopsService;
    	
    	@Override
    	protected CrudService<Workshop> getCrudService() {
    		return manageWorkshopsService;
    	}
    
    	@Override
    	protected void initFilters() { }
    }
  8. Right-click Deployed Resources/webapp and use New > Folder in order to create the folder structure core/manageWorkshops;

  9. Copy Deployed Resources/webapp/index.xhtml into core/manageWorkshops and change its contents to:

    <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="listingForm">
    		<p:dataTable id="entitiesDataTable" var="entity" value="#{manageWorkshopsController.lazyEntities}"
    			selection="#{manageWorkshopsController.selectedEntity}" selectionMode="single" paginator="true"
    			rows="#{manageWorkshopsController.maxDataTableRowsPerPage}"
    			paginatorTemplate="{RowsPerPageDropdown} {FirstPageLink} {PreviousPageLink} {CurrentPageReport} {NextPageLink} {LastPageLink}"
    			rowsPerPageTemplate="#{manageWorkshopsController.halfMaxDataTableRowsPerPage},#{manageWorkshopsController.maxDataTableRowsPerPage},#{manageWorkshopsController.doubleMaxDataTableRowsPerPage}"
    			lazy="true" paginatorPosition="bottom" emptyMessage="#{msgsCore['manageWorkshops.text.noEntities']}">
    			<p:ajax event="rowSelect" update="buttonsGroup" />
    			<p:ajax event="rowUnselect" update="buttonsGroup" />
    			<f:facet name="header">
    				<h:outputText value="#{msgsCore['manageWorkshops.text.entities']}" />
    			</f:facet>
    			<p:column headerText="#{msgsCore['manageWorkshops.field.year']}">
    				<h:outputText value="#{entity.year}" />
    			</p:column>
    			<p:column headerText="#{msgsCore['manageWorkshops.field.name']}">
    				<h:outputText value="#{entity.name}" />
    			</p:column>
    			<p:column headerText="#{msgsCore['manageWorkshops.field.submissionDeadline']}">
    				<h:outputText value="#{entity.submissionDeadline}">
    					<f:convertDateTime type="date" pattern="#{msgs['jbutler.format.date.java']}" />
    				</h:outputText>
    			</p:column>
    			<f:facet name="footer">
    				<h:panelGroup id="buttonsGroup">
    					<p:commandButton action="#{manageWorkshopsController.create}" icon="fa fa-plus" value="#{msgs['jbutler.crud.button.create']}" />
    					<p:commandButton action="#{manageWorkshopsController.retrieve}" icon="fa fa-search" value="#{msgs['jbutler.crud.button.retrieve']}"
    						disabled="#{manageWorkshopsController.selectedEntity == null}" />
    					<p:commandButton action="#{manageWorkshopsController.update}" icon="fa fa-edit" value="#{msgs['jbutler.crud.button.update']}"
    						disabled="#{manageWorkshopsController.selectedEntity == null}" />
    					<p:commandButton action="#{manageWorkshopsController.trash}" icon="fa fa-trash" value="#{msgs['jbutler.crud.button.delete']}"
    						disabled="#{manageWorkshopsController.selectedEntity == null}" update=":listingForm:trashGroup" />
    				</h:panelGroup>
    			</f:facet>
    		</p:dataTable>
    		<h:panelGroup id="trashGroup">
    			<hr />
    			<p:panel id="trashPanel" header="#{msgs['jbutler.crud.text.trashHeader']}" toggleable="true" toggleSpeed="500"
    				rendered="#{not empty manageWorkshopsController.trashCan}">
    				<p:dataTable id="trashDataTable" var="entity" value="#{manageWorkshopsController.trashCan}">
    					<p:column headerText="#{msgsCore['manageWorkshops.field.year']}">
    						<h:outputText value="#{entity.year}" />
    					</p:column>
    					<p:column headerText="#{msgsCore['manageWorkshops.field.name']}">
    						<h:outputText value="#{entity.name}" />
    					</p:column>
    					<p:column headerText="#{msgsCore['manageWorkshops.field.submissionDeadline']}">
    						<h:outputText value="#{entity.submissionDeadline}">
    							<f:convertDateTime type="date" pattern="#{msgs['jbutler.format.date.java']}" />
    						</h:outputText>
    					</p:column>
    					<f:facet name="footer">
    						<p:commandButton action="#{manageWorkshopsController.cancelDeletion}" value="#{msgs['jbutler.crud.button.cancelDeletion']}"
    							icon="fa fa-fw fa-close" process="@this" update=":listingForm" />
    						<p:commandButton action="#{manageWorkshopsController.delete}" value="#{msgs['jbutler.crud.button.confirmDeletion']}"
    							icon="fa fa-fw fa-trash-o" process="@this" update=":listingForm" />
    					</f:facet>
    				</p:dataTable>
    			</p:panel>
    		</h:panelGroup>
    	</h:form>
    </ui:define>
  10. Inside core/manageWorkshops, copy index.xhtml to form.xhtml and change its contents to:

    <ui:define name="title">
    	<h:outputText value="#{msgsCore['manageWorkshops.title.create']}"
    		rendered="#{(! manageWorkshopsController.readOnly) and (manageWorkshopsController.selectedEntity.id == null)}" />
    	<h:outputText value="#{msgsCore['manageWorkshops.title.update']}"
    		rendered="#{(! manageWorkshopsController.readOnly) and (manageWorkshopsController.selectedEntity.id != null)}" />
    	<h:outputText value="#{msgsCore['manageWorkshops.title.retrieve']}" rendered="#{manageWorkshopsController.readOnly}" />
    </ui:define>
    
    <ui:define name="description">
    	<h:outputText value="#{msgsCore['manageWorkshops.title.create.description']}"
    		rendered="#{(! manageWorkshopsController.readOnly) and (manageWorkshopsController.selectedEntity.id == null)}" />
    	<h:outputText value="#{msgsCore['manageWorkshops.title.update.description']}"
    		rendered="#{(! manageWorkshopsController.readOnly) and (manageWorkshopsController.selectedEntity.id != null)}" />
    	<h:outputText value="#{msgsCore['manageWorkshops.title.retrieve.description']}" rendered="#{manageWorkshopsController.readOnly}" />
    </ui:define>
    
    <ui:define name="body">
    	<h:form id="entitiesForm">
    		<p:panelGrid columns="2" columnClasses="ui-grid-col-4,ui-grid-col-8" layout="grid" styleClass="ui-panelgrid-blank">
    			<p:outputLabel for="yearField" value="#{msgsCore['manageWorkshops.field.year']}" />
    			<p:inputText id="yearField" value="#{manageWorkshopsController.selectedEntity.year}" style="width: 100%;" />
    			<p:outputLabel for="nameField" value="#{msgsCore['manageWorkshops.field.name']} " />
    			<p:inputText id="nameField" value="#{manageWorkshopsController.selectedEntity.name}" style="width: 100%;" />
    			<p:outputLabel for="acronymField" value="#{msgsCore['manageWorkshops.field.acronym']}" />
    			<p:inputText id="acronymField" value="#{manageWorkshopsController.selectedEntity.acronym}" style="width: 100%;" />
    			<p:outputLabel for="submissionDeadlineField" value="#{msgsCore['manageWorkshops.field.submissionDeadline']}" />
    			<p:calendar id="submissionDeadlineField" value="#{manageWorkshopsController.selectedEntity.submissionDeadline}" showOn="button"
    				pattern="dd/MM/yyyy" mask="true" style="width: 100%;" />
    			<p:outputLabel for="reviewDeadlineField" value="#{msgsCore['manageWorkshops.field.reviewDeadline']}" />
    			<p:calendar id="reviewDeadlineField" value="#{manageWorkshopsController.selectedEntity.reviewDeadline}" showOn="button" pattern="dd/MM/yyyy"
    				mask="true" style="width: 100%;" />
    		</p:panelGrid>
    		<p:commandButton id="cancelButton" value="#{msgs['jbutler.crud.button.cancel']}" icon="fa fa-close"
    			action="#{manageWorkshopsController.list}" immediate="true" rendered="#{! manageWorkshopsController.readOnly}" />
    		<p:commandButton id="saveButton" value="#{msgs['jbutler.crud.button.save']}" icon="fa fa-save" action="#{manageWorkshopsController.save}"
    			rendered="#{! manageWorkshopsController.readOnly}" />
    		<p:defaultCommand target="saveButton" />
    		<p:commandButton id="backButton" value="#{msgs['jbutler.crud.button.back']}" icon="fa fa-arrow-circle-left"
    			action="#{manageWorkshopsController.list}" immediate="true" rendered="#{manageWorkshopsController.readOnly}" />
    	</h:form>
    </ui:define>
  11. Right-click the br.ufes.informatica.oldenburg.core.view package and select New > Other.... Choose General > File from the list and click Next. Create a file called messages.properties with the following contents:

    ##
    ## Resource bundle for package: core 
    ## Language: American English
    ##
    
    # Menu labels for all functionalities of the package:
    core.menu.admin = Administration
    core.menu.admin.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.text.deleteSucceeded = Successfully deleted {0,choice,0#no workshops|1#one workshop|1<{0,number,integer} workshops}
    manageWorkshops.text.entities = Workshops
    manageWorkshops.text.noEntities = No Workshop registered yet
    manageWorkshops.text.noEntitiesFiltered = No workshop found for this filter
    manageWorkshops.title = Manage Workshops
    manageWorkshops.title.description = Create, retrieve, update and delete data about workshops
    manageWorkshops.title.create = Register new Workshop
    manageWorkshops.title.create.description = Insert new workshop information
    manageWorkshops.title.update = Update Workshop
    manageWorkshops.title.update.description = Modify an existing workshop's information
    manageWorkshops.title.retrieve = Workshop's data
    manageWorkshops.title.retrieve.description = View an existing workshop's information
  12. Open Deployed Resources/webapp/WEB-INF/faces-config.xml, go to the Source tab and add the resource bundle for the core package inside <application></application>:

    <resource-bundle>
    	<base-name>br.ufes.informatica.oldenburg.core.view.messages</base-name>
    	<var>msgsCore</var>
    </resource-bundle>
  13. Finally, add a menu entry for the CRUD feature in the template file Deployed Resources/webapp/WEB-INF/templates/template.xhtml inside <ul class="sidebar-menu"></ul>:

    <li class="header"><h:outputText value="#{msgsCore['core.menu.admin']}" /></li>
    <li><p:link outcome="/core/manageWorkshops/index" onclick="clearBreadCrumbs()">
    		<i class="fa fa-calendar"></i>
    		<span><h:outputText value="#{msgsCore['core.menu.admin.manageWorkshops']}" /></span>
    	</p:link></li>

Finally, redeploy the application, click on the new menu item for the CRUD feature and try creating, retrieving, updating and deleting some workshops.

How does it work?

Work in progress...