Enterprise Java

Java EE 7 with Angular JS – CRUD, REST, Validations – Part 2

This is the promised follow up to the Java EE 7 with Angular JS – Part 1. It took longer than I expect (to find the time to prepare the code and blog post), but it’s finally here!

The Application

The original application in Part 1 it’s only a simple list with pagination and a REST service that feeds the list data.

 
 
 
 
javaee7-angular-list

In this post we’re going to add CRUD (Create, Read, Update, Delete) capabilities, bind REST services to perform these operations on the server side and validate the data.

The Setup

The Setup is the same from Part 1, but here is the list for reference:

The Code

Backend – Java EE 7

The backend does not required many changes. Since we want the ability to create, read, update and delete, we need to add the appropriate methods in the REST service to perform these operations:

PersonResource

package com.cortez.samples.javaee7angular.rest;

import com.cortez.samples.javaee7angular.data.Person;
import com.cortez.samples.javaee7angular.pagination.PaginatedListWrapper;

import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;
import javax.ws.rs.*;
import javax.ws.rs.core.Application;
import javax.ws.rs.core.MediaType;
import java.util.List;

@Stateless
@ApplicationPath("/resources")
@Path("persons")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class PersonResource extends Application {
    @PersistenceContext
    private EntityManager entityManager;

    private Integer countPersons() {
        Query query = entityManager.createQuery("SELECT COUNT(p.id) FROM Person p");
        return ((Long) query.getSingleResult()).intValue();
    }

    @SuppressWarnings("unchecked")
    private List<Person> findPersons(int startPosition, int maxResults, String sortFields, String sortDirections) {
        Query query = entityManager.createQuery("SELECT p FROM Person p ORDER BY " + sortFields + " " + sortDirections);
        query.setFirstResult(startPosition);
        query.setMaxResults(maxResults);
        return query.getResultList();
    }

    private PaginatedListWrapper<Person> findPersons(PaginatedListWrapper<Person> wrapper) {
        wrapper.setTotalResults(countPersons());
        int start = (wrapper.getCurrentPage() - 1) * wrapper.getPageSize();
        wrapper.setList(findPersons(start,
                                    wrapper.getPageSize(),
                                    wrapper.getSortFields(),
                                    wrapper.getSortDirections()));
        return wrapper;
    }

    @GET
    public PaginatedListWrapper<Person> listPersons(@DefaultValue("1")
                                                    @QueryParam("page")
                                                    Integer page,
                                                    @DefaultValue("id")
                                                    @QueryParam("sortFields")
                                                    String sortFields,
                                                    @DefaultValue("asc")
                                                    @QueryParam("sortDirections")
                                                    String sortDirections) {
        PaginatedListWrapper<Person> paginatedListWrapper = new PaginatedListWrapper<>();
        paginatedListWrapper.setCurrentPage(page);
        paginatedListWrapper.setSortFields(sortFields);
        paginatedListWrapper.setSortDirections(sortDirections);
        paginatedListWrapper.setPageSize(10);
        return findPersons(paginatedListWrapper);
    }

    @GET
    @Path("{id}")
    public Person getPerson( @PathParam("id") Long id) {
        return entityManager.find(Person.class, id);
    }

    @POST
    public Person savePerson(Person person) {
        if (person.getId() == null) {
            Person personToSave = new Person();
            personToSave.setName(person.getName());
            personToSave.setDescription(person.getDescription());
            personToSave.setImageUrl(person.getImageUrl());
            entityManager.persist(person);
        } else {
            Person personToUpdate = getPerson(person.getId());
            personToUpdate.setName(person.getName());
            personToUpdate.setDescription(person.getDescription());
            personToUpdate.setImageUrl(person.getImageUrl());
            person = entityManager.merge(personToUpdate);
        }

        return person;
    }

    @DELETE
    @Path("{id}")
    public void deletePerson(@PathParam("id") Long id) {
        entityManager.remove(getPerson(id));
    }
}

The code is exactly as a normal Java POJO, but using the Java EE annotations to enhance the behaviour. @ApplicationPath("/resources") and @Path("persons") will expose the REST service at the url yourdomain/resources/persons (yourdomain will be the host where the application is running). @Consumes(MediaType.APPLICATION_JSON) and @Produces(MediaType.APPLICATION_JSON) accept and format REST request and response as JSON.

For the REST operations:

