Building a flexible registration module

This is the first hands-on module. We will write one of the most needed functionalities of modern web applications in a module, a registration module featuring a double opt-in with a confirmation e-mail. The following tasks have to be covered by this module:

  • A registration has to be triggered by a user
  • An e-mail is send to an e-mail address including a URL to confirm registration
  • A URL can be opened to confirm registration
  • On confirmation of the registration by the user, the account should be enabled
  • A Job to remove registrations, which are older than a specified time interval

You will also see that there are some problems in keeping your application modularized, which will have to be worked around and require understanding, as to when to put code into a module and when to put it into the application.

The source code of the example is available at examples/chapter5/registration.

Getting ready

Create an application, or you possibly already have an application, where you need the registration functionality. Inside this application should be a class resembling a user, which has an e-mail address property and a property which defines that the user is active. Create a new module via play new-module registration named registration.

As there will be two applications written in this example, the module as well as the application, an additional directory name will be prepended to any file path. In case of the module this will be "registration", where in the case of the real application this will be "register-app". This should sort out any possible confusion.

How to do it...

Starting with the plugin, it will feature a simple controller, which allows confirmation of the registration. This should be put into registration/app/controllers/Registration.java:

public class Registration extends Controller {

   public static void confirm(String uuid) {
      RegistrationPlugin.confirm(uuid);
      Application.index();
   }
}

Furthermore, this module has its own routes definitions, right in registration/conf/routes:

GET   /{uuid}/confirm   registration.Registration.confirm

The next step is to define an interface for the process of registration, which we will implement in the application itself. This file needs to be put in registration/src/play/modules/registration/RegistrationService.java:

public interface RegistrationService {
   public void createRegistration(Object context);
   public void triggerEmail(Object context);
   public boolean isAllowedToExecute(Object context);
   public void confirm(Object context);
}

Now the plugin itself can be implemented. Put it into registration/src/play/modules/registration/RegistrationPlugin.java:

public class RegistrationPlugin extends PlayPlugin {
   
   private static boolean pluginActive = false;
   private static RegistrationService service;
   
   public void onApplicationStart() {
      ApplicationClass registrationService = Play.classes.getAssignableClasses(RegistrationService.class).get(0);
      
      if (registrationService == null) {
         Logger.error("Registration plugin disabled. No class implements RegistrationService interface");
      } else {
         try {
            service = (RegistrationService) registrationService.javaClass.newInstance();
            pluginActive = true;
         } catch (Exception e) {
            Logger.error(e, "Registration plugin disabled. Error when creating new instance");
         }
      }
   }
   
   public void onEvent(String message, Object context) {
      boolean eventMatched = "JPASupport.objectPersisted".equals(message);
      if (pluginActive && eventMatched && service.isAllowedToExecute(context)) {
         service.createRegistration(context);
         service.triggerEmail(context);
      }
   }
    
   public static void confirm(Object uuid) {
      if (pluginActive) {
         service.confirm(uuid);
      }
   }
}

After creating the plugin the obligatory play.plugins file should not be forgotten, which must be put into registration/src/play.plugins:

900:play.modules.registration.RegistrationPlugin

Now the module is finished and you can create it via play build-module in the module directory.

In order to keep the whole application simple and the way it works together with the module, the whole application will be explained in this example.

So including the module in the register-app/conf/dependencies.yml is the first step. Running play deps after that is required:

require:
    - play
    - registration -> registration

repositories:
    - registrationModules:
        type:       local
        artifact:   "/absolute/path/to/registration/module"
        contains:
            - registration -> *

Then it needs to be enabled in the register-app/conf/routes file:

*   /registration      module:registration

The application itself consists of two entities, a user, and the registration entity itself:

@Entity
public class User extends Model {

   public String name;
   @Email
   public String email;
   public Boolean active;
}

The registration entity is also pretty short:

@Entity
public class Registration extends Model {

   public String uuid;
   @OneToOne
   public User user;
   public Date createdAt = new Date();
}

The controllers for the main application consist of one index controller and one controller for creating a new user. After the last one is executed, the logic of the registration plugin should be triggered:

public class Application extends Controller {

    public static void index() {
        render();
    }
    
    public static void addUser(User user) {
       user.active = false;
       if (validation.hasErrors()) {
          error("Validation errors");
       }
       
       user.create();
       index();
    }
}

When a user registers, a mail should be sent. So a mailer needs to be created, in this case at register-app/app/notifier/Mails.java:

public class Mails extends Mailer {

   public static void sendConfirmation(Registration registration) {
      setSubject("Confirm your registration");
      addRecipient(registration.user.email);
      String from = Play.configuration.getProperty("registration.mail.from");
      setFrom(from);
      send(registration);
   }
}

A registration clean up job is also needed, which removes stale registrations once per week. Put it at register-app/app/jobs/RegistrationCleanupJob.java:

@Every("7d")
public class RegistrationCleanupJob extends Job {

   public void doJob() {
      Calendar cal = Calendar.getInstance();
      cal.add(Calendar.MONTH, -1);
      List<Registration> registrations = Registration.find("createdAt < ?", cal.getTime()).fetch();
      for (Registration registration : registrations) {
         registration.delete();
      }
      Logger.info("Deleted %s stale registrations", registrations.size());
   }
}

The last part is the actual implementation of the RegistrationService interface from the plugin. This can be put into register-app/app/service/RegistrationServiceImpl.java:

public class RegistrationServiceImpl implements RegistrationService {

   @Override
   public void createRegistration(Object context) {
      if (context instanceof User) {
         User user = (User) context;
         Registration r = new Registration();
         r.uuid = UUID.randomUUID().toString().replaceAll("-", "");
         r.user = user;
         r.create();
      }
   }

