Hi Martin,
Code:
That's nice to hear. Do you use the same class with diffrent persistance strategies or do you use this strategy 'just for' organisation of your logic?
Currently i use it with hibernate only. But the idea is to make a switch as smooth as possible. So, basically it's currently only 'just' for organization.
Code:
Also I miss the addUser method in your repository . How is the repository finally be used? Where is the user added and removed? Where are the update methods? Can you please outline the dependencies of this repository? You know, who uses it and for what...
I did not refactor all the repositories to the strategy pattern. The example is not the whole code
just typed it on the fly. Also i still do not have diagrams in digital form (i'm lazy...).
The following repository is working though (used to store/lookup hashes for the user enrollment, the name will change eventually):
Code:
public class RegistrationTicketRepository {
/**
* The persistence strategy used for this repository
*/
private RepositoryStrategy strategy;
/**
* Private construcor. This class should never be instantiatet manually.
*/
private RegistrationTicketRepository() {}
// ~ Bean setters/getters --------------------------------------------------
public void setStrategy(RepositoryStrategy strategy) {
this.strategy = strategy;
}
// ~ Methods ---------------------------------------------------------------
public RegistrationTicket findTicket(String hash) {
// This is OK but not very clean since the hibernate
// criteria api bleeds into the repository that should not be
// A domain criteria specification object wwhich the strategy is
// aware of should be passed to the strategy here
// nevertheless this code is easie to refactor if the persistence
// layer would change.
Criteria crit = strategy.createCriteria(RegistrationTicket.class);
crit.add(Restrictions.eq("hash", hash));
return (RegistrationTicket) strategy.soleMatch(crit);
}
public void storeTicket(RegistrationTicket ticket) {
strategy.save(ticket);
}
public void removeTicket(RegistrationTicket ticket) {
if (!ticket.isValid()) {
strategy.remove(ticket);
} else {
// TODO-HIGH: convert to business exception
throw new RuntimeException("Can't remove a valid ticket. Please invalidat first.");
}
}
}
The ticket classes
Code:
/**
* An abstract Ticket. A <em>Ticket</em> is basically a object that holds a
* world wide unique and random hash (GUID). A Ticket should be subclassed to
* provided case specific behavior and data.
*
* @author Andreas Aderhold
* @version $Revision$
*/
public abstract class Ticket extends Entity implements Serializable {
private String hash = "";
private boolean valid = false;
private Date created = new Date();
Ticket() {}
public String getHash() {
return hash;
}
protected String buildNewHash() {
this.hash = AnotherGuid.getInstance().genNewGuid();
this.valid = true;
return getHash();
}
public void invalidate() {
this.valid = false;
}
public boolean isValid() {
return this.valid;
}
public Date created() {
return this.created;
}
}
public class RegistrationTicket extends Ticket {
public static final RegistrationTicket EMPTY_TICKET = new RegistrationTicket();
private User user;
RegistrationTicket() { super(); }
private RegistrationTicket(User user) {
this.user = user;
buildNewHash();
}
public User getUser() {
return user;
}
public void validateUser() {
if (isValid()) {
if (!user.isEnabled()) {
user.setEnabled(true);
}
invalidate();
}
}
public static RegistrationTicket newTicketForUser(User user) {
if (user == null) {
throw new IllegalArgumentException("The user can't be null.");
}
return new RegistrationTicket(user);
}
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof RegistrationTicket)) return false;
final RegistrationTicket ticket = (RegistrationTicket) o;
if (!getHash().equals(ticket.getHash())) return false;
if (!getId().equals(ticket.getId())) return false;
return true;
}
public int hashCode() {
return this.getHash().hashCode();
}
public String toString() {
return "RegistrationTicket (HASH= "+ getHash() +")";
}
}
The repository is used by a service:
Code:
public interface IRegistrationService {
/**
* Register a new User with the system.
* <p>
* This method should throw an {@link UserExistsException} if the user is
* already in the database and a {@link EmailAddressExistsException} if
* the Email addess of the user is already taken by another user.
* <p>
* This methdod is also responsible for sending a email to the user that
* the enrollment process started.
* <p>
* This can be done by a advisor wich sends a email to the user. It can
* also be done programatically by sending the mail directly within the
* implementation. Maybe the preffered choice.
* <p>
*
* @param user The configured User to sign up.
* @throws BusinessException in case of errors
* @return <code>true</code> if the registration completed;
* <code>false</code> otherwise
*/
abstract boolean registerUser(User user) throws BusinessException;
/**
* Enroll a registered user.
* <p>
* Completes the final enrollment of a user by checking against a hash.
* If the hash can be found in the pending enrollemnts the corresponding
* User is set active and the pending enrollment is deleted.
* <p>
* This methdod is also responsible for sending a email to the user that
* the enrollment process successfully completed.
* <p>
* This can be done by a advisor wich sends a email to the user. It can
* also be done programatically by sending the mail directly within the
* implementation. Maybe the preffered choice.
*
* @param hash
* @throws BusinessException
* @return The enrolled <code>User</code>
*
*/
abstract User enroll(String hash) throws BusinessException;
}
Implementations
Code:
public boolean registerUser(User user) {
if (logger.isDebugEnabled()) {
logger.debug("Starting signup for user " + user.getUsername());
}
if (userRepository.findUserByUsername(user.getUsername()) != null) {
throw new UserExistsException();
}
if (userRepository.findUserByEmail(user.getEmail()) != null) {
throw new EmailAddressExistsException();
}
Role roleEveryone = roleRepository.findRole(ROLE_EVERYONE);
Role roleCustomer = roleRepository.findRole(ROLE_CUSTOMER);
Group groupUsers = groupRepository.findGroup(GROUP_USERS);
user.addToRole(roleEveryone);
user.addToRole(roleCustomer);
user.addToGroup(groupUsers);
userRepository.storeUser(user);
if (logger.isDebugEnabled()) {
logger.debug("Creating validation ticket...");
}
RegistrationTicket ticket = RegistrationTicket.newTicketForUser(user);
ticketRepository.storeTicket(ticket);
String hash = ticket.getHash();
if (logger.isDebugEnabled()) {
logger.debug("Ticket created and saved with hash: " + hash);
}
// TODO: USE SCHEDULER, fire and forget the mail to a scheduler
// mail sending should not happen in transactional context
try {
MimeMessage mm = mailSender.createMimeMessage();
MimeMessageHelper msg = new MimeMessageHelper(mm, "UTF-8");
msg.setFrom("xxx@xxxx.com");
msg.setTo(user.getEmail());
msg.setSubject("Welcome...");
msg.setText("Hallo please click here: " + hash);
mailSender.send(msg.getMimeMessage());
} catch (MessagingException ex) {
logger.error("Error sending mail", ex);
throw new RuntimeException("Could not complete request, sending email failed", ex);
}
if (logger.isDebugEnabled()) {
logger.debug("Successfully sent confimation mail to: " + user.getEmail());
}
if (logger.isInfoEnabled()) {
logger.info("New registration pending for validation (user=" + user.getName().toString() +"/id=" + user.getId() + ")");
}
return true;
}
public User enroll(String hash) {
if (logger.isDebugEnabled()) {
logger.debug("Looking up hash: " + hash);
}
RegistrationTicket ticket = ticketRepository.findTicket(hash);
if (ticket == null || !ticket.isValid()) {
throw new ConfirmRegistrationException("User already confirmed or confirmation sequence does not exist");
}
if (logger.isDebugEnabled()) {
logger.debug("Found valid hash: " + hash);
}
ticket.validateUser();
User user = ticket.getUser();
if (logger.isDebugEnabled()) {
logger.debug("Got associated user: " + user.toString());
}
ticketRepository.removeTicket(ticket);
// TODO: Need programmatic transaction here or put in advisor
// ALSO: USE SCHEDULER, fire and forget the mail to a scheduler
// mail sending should not happen in transactional context
try {
MimeMessage mm = mailSender.createMimeMessage();
MimeMessageHelper msg = new MimeMessageHelper(mm, "UTF-8");
msg.setFrom("xxx@xxx.com");
msg.setTo(user.getEmail());
msg.setSubject("Registration complete");
msg.setText("it worked Hallo please click here to login...");
mailSender.send(msg.getMimeMessage());
} catch (MessagingException ex) {
logger.error("Error sending mail", ex);
throw new ConfirmRegistrationException(ex);
}
if (logger.isDebugEnabled()) {
logger.debug("User confirmed successfully: " + user.toString());
}
return user;
}
Reg.Service is used in a controller .. i.e.:
Code:
public class RegistrationEnrollController extends ParameterizableViewController implements InitializingBean {
// ~ Instance fields =======================================================
private IRegistrationService regService;
// ~ Methods ===============================================================
public void setRegistrationService(IRegistrationService service) {
this.regService = service;
}
public void afterPropertiesSet() throws Exception {
if (regService == null) {
throw new IllegalArgumentException("A registration service is required");
}
}
protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
String hash = RequestUtils.getRequiredStringParameter(request, "enroll");
User user = regService.enroll(hash);
Map model = new HashMap();
if (user.isEnabled()) {
model.put("username", user.getName().first());
}
return new ModelAndView(getViewName(), model);
}
}
The strategy classes
Code:
/**
* This <em>Layer Supertype</em> is an attempt to unify repository strategies.
*
* <p>The motivation behind this is to reduce code duplication in combination
* with persistence strategies used in different repositories. Most
* repositories perform CRUD operations all the time. They expose their
* functions with <em>intetion revealing interfaces</em> that underneath
* calling the appropriate strategies to manage persitence.</p>
*
* <p>However, the intention revealing interfaces are duplicated from the
* Respositories down to the strategies resulting in seperate strategies for
* seperate Repositories. Though the strategies are mostly doing CRUD or other
* common operations the intent is to unify the strategy in one that works
* for all repositories.</p>
*
* <p><strong>This class (and implementations) should be considered highly
* experimental at this stage and must be used soly for experimentation.</strong></p>
*
* @author Andreas Aderhold
* @version $Revision$
*/
public interface RepositoryStrategy {
// cirteria should NOT be hibernate specific
Collection matching(Criteria specs);
Object soleMatch(Criteria specs);
void save(Object object) throws DataAccessException;
void update(Object object) throws DataAccessException;
void refresh(Object object) throws DataAccessException;
void evict(Object object);
void remove(Object role) throws DataAccessException;
String getObjectId(Object role) throws DataAccessException;
void removeById(Class clazz, Serializable id) throws DataAccessException;
Object findById(Class clazz, Serializable id) throws DataAccessException;
Criteria createCriteria(Class clazz);
}
public class HibernateRepositoryStrategy extends HibernateDaoSupport implements RepositoryStrategy {
public void evict(Object object) {
getHibernateTemplate().evict(object);
}
public Object findById(Class clazz, Serializable id) throws DataAccessException {
return getHibernateTemplate().load(clazz, id);
}
public String getObjectId(Object object) throws DataAccessException {
return getSession().getIdentifier(object).toString();
}
// cirteria that come in should NOT be hibernate specific
public Collection matching(Criteria specs) {
return specs.list();
}
public void refresh(Object object) throws DataAccessException {
getHibernateTemplate().refresh(object);
}
public void remove(Object object) throws DataAccessException {
getHibernateTemplate().delete(object);
}
public void removeById(Class clazz, Serializable id) throws DataAccessException {
getHibernateTemplate().delete(findById(clazz, id));
}
public void save(Object object) throws DataAccessException {
getHibernateTemplate().saveOrUpdate(object);
}
public Object soleMatch(Criteria specs) {
List objs = specs.list();
if (objs.iterator().hasNext())
return objs.iterator().next();
return null;
}
public void update(Object object) throws DataAccessException {
getHibernateTemplate().update(object);
}
public Criteria createCriteria(Class clazz) {
return getSession().createCriteria(clazz);
}
}
Hope this gives you the picture. The comments are not always proper, i'm lazy here too...