Annotation / HTTP MethodJava MethodURLBehaviour
@GET / GETlistPersonshttp://yourdomain/resources/personsReturns a paginated list of 10 persons.
@GET / GETgetPersonhttp://yourdomain/resources/persons/{id}Returns a Person entity by it’s id.
@POST / POSTsavePersonhttp://yourdomain/resources/personsCreates or Updates a Person.
@DELETE / DELETEdeletePersonhttp://yourdomain/resources/persons/{id}Deletes a Person entity by it’s id.

The url invoked for each operations is very similar. The magic to distinguish which operation needs to be called is defined in the HTTP method itself when the request is submitted. Check HTTP Method definitions.

For getPerson and deletePerson note that we added the annotation @Path("{id}") which defines an optional path to call the service. Since we need to know which object we want to get or delete, we need to indicate the id somehow. This is done in the service url to be called, so if we want to delete the Person with id 1, we would call http://yourdomain/resources/persons/1 with the HTTP method DELETE.

That’s it for the backend stuff. Only 30 lines of code added to the old REST service. I have also added a new property to the Person object, to hold a link to image with the purpose of displaying an avatar of the person.

UI – Angular JS

For the UI part, I’ve decided to split it into 3 sections: the grid, the form and the feedback messages sections, each with its own Angular controller. The grid is mostly the same from Part 1, but it did require some tweaks for the new stuff:

Grid HTML

<!-- Specify a Angular controller script that binds Javascript variables to the grid.-->
<div class="grid" ng-controller="personsListController">
    <div>
        <h3>List Persons</h3>
    </div>

    <!-- Binds the grid component to be displayed. -->
    <div class="gridStyle" ng-grid="gridOptions"></div>

    <!--  Bind the pagination component to be displayed. -->
    <pagination direction-links="true" boundary-links="true"
                total-items="persons.totalResults" items-per-page="persons.pageSize"
                ng-model="persons.currentPage" ng-change="refreshGrid()">
    </pagination>
</div>

Nothing special here. Pretty much the same as Part 1.

Grid Angular Controller

app.controller('personsListController', function ($scope, $rootScope, personService) {
    // Initialize required information: sorting, the first page to show and the grid options.
    $scope.sortInfo = {fields: ['id'], directions: ['asc']};
    $scope.persons = {currentPage: 1};

    $scope.gridOptions = {
        data: 'persons.list',
        useExternalSorting: true,
        sortInfo: $scope.sortInfo,

        columnDefs: [
            { field: 'id', displayName: 'Id' },
            { field: 'name', displayName: 'Name' },
            { field: 'description', displayName: 'Description' },
            { field: '', width: 30, cellTemplate: '<span class="glyphicon glyphicon-remove remove" ng-click="deleteRow(row)"></span>' }
        ],

        multiSelect: false,
        selectedItems: [],
        // Broadcasts an event when a row is selected, to signal the form that it needs to load the row data.
        afterSelectionChange: function (rowItem) {
            if (rowItem.selected) {
                $rootScope.$broadcast('personSelected', $scope.gridOptions.selectedItems[0].id);
            }
        }
    };

    // Refresh the grid, calling the appropriate rest method.
    $scope.refreshGrid = function () {
        var listPersonsArgs = {
            page: $scope.persons.currentPage,
            sortFields: $scope.sortInfo.fields[0],
            sortDirections: $scope.sortInfo.directions[0]
        };

        personService.get(listPersonsArgs, function (data) {
            $scope.persons = data;
        })
    };

    // Broadcast an event when an element in the grid is deleted. No real deletion is perfomed at this point.
    $scope.deleteRow = function (row) {
        $rootScope.$broadcast('deletePerson', row.entity.id);
    };

    // Watch the sortInfo variable. If changes are detected than we need to refresh the grid.
    // This also works for the first page access, since we assign the initial sorting in the initialize section.
    $scope.$watch('sortInfo.fields[0]', function () {
        $scope.refreshGrid();
    }, true);

    // Do something when the grid is sorted.
    // The grid throws the ngGridEventSorted that gets picked up here and assigns the sortInfo to the scope.
    // This will allow to watch the sortInfo in the scope for changed and refresh the grid.
    $scope.$on('ngGridEventSorted', function (event, sortInfo) {
        $scope.sortInfo = sortInfo;
    });

    // Picks the event broadcasted when a person is saved or deleted to refresh the grid elements with the most
    // updated information.
    $scope.$on('refreshGrid', function () {
        $scope.refreshGrid();
    });

    // Picks the event broadcasted when the form is cleared to also clear the grid selection.
    $scope.$on('clear', function () {
        $scope.gridOptions.selectAll(false);
    });
});