   @Override
   public void triggerEmail(Object context) {
      if (context instanceof User) {
         User user = (User) context;
         Registration registration = Registration.find("byUser", user).first();
         Mails.sendConfirmation(registration);
      }
   }

   @Override
   public boolean isAllowedToExecute(Object context) {
      if (context instanceof User) {
         User user = (User) context;
         return !user.active;
      }
      return false;
   }

   @Override
   public void confirm(Object context) {
      if (context != null) {
         Registration r = Registration.find("byUuid", context.toString()).first();
         if (r == null) {
            return;
         }
         User user = r.user;
         user.active = true;
         user.create();
         r.delete();
         Flash.current().put("registration", "Thanks for registering");
      }
   }
}

There are only two remaining steps, the creation of two templates. The first one is the register-app/app/views/Application/index.html template, which features a registration mechanism and additional messages from the flash scope:

#{extends 'main.html' /}
#{set title:'Home' /}

${flash.registration}

#{form @Application.addUser()}
Name: <input type="text" name="user.name" /><br />
Email: <input type="text" name="user.email" /><br />
<input type="submit" value="Add" />
#{/form}

The last template is the one for the registration e-mail, which is very simple. And put the following under register-app/app/views/notifier/Mails/sendConfirmation.txt:

Hey there...

a very warm welcome.
We need you to complete your registration at

@@{registration.Registration.confirm(registration.uuid)}

Also you should configure your register-app/conf/application.conf file with a valid from e-mail address, which should be set in the sent out e-mails. Therefore, put the parameter [email protected] in your configuration.

Many things happened here in two applications. In order to make sure, you got it right and where to put what file, here is a list of each. This might help you not to be confused. First goes the module, which is in the registration directory:

registration/app/controllers/registration/Registration.java
registration/build.xml
registration/conf/routes
registration/lib/play-registration.jar
registration/src/play/modules/registration/RegistrationPlugin.java
registration/src/play/modules/registration/RegistrationService.java
registration/src/play.plugins

Note that the play-registration.jar will only be there, after you built the module. Your register application should consist of the following files:

register-app/app/controllers/Application.java
register-app/app/jobs/RegistrationCleanupJob.java
register-app/app/models/Registration.java
register-app/app/models/User.java
register-app/app/notifier/Mails.java
register-app/app/service/RegistrationServiceImpl.java
register-app/app/views/Application/index.html
register-app/app/views/main.html
register-app/app/views/notifier/Mails/sendConfirmation.txt
register-app/conf/application.conf
register-app/conf/routes

After checking you can start your application, go to the index page and enter a username and an e-mail address. Then the application will log the sent mail, as long as the mock mailer is configured. You can check the template and that the sent e-mail has an absolute URL to the configuration including an URL with /registration/ in it, where the registration module is mounted. When visiting the link of the e-mail, you will be redirected to the start page, but there is a message at the top of the page. When reloading, this message will vanish, as it is only put in the flash scope.

How it works...

The preceding list, where to find which resources, should primarily show one thing. This example plugin is far from ideal. Indeed, it is very far from that. It should make you think about when you really need a plugin, and when you should include most of the stuff inside your actual project. It's time to discuss flaws and advantages of this example.

First, when you take a look at the RegistrationPlugin in the module, you will see a loosely coupled integration. The plugin searches for an implementation of the RegistrationService interface on startup, and will be marked active, if it finds such an implementation. The implementation in turn is completely independent of the module and therefore done in the application. When you take another look at the plugin, there is an invocation of the service, if a certain JPA event occurs, like the creation of a new object in this case. Again the actual implementation of the service should decide, whether it should be invoked or not. This happens via the isAllowedToExecute() method.

In case you are wondering, why there is no concrete implementation and especially no use of the registration entity: this is a limitation of the Play framework. You cannot use models in modules currently. You cannot import them in your plugin, as they are not visible at compile time of your module. Instead you would have to get them via the Play classloader. Using an interface, which is implemented in the application itself, was a design decision to circumvent exactly this problem with models. Still this means a certain decoupling from your concrete application and is not too good either. This is also the reason, why the RegistrationCleanupJob is also in the application instead of being at the module. Otherwise, it would also have to be configurable, like adding the time, how often it should run, and what entities should be cleaned. As opposed in this example, any user who has not registered might have to be cleaned as well. As all mailer classes are enhanced, they also do not fit into the module. The same applies for the e-mail template due to its flexibility, which implies it should not be packaged in any JAR files or external module, because it cannot be changed in the application then.

So, as you can see in this example, there is no clear and implicit structure. Even though the integration and writing of the plugin is nice, as it is completely decoupled from the storing of a user entity, it would make more sense in this example, to write the logic of this plugin directly in your application instead of building an own module with own packaging, as you could use all your models easily.

So when does writing a module make more sense? It makes more sense whenever you provide infrastructure code. This code is like your own persistence layer, specific templating, specific parsing of incoming data, external authentication or general integration of external services like a search engine. There are going to be enough examples inside this chapter to give a grip what belongs in a module and what belongs in your application.

A last important note and weakness of modules are currently the test capabilities. You have to write an application and use the module inside the applications tests. This is currently the only way to ensure the functionality of your module.

There's more...

Hopefully, this example gave you a glimpse of how to write a first module, but more importantly explains there is not always a use case for a module.

Think about when to write a module

If you sacrifice readability and structure of your application in favor of reuse, you should think about whether this is the way to go. Make sure your application is kept as simple as possible. It will be done this way the rest of this chapter.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.142.166.31