A few more attributes are required to configure the behaviour of the grid. The important bits are the data: 'persons.list' which binds the grid data to Angular model value $scope.persons, the columnDefs which allow us to model the grid as we see fit. Since I wanted to add an option to delete each row, I needed to add a new cell which call the function deleteRow when you click in cross icon. The afterSelectionChanges function is required to update the form data with the person selected in the grid. You can check other grid options here.

The rest of the code is self-explanatory and there is also a few comments in there. A special note about $rootScope.$broadcast: this is used to dispatch an event to all the other controllers. This is a way to communicate between controllers, since the grid, form and feedback messages have separate controllers. If everything was in only one controller, this was not required and a simple function call would be enough. Another possible solution if we want to keep the multiple controllers, would be to use Angular services. The used approach seems much cleaner since it separates the application concerns and does not require you to implement additional Angular services, but it might be a little harder to debug if needed.

Form HTML

<div class="form" ng-controller="personsFormController">
    <!-- Verify person, if there is no id present, that we are Adding a Person -->
    <div ng-if="person.id == null">
        <h3>Add Person</h3>
    </div>
    <!-- Otherwise it's an Edit -->
    <div ng-if="person.id != null">
        <h3>Edit Person</h3>
    </div>

    <div>
        <!-- Specify the function to be called on submit and disable HTML5 validation, since we're using Angular validation-->
        <form name="personForm" ng-submit="updatePerson()" novalidate>

            <!-- Display an error if the input is invalid and is dirty (only when someone changes the value) -->
            <div class="form-group" ng-class="{'has-error' : personForm.name.$invalid && personForm.name.$dirty}">
                <label for="name">Name:</label>
                <!-- Display a check when the field is valid and was modified -->
                <span ng-class="{'glyphicon glyphicon-ok' : personForm.name.$valid && personForm.name.$dirty}"></span>

                <input id="name" name="name" type="text" class="form-control" maxlength="50"
                       ng-model="person.name"
                       required ng-minlength="2" ng-maxlength="50"/>

                <!-- Validation messages to be displayed on required, minlength and maxlength -->
                <p class="help-block" ng-show="personForm.name.$error.required">Add Name.</p>
                <p class="help-block" ng-show="personForm.name.$error.minlength">Name must be at least 2 characters long.</p>
                <p class="help-block" ng-show="personForm.name.$error.maxlength">Name cannot be longer than 50 characters.</p>
            </div>

            <!-- Display an error if the input is invalid and is dirty (only when someone changes the value) -->
            <div class="form-group" ng-class="{'has-error' : personForm.description.$invalid && personForm.description.$dirty}">
                <label for="description">Description:</label>
                <!-- Display a check when the field is valid and was modified -->
                <span ng-class="{'glyphicon glyphicon-ok' : personForm.description.$valid && personForm.description.$dirty}"></span>

                <input id="description" name="description" type="text" class="form-control" maxlength="100"
                       ng-model="person.description"
                       required ng-minlength="5" ng-maxlength="100"/>

                <!-- Validation messages to be displayed on required, minlength and maxlength -->
                <p class="help-block" ng-show="personForm.description.$error.required">Add Description.</p>
                <p class="help-block" ng-show="personForm.description.$error.minlength">Description must be at least 5 characters long.</p>
                <p class="help-block" ng-show="personForm.description.$error.maxlength">Description cannot be longer than 100 characters.</p>
            </div>

            <!-- Display an error if the input is invalid and is dirty (only when someone changes the value) -->
            <div class="form-group" ng-class="{'has-error' : personForm.imageUrl.$invalid && personForm.imageUrl.$dirty}">
                <label for="imageUrl">Image URL:</label>
                <!-- Display a check when the field is valid and was modified -->
                <span ng-class="{'glyphicon glyphicon-ok' : personForm.imageUrl.$valid && personForm.imageUrl.$dirty}"></span>

                <input id="imageUrl" name="imageUrl" type="url" class="form-control" maxlength="500"
                       ng-model="person.imageUrl"
                       required/>

                <!-- Validation messages to be displayed on required and invalid. Type 'url' makes checks to a proper url format. -->
                <p class="help-block" ng-show="personForm.imageUrl.$error.required">Add Image URL.</p>
                <p class="help-block" ng-show="personForm.imageUrl.$invalid && personForm.imageUrl.$dirty">Invalid Image URL.</p>
            </div>

            <div class="avatar" ng-if="person.imageUrl">
                <img ng-src="{{person.imageUrl}}" width="400" height="250"/>
            </div>

            <!-- Form buttons. The 'Save' button is only enabled when the form is valid. -->
            <div class="buttons">
                <button type="button" class="btn btn-primary" ng-click="clearForm()">Clear</button>
                <button type="submit" class="btn btn-primary" ng-disabled="personForm.$invalid">Save</button>
            </div>
        </form>
    </div>
</div>

Here is the looks:

javaee7-angular-form

A lot of codeis for validation purposes, but lets look into this a bit more in detail: each input element binds its value to person.something. This allows to model the data between the HTML and the Javascript controller, so we can write $scope.person.name in our controller to get the value filled in the form input with name, name. To access the data inside the HTML form we use the form name personForm plus the name of the input field.

HTML5 have its own set of validations in the input fields, but we want to use the Angular ones. In that case, we need to disable form validations by using novalidate at the form element. Now, to use Angular validations, we can use a few Angular directives in the input elements. For this very basic form, we only use required, ng-minlength and ng-maxlength, but you can use others. Just look into the documentation.

Angular assigns CSS classes based on the input validation state. To have an idea, these are the possible values:

StateCSSOn
validng-validWhen the field is valid.
invalidng-invalidWhen the field is invalid.
pristineng-pristineWhen the field was never touched before.
dirtyng-dirtyWhen the field is changed.

These CSS classes are empty. You need to create them and assign them styles in an included CSS sheet for the application. Instead, we’re going to use styles from Bootstrap which are very nice. For them to work, a few additional classes need to be applied to the elements. The div element enclosing the input needs the CSS class form-group and the input element needs the CSS class form-control.

To display an invalid input field we add ng-class="{'has-error' : personForm.name.$invalid && personForm.name.$dirty}" to the containing input div. This code evaluates if the name in the personForm is invalid and if it’s dirty. It the condition verifies, then the input is displayed as invalid.

Finally, for the form validation messages we need to verify the $error directive for each of the inputs and types of validations being performed. Just add ng-show="personForm.name.$error.minlength" to an HTML display element with a message to warn the user that the name input field is too short.

Form Angular Controller

// Create a controller with name personsFormController to bind to the form section.
app.controller('personsFormController', function ($scope, $rootScope, personService) {
    // Clears the form. Either by clicking the 'Clear' button in the form, or when a successfull save is performed.
    $scope.clearForm = function () {
        $scope.person = null;
        // For some reason, I was unable to clear field values with type 'url' if the value is invalid.
        // This is a workaroud. Needs proper investigation.
        document.getElementById('imageUrl').value = null;
        // Resets the form validation state.
        $scope.personForm.$setPristine();
        // Broadcast the event to also clear the grid selection.
        $rootScope.$broadcast('clear');
    };

    // Calls the rest method to save a person.
    $scope.updatePerson = function () {
        personService.save($scope.person).$promise.then(
            function () {
                // Broadcast the event to refresh the grid.
                $rootScope.$broadcast('refreshGrid');
                // Broadcast the event to display a save message.
                $rootScope.$broadcast('personSaved');
                $scope.clearForm();
            },
            function () {
                // Broadcast the event for a server error.
                $rootScope.$broadcast('error');
            });
    };

    // Picks up the event broadcasted when the person is selected from the grid and perform the person load by calling
    // the appropiate rest service.
    $scope.$on('personSelected', function (event, id) {
        $scope.person = personService.get({id: id});
    });

    // Picks us the event broadcasted when the person is deleted from the grid and perform the actual person delete by
    // calling the appropiate rest service.
    $scope.$on('deletePerson', function (event, id) {
        personService.delete({id: id}).$promise.then(
            function () {
                // Broadcast the event to refresh the grid.
                $rootScope.$broadcast('refreshGrid');
                // Broadcast the event to display a delete message.
                $rootScope.$broadcast('personDeleted');
                $scope.clearForm();
            },
            function () {
                // Broadcast the event for a server error.
                $rootScope.$broadcast('error');
            });
    });
});

For the form controller, we need the two functions that perform the operations associated with the button Clear and the button Save which are self-explanatory. A quick note: for some reason, Angular does not clear input fields which are in invalid state. I did found a few people complaining about the same problem, but I need to investigate this further. Maybe it’s something I’m doing wrong.

REST services are called using save and delete from the $resource object which already implement the correspondent HTTP methods. Check the documentation. You can get a $resource with the following factory:

REST Service

 // Service that provides persons operations
app.factory('personService', function ($resource) {
    return $resource('resources/persons/:id');
});

The rest of the controller code, are functions to pickup the events created by the grid to load the person data in the form and delete the person. This controller also create a few events. If we add or remove persons, the grid needs to be updated so an event is generated requesting the grid to be updated.

Feedback Messages HTML

<!-- Specify a Angular controller script that binds Javascript variables to the feedback messages.-->
<div class="message" ng-controller="alertMessagesController">
    <alert ng-repeat="alert in alerts" type="{{alert.type}}" close="closeAlert($index)">{{alert.msg}}</alert>
</div>

This is just the top section of the application, to display success or error messages based on save, delete or server error.

Feedback Messages Angular Controller

// Create a controller with name alertMessagesController to bind to the feedback messages section.
app.controller('alertMessagesController', function ($scope) {
    // Picks up the event to display a saved message.
    $scope.$on('personSaved', function () {
        $scope.alerts = [
            { type: 'success', msg: 'Record saved successfully!' }
        ];
    });

    // Picks up the event to display a deleted message.
    $scope.$on('personDeleted', function () {
        $scope.alerts = [
            { type: 'success', msg: 'Record deleted successfully!' }
        ];
    });

    // Picks up the event to display a server error message.
    $scope.$on('error', function () {
        $scope.alerts = [
            { type: 'danger', msg: 'There was a problem in the server!' }
        ];
    });

    $scope.closeAlert = function (index) {
        $scope.alerts.splice(index, 1);
    };
});

This is the controller that push the messages to the view. Listens to the events created by the grid and the form controllers.

The End Result

Uff.. that was a lot of code and new information. Let’s see the final result:

javaee7-angular-full

There is also a live version running in http://javaee7-angular.radcortez.cloudbees.net, thanks to Cloudbees. It may take a while to open if the cloud instances is hibernated (because of no usage).

Resources

You can clone a full working copy from my github repository and deploy it to Wildfly. You can find instructions there to deploy it. Should also work on Glassfish.

Java EE – Angular JS Source

Since I may modify the code in the future, you can download the original source of this post from the release 3.0. In alternative, clone the repo and checkout the tag from release 3.0 with the following command: git checkout 3.0.

Check also:

Final Thoughts

  • The form validation kicks in right after you start typing. Angular 1.3 will have an on blur property to validate only after loosing focus, but I’m still using Angular 1.2.x.
  • I have to confess that I found the validation code a bit too verbose. I don’t know if there is a way to simplify it, but you shouldn’t need to add each message validation to each input.
  • A few things are still lacking here, like parameters sanitisation or server side validation. I’ll cover those in a next blog post.

This was a very long post, actually the longest I’ve wrote on my blog. If you reached this far, thank you so much for your time reading this post. I hope you enjoyed it! Let me know if you have any comments.

Roberto Cortez

My name is Roberto Cortez and I was born in Venezuela, but I have spent most of my life in Coimbra – Portugal, where I currently live. I am a professional Java Developer working in the software development industry, with more than 8 years of experience in business areas like Finance, Insurance and Government. I work with many Java based technologies like JavaEE, Spring, Hibernate, GWT, JBoss AS and Maven just to name a few, always relying on my favorite IDE: IntelliJ IDEA.Most recently, I became a Freelancer / Independent Contractor. My new position is making me travel around the world (an old dream) to customers, but also to attend Java conferences. The direct contact with the Java community made me want to become an active member in the community itself. For that reason, I have created the Coimbra Java User Group, started to contribute to Open Source on Github and launched my own blog (www.radcortez.com), so I can share some of the knowledge that I gained over the years.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

4 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Google_girl
Google_girl
7 years ago

Is there src code available? Should I be able to run this on Jboss?

Roberto Cortez
7 years ago

Hi Google_girl,

Thank you very much for reading.

You can grab the source from here:
https://github.com/radcortez/javaee7-angular

You need a compatible Java EE 7 server to run it. If you are looking into JBoss products, you can run it in Wildfly.

Cheers,
Roberto

Waleed
Waleed
7 years ago

Can I run This Code at webserver such as tomcat or should be Application Server(glassfish)

Roberto Cortez
7 years ago
Reply to  Waleed

Hi Waleed,

You need a Java EE enabled server to run it. You can for instance try TomEE. Glassfish also works.

Cheers,
Roberto

Back to top button