You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@rave.apache.org by mf...@apache.org on 2012/02/22 17:05:58 UTC

svn commit: r1292365 [1/2] - in /incubator/rave/trunk: ./ rave-components/rave-commons/src/main/java/org/apache/rave/exception/ rave-components/rave-core/ rave-components/rave-core/src/main/java/org/apache/rave/portal/model/ rave-components/rave-core/s...

Author: mfranklin
Date: Wed Feb 22 16:05:57 2012
New Revision: 1292365

URL: http://svn.apache.org/viewvc?rev=1292365&view=rev
Log:
Applied patch submitted for RAVE-147

Added:
    incubator/rave/trunk/rave-components/rave-commons/src/main/java/org/apache/rave/exception/EmailException.java
    incubator/rave/trunk/rave-components/rave-core/src/main/java/org/apache/rave/portal/service/EmailService.java
    incubator/rave/trunk/rave-components/rave-core/src/main/java/org/apache/rave/portal/service/impl/DefaultEmailService.java
    incubator/rave/trunk/rave-components/rave-web/src/main/java/org/apache/rave/portal/web/controller/ChangePasswordController.java
    incubator/rave/trunk/rave-components/rave-web/src/main/java/org/apache/rave/portal/web/controller/ReminderController.java
    incubator/rave/trunk/rave-components/rave-web/src/main/java/org/apache/rave/portal/web/validator/ChangePasswordValidator.java
    incubator/rave/trunk/rave-components/rave-web/src/main/java/org/apache/rave/portal/web/validator/NewPasswordValidator.java
    incubator/rave/trunk/rave-components/rave-web/src/test/java/org/apache/rave/portal/web/controller/ChangePasswordControllerTest.java
    incubator/rave/trunk/rave-components/rave-web/src/test/java/org/apache/rave/portal/web/controller/ReminderControllerTest.java
    incubator/rave/trunk/rave-portal-resources/src/main/webapp/WEB-INF/jsp/views/changepassword.jsp
    incubator/rave/trunk/rave-portal-resources/src/main/webapp/WEB-INF/jsp/views/newpassword.jsp
    incubator/rave/trunk/rave-portal-resources/src/main/webapp/WEB-INF/jsp/views/retrieveusername.jsp
    incubator/rave/trunk/rave-portal-resources/src/main/webapp/WEB-INF/mailtemplates/
    incubator/rave/trunk/rave-portal-resources/src/main/webapp/WEB-INF/mailtemplates/password_reminder.ftl
    incubator/rave/trunk/rave-portal-resources/src/main/webapp/WEB-INF/mailtemplates/username_reminder.ftl
Modified:
    incubator/rave/trunk/pom.xml
    incubator/rave/trunk/rave-components/rave-core/pom.xml
    incubator/rave/trunk/rave-components/rave-core/src/main/java/org/apache/rave/portal/model/NewUser.java
    incubator/rave/trunk/rave-components/rave-core/src/main/java/org/apache/rave/portal/model/User.java
    incubator/rave/trunk/rave-components/rave-core/src/main/java/org/apache/rave/portal/repository/UserRepository.java
    incubator/rave/trunk/rave-components/rave-core/src/main/java/org/apache/rave/portal/repository/impl/JpaUserRepository.java
    incubator/rave/trunk/rave-components/rave-core/src/main/java/org/apache/rave/portal/service/UserService.java
    incubator/rave/trunk/rave-components/rave-core/src/main/java/org/apache/rave/portal/service/impl/DefaultUserService.java
    incubator/rave/trunk/rave-components/rave-core/src/main/resources/org/apache/rave/core-applicationContext.xml
    incubator/rave/trunk/rave-components/rave-core/src/test/resources/portal.properties
    incubator/rave/trunk/rave-components/rave-web/src/main/java/org/apache/rave/portal/web/controller/NewAccountController.java
    incubator/rave/trunk/rave-components/rave-web/src/main/java/org/apache/rave/portal/web/util/ModelKeys.java
    incubator/rave/trunk/rave-components/rave-web/src/main/java/org/apache/rave/portal/web/util/ViewNames.java
    incubator/rave/trunk/rave-components/rave-web/src/main/java/org/apache/rave/portal/web/validator/NewAccountValidator.java
    incubator/rave/trunk/rave-portal-resources/src/main/resources/messages.properties
    incubator/rave/trunk/rave-portal-resources/src/main/resources/messages_nl.properties
    incubator/rave/trunk/rave-portal-resources/src/main/resources/portal.properties
    incubator/rave/trunk/rave-portal-resources/src/main/webapp/WEB-INF/applicationContext-security.xml
    incubator/rave/trunk/rave-portal-resources/src/main/webapp/login.jsp
    incubator/rave/trunk/rave-portal/pom.xml
    incubator/rave/trunk/rave-portal/src/main/dist/NOTICE
    incubator/rave/trunk/rave-portal/src/test/resources/portal.properties

Modified: incubator/rave/trunk/pom.xml
URL: http://svn.apache.org/viewvc/incubator/rave/trunk/pom.xml?rev=1292365&r1=1292364&r2=1292365&view=diff
==============================================================================
--- incubator/rave/trunk/pom.xml (original)
+++ incubator/rave/trunk/pom.xml Wed Feb 22 16:05:57 2012
@@ -71,7 +71,9 @@
         <tiles.version>2.2.2</tiles.version>
         <recaptcha4j.version>0.0.7</recaptcha4j.version>
         <apacheds.version>1.5.5</apacheds.version>
-
+        <javax.mail.version>1.4.4</javax.mail.version>
+        <javax.activation.version>1.1</javax.activation.version>
+        <freemarker.version>2.3.18</freemarker.version>
         <!-- The location of Rave's H2 file DB. No trailing / -->
         <rave.database.location>/tmp/rave_db</rave.database.location>
 
@@ -183,6 +185,11 @@
                 <version>${org.springframework.version}</version>
             </dependency>
             <dependency>
+              <groupId>org.springframework</groupId>
+              <artifactId>spring-context-support</artifactId>
+              <version>${org.springframework.version}</version>
+            </dependency>
+            <dependency>
                 <groupId>org.springframework</groupId>
                 <artifactId>spring-web</artifactId>
                 <version>${org.springframework.version}</version>
@@ -397,6 +404,24 @@
                 <artifactId>recaptcha4j</artifactId>
                 <version>${recaptcha4j.version}</version>
             </dependency>
+            <!-- Mail-->
+          <dependency>
+            <groupId>javax.mail</groupId>
+            <artifactId>mail</artifactId>
+            <version>${javax.mail.version}</version>
+            <scope>provided</scope>
+          </dependency>
+          <dependency>
+            <groupId>javax.activation</groupId>
+            <artifactId>activation</artifactId>
+            <version>${javax.activation.version}</version>
+            <scope>provided</scope>
+          </dependency>
+          <dependency>
+            <groupId>org.freemarker</groupId>
+            <artifactId>freemarker</artifactId>
+            <version>${freemarker.version}</version>
+          </dependency>
 
             <!-- ApacheDS (LDAP) -->
             <dependency>
@@ -412,7 +437,7 @@
             </dependency>
 
 
-            <!-- Test -->
+          <!-- Test -->
             <dependency>
                 <groupId>junit</groupId>
                 <artifactId>junit</artifactId>

Added: incubator/rave/trunk/rave-components/rave-commons/src/main/java/org/apache/rave/exception/EmailException.java
URL: http://svn.apache.org/viewvc/incubator/rave/trunk/rave-components/rave-commons/src/main/java/org/apache/rave/exception/EmailException.java?rev=1292365&view=auto
==============================================================================
--- incubator/rave/trunk/rave-components/rave-commons/src/main/java/org/apache/rave/exception/EmailException.java (added)
+++ incubator/rave/trunk/rave-components/rave-commons/src/main/java/org/apache/rave/exception/EmailException.java Wed Feb 22 16:05:57 2012
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.rave.exception;
+
+/**
+ * @version "$Id$"
+ */
+public class EmailException extends RuntimeException {
+
+    private static final long serialVersionUID = 1L;
+
+    public EmailException() {
+    }
+
+    public EmailException(String message) {
+        super(message);
+    }
+
+    public EmailException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public EmailException(Throwable cause) {
+        super(cause);
+    }
+}

Modified: incubator/rave/trunk/rave-components/rave-core/pom.xml
URL: http://svn.apache.org/viewvc/incubator/rave/trunk/rave-components/rave-core/pom.xml?rev=1292365&r1=1292364&r2=1292365&view=diff
==============================================================================
--- incubator/rave/trunk/rave-components/rave-core/pom.xml (original)
+++ incubator/rave/trunk/rave-components/rave-core/pom.xml Wed Feb 22 16:05:57 2012
@@ -144,6 +144,20 @@
             <artifactId>recaptcha4j</artifactId>
         </dependency>
 
+        <!-- mail -->
+        <dependency>
+            <groupId>javax.mail</groupId>
+            <artifactId>mail</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-context-support</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.freemarker</groupId>
+            <artifactId>freemarker</artifactId>
+        </dependency>
+
 
         <!-- Test -->
         <dependency>

Modified: incubator/rave/trunk/rave-components/rave-core/src/main/java/org/apache/rave/portal/model/NewUser.java
URL: http://svn.apache.org/viewvc/incubator/rave/trunk/rave-components/rave-core/src/main/java/org/apache/rave/portal/model/NewUser.java?rev=1292365&r1=1292364&r2=1292365&view=diff
==============================================================================
--- incubator/rave/trunk/rave-components/rave-core/src/main/java/org/apache/rave/portal/model/NewUser.java (original)
+++ incubator/rave/trunk/rave-components/rave-core/src/main/java/org/apache/rave/portal/model/NewUser.java Wed Feb 22 16:05:57 2012
@@ -38,6 +38,7 @@ public class NewUser {
     private String displayName;
     private String status;
     private String aboutMe;
+    private String forgotPasswordHash;
 
 	public NewUser(){
 	}
@@ -181,5 +182,12 @@ public class NewUser {
 	public void setAboutMe(String aboutMe) {
 		this.aboutMe = aboutMe;
 	}
-	
+
+    public String getForgotPasswordHash() {
+        return forgotPasswordHash;
+    }
+
+    public void setForgotPasswordHash(String forgotPasswordHash) {
+        this.forgotPasswordHash = forgotPasswordHash;
+    }
 }
\ No newline at end of file

Modified: incubator/rave/trunk/rave-components/rave-core/src/main/java/org/apache/rave/portal/model/User.java
URL: http://svn.apache.org/viewvc/incubator/rave/trunk/rave-components/rave-core/src/main/java/org/apache/rave/portal/model/User.java?rev=1292365&r1=1292364&r2=1292365&view=diff
==============================================================================
--- incubator/rave/trunk/rave-components/rave-core/src/main/java/org/apache/rave/portal/model/User.java (original)
+++ incubator/rave/trunk/rave-components/rave-core/src/main/java/org/apache/rave/portal/model/User.java Wed Feb 22 16:05:57 2012
@@ -27,6 +27,7 @@ import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Date;
 
 /**
  * {@inheritDoc}
@@ -38,6 +39,7 @@ import java.util.Collections;
         @NamedQuery(name = User.USER_GET_BY_USERNAME, query = "select u from User u where u.username = :"+User.PARAM_USERNAME),
         @NamedQuery(name = User.USER_GET_BY_USER_EMAIL, query = "select u from User u where u.email = :"+User.PARAM_EMAIL),
         @NamedQuery(name = User.USER_GET_ALL, query = "select u from User u order by u.username asc"),
+        @NamedQuery(name = User.USER_GET_BY_FORGOT_PASSWORD_HASH, query = "select u from User u where u.forgotPasswordHash = :" + User.PARAM_FORGOT_PASSWORD_HASH),
         @NamedQuery(name = User.USER_COUNT_ALL, query = "select count(u) from User u"),
         @NamedQuery(name = User.USER_FIND_BY_USERNAME_OR_EMAIL, query = "select u from User u " +
                 "where lower(u.username) like :"+User.PARAM_SEARCHTERM+" or lower(u.email) like :"+User.PARAM_SEARCHTERM+" order by u.username asc"),
@@ -56,8 +58,10 @@ public class User extends Person impleme
     public static final String USER_COUNT_FIND_BY_USERNAME_OR_EMAIL = "User.countFindByUsernameOrEmail";
     public static final String USER_GET_COMMENTERS = "User.getCommenters";
     public static final String USER_GET_ALL_FOR_ADDED_WIDGET = "User.getAllForAddedWidget";
+    public static final String USER_GET_BY_FORGOT_PASSWORD_HASH = "User.getByForgotPasswordHash";
 
     public static final String PARAM_USERNAME = "username";
+    public static final String PARAM_FORGOT_PASSWORD_HASH = "forgotPasswordHash";
     public static final String PARAM_EMAIL = "email";
     public static final String PARAM_SEARCHTERM = "searchTerm";
     public static final String PARAM_WIDGET_ID = "widgetId";
@@ -81,8 +85,17 @@ public class User extends Person impleme
 
     @Basic
     @Column(name = "openid")
-    private String openId;       
-    
+    private String openId;
+
+    @Basic
+    @Column(name = "forgotPasswordHash", unique = true)
+    private String forgotPasswordHash;
+
+    @Basic
+    @Column(name = "password_hash_time")
+    @Temporal(TemporalType.TIMESTAMP)
+    private Date forgotPasswordTime;
+
     @ManyToOne
     @JoinColumn(name="default_page_layout_id")
     private PageLayout defaultPageLayout;    
@@ -239,7 +252,23 @@ public class User extends Person impleme
     public void setOpenId(String openId) {
         this.openId = openId;
     }
-    
+
+    public String getForgotPasswordHash() {
+        return forgotPasswordHash;
+    }
+
+    public void setForgotPasswordHash(String forgotPasswordHash) {
+        this.forgotPasswordHash = forgotPasswordHash;
+    }
+
+    public Date getForgotPasswordTime() {
+        return forgotPasswordTime;
+    }
+
+    public void setForgotPasswordTime(Date forgotPasswordTime) {
+        this.forgotPasswordTime = forgotPasswordTime;
+    }
+
     public PageLayout getDefaultPageLayout() {
         return defaultPageLayout;
     }

Modified: incubator/rave/trunk/rave-components/rave-core/src/main/java/org/apache/rave/portal/repository/UserRepository.java
URL: http://svn.apache.org/viewvc/incubator/rave/trunk/rave-components/rave-core/src/main/java/org/apache/rave/portal/repository/UserRepository.java?rev=1292365&r1=1292364&r2=1292365&view=diff
==============================================================================
--- incubator/rave/trunk/rave-components/rave-core/src/main/java/org/apache/rave/portal/repository/UserRepository.java (original)
+++ incubator/rave/trunk/rave-components/rave-core/src/main/java/org/apache/rave/portal/repository/UserRepository.java Wed Feb 22 16:05:57 2012
@@ -21,6 +21,7 @@ package org.apache.rave.portal.repositor
 import org.apache.rave.persistence.Repository;
 import org.apache.rave.portal.model.User;
 
+import java.util.Date;
 import java.util.List;
 
 public interface UserRepository extends Repository<User> {
@@ -80,4 +81,14 @@ public interface UserRepository extends 
      * @return List of User objects in alphabetical order sorted by familyname, givenname
      */
     List<User> getAllByAddedWidget(long widgetId);
+
+    /**
+     * Gets a {@link User} by generated forgot email hash
+     *
+     * @param hash unique generated hash
+     * @return {@link User} if one exists for given hash, otherwise {@literal null}
+     */
+    User getByForgotPasswordHash(String hash);
+
 }
+

Modified: incubator/rave/trunk/rave-components/rave-core/src/main/java/org/apache/rave/portal/repository/impl/JpaUserRepository.java
URL: http://svn.apache.org/viewvc/incubator/rave/trunk/rave-components/rave-core/src/main/java/org/apache/rave/portal/repository/impl/JpaUserRepository.java?rev=1292365&r1=1292364&r2=1292365&view=diff
==============================================================================
--- incubator/rave/trunk/rave-components/rave-core/src/main/java/org/apache/rave/portal/repository/impl/JpaUserRepository.java (original)
+++ incubator/rave/trunk/rave-components/rave-core/src/main/java/org/apache/rave/portal/repository/impl/JpaUserRepository.java Wed Feb 22 16:05:57 2012
@@ -29,6 +29,8 @@ import org.springframework.stereotype.Re
 
 import javax.persistence.Query;
 import javax.persistence.TypedQuery;
+
+import java.util.Date;
 import java.util.List;
 
 import static org.apache.rave.persistence.jpa.util.JpaUtil.getPagedResultList;
@@ -91,4 +93,12 @@ public class JpaUserRepository extends A
         query.setParameter(User.PARAM_WIDGET_ID, widgetId);
         return query.getResultList();
     }
+
+    @Override
+    public User getByForgotPasswordHash(String hash) {
+        TypedQuery<User> query = manager.createNamedQuery(User.USER_GET_BY_FORGOT_PASSWORD_HASH, User.class);
+        query.setParameter(User.PARAM_FORGOT_PASSWORD_HASH, hash);
+        return getSingleResult(query.getResultList());
+    }
+
 }

Added: incubator/rave/trunk/rave-components/rave-core/src/main/java/org/apache/rave/portal/service/EmailService.java
URL: http://svn.apache.org/viewvc/incubator/rave/trunk/rave-components/rave-core/src/main/java/org/apache/rave/portal/service/EmailService.java?rev=1292365&view=auto
==============================================================================
--- incubator/rave/trunk/rave-components/rave-core/src/main/java/org/apache/rave/portal/service/EmailService.java (added)
+++ incubator/rave/trunk/rave-components/rave-core/src/main/java/org/apache/rave/portal/service/EmailService.java Wed Feb 22 16:05:57 2012
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.rave.portal.service;
+
+import java.util.Map;
+
+/**
+ * @version "$Id$"
+ */
+public interface EmailService {
+
+    /**
+     * Sends an email to specified recipient
+     *
+     * @param to           recipient e.g. {@code John Doe<jo...@example.com>}
+     * @param subject      email subject
+     * @param templateName name of the Freemarker template
+     * @param templateData data provided to Freemarker template
+     */
+    void sendEmail(String to, String subject, String templateName, Map<String, Object> templateData);
+}

Modified: incubator/rave/trunk/rave-components/rave-core/src/main/java/org/apache/rave/portal/service/UserService.java
URL: http://svn.apache.org/viewvc/incubator/rave/trunk/rave-components/rave-core/src/main/java/org/apache/rave/portal/service/UserService.java?rev=1292365&r1=1292364&r2=1292365&view=diff
==============================================================================
--- incubator/rave/trunk/rave-components/rave-core/src/main/java/org/apache/rave/portal/service/UserService.java (original)
+++ incubator/rave/trunk/rave-components/rave-core/src/main/java/org/apache/rave/portal/service/UserService.java Wed Feb 22 16:05:57 2012
@@ -19,13 +19,14 @@
 
 package org.apache.rave.portal.service;
 
+import java.util.List;
+
+import org.apache.rave.portal.model.NewUser;
 import org.apache.rave.portal.model.Person;
 import org.apache.rave.portal.model.User;
 import org.apache.rave.portal.model.util.SearchResult;
 import org.springframework.security.core.userdetails.UserDetailsService;
 
-import java.util.List;
-
 public interface UserService extends UserDetailsService {
     /**
      * Get the currently authenticated user.
@@ -97,8 +98,8 @@ public interface UserService extends Use
      * Gets a {@link SearchResult} for {@link User}'s that match the search term
      *
      * @param searchTerm free text input to search on users
-     * @param offset   start point within the resultset (for paging)
-     * @param pageSize maximum number of items to be returned (for paging)
+     * @param offset     start point within the resultset (for paging)
+     * @param pageSize   maximum number of items to be returned (for paging)
      * @return SearchResult
      */
     SearchResult<User> getUsersByFreeTextSearch(String searchTerm, int offset, int pageSize);
@@ -117,4 +118,38 @@ public interface UserService extends Use
      * @return List of Person objects in alphabetical order sorted by familyname, givenname
      */
     List<Person> getAllByAddedWidget(long widgetId);
+
+    /**
+     * Sends an email which contains link for changing user password
+     *
+     * @param user the {@link org.apache.rave.portal.model.NewUser} which requested password change
+     */
+    void sendPasswordReminder(NewUser user);
+
+    /**
+     * Sends an email which contains username
+     *
+     * @param user the {@link org.apache.rave.portal.model.NewUser} which requested username reminder
+     */
+    void sendUserNameReminder(NewUser user);
+
+
+    /**
+     * Changes password for given user
+     *
+     * @param user the {@link org.apache.rave.portal.model.NewUser} which requested password change
+     * @throws Exception in case something goes wrong
+     */
+    void updatePassword(NewUser user);
+
+    /**
+     * Check if username/email reminder request is still valid (not expired)
+     *
+     * @param forgotPasswordHash hash provided by user
+     * @return true if forgotPasswordHash is stil within given time range
+     * @throws Exception in case something goes wrong
+     */
+    boolean isValidReminderRequest(String forgotPasswordHash, int nrOfMinutesValid);
+
+
 }
\ No newline at end of file

Added: incubator/rave/trunk/rave-components/rave-core/src/main/java/org/apache/rave/portal/service/impl/DefaultEmailService.java
URL: http://svn.apache.org/viewvc/incubator/rave/trunk/rave-components/rave-core/src/main/java/org/apache/rave/portal/service/impl/DefaultEmailService.java?rev=1292365&view=auto
==============================================================================
--- incubator/rave/trunk/rave-components/rave-core/src/main/java/org/apache/rave/portal/service/impl/DefaultEmailService.java (added)
+++ incubator/rave/trunk/rave-components/rave-core/src/main/java/org/apache/rave/portal/service/impl/DefaultEmailService.java Wed Feb 22 16:05:57 2012
@@ -0,0 +1,94 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.rave.portal.service.impl;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.rave.exception.EmailException;
+import org.apache.rave.portal.service.EmailService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.mail.MailMessage;
+import org.springframework.mail.SimpleMailMessage;
+import org.springframework.mail.javamail.JavaMailSender;
+import org.springframework.stereotype.Service;
+import org.springframework.ui.freemarker.FreeMarkerConfigurationFactoryBean;
+import org.springframework.ui.freemarker.FreeMarkerTemplateUtils;
+
+import freemarker.template.Configuration;
+import freemarker.template.TemplateException;
+
+/**
+ * Default implementation of EmailService
+ *
+ * @version "$Id$"
+ */
+@Service
+public class DefaultEmailService implements EmailService {
+
+    @Autowired
+    private SimpleMailMessage emailServiceMailMessage;
+
+    @Autowired
+    private JavaMailSender mailSender;
+
+    @Autowired
+    private FreeMarkerConfigurationFactoryBean freemarkerMailConfiguration;
+
+
+    private static Logger log = LoggerFactory.getLogger(DefaultEmailService.class);
+
+    @Override
+    public void sendEmail(String to, String subject, String templateName, Map<String, Object> templateData) {
+        // create a copy of mail message:
+        SimpleMailMessage message = new SimpleMailMessage(emailServiceMailMessage);
+        message.setSubject(subject);
+        message.setTo(to);
+        // set body text:
+        Configuration configuration = freemarkerMailConfiguration.getObject();
+        String text = parseTemplate(configuration, templateData, templateName);
+        message.setText(text);
+        // send email:
+        mailSender.send(message);
+    }
+
+    private String parseTemplate(Configuration configuration, Map<String, Object> templateData, String templateName) {
+
+        try {
+            return FreeMarkerTemplateUtils.processTemplateIntoString(configuration.getTemplate(templateName), templateData);
+        } catch (IOException e) {
+            if (log.isDebugEnabled()) {
+                log.error("Error parsing email template" + templateName, e);
+            }
+            throw new EmailException("Username reminder: error parsing email template" + templateName);
+        } catch (TemplateException e) {
+            if (log.isDebugEnabled()) {
+                log.error("failed to render email template " + templateName, e);
+            }
+            throw new EmailException("Username reminder: error rendering email template: " + templateName);
+        }
+    }
+    
+    
+    
+}

Modified: incubator/rave/trunk/rave-components/rave-core/src/main/java/org/apache/rave/portal/service/impl/DefaultUserService.java
URL: http://svn.apache.org/viewvc/incubator/rave/trunk/rave-components/rave-core/src/main/java/org/apache/rave/portal/service/impl/DefaultUserService.java?rev=1292365&r1=1292364&r2=1292365&view=diff
==============================================================================
--- incubator/rave/trunk/rave-components/rave-core/src/main/java/org/apache/rave/portal/service/impl/DefaultUserService.java (original)
+++ incubator/rave/trunk/rave-components/rave-core/src/main/java/org/apache/rave/portal/service/impl/DefaultUserService.java Wed Feb 22 16:05:57 2012
@@ -19,15 +19,22 @@
 
 package org.apache.rave.portal.service.impl;
 
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.rave.portal.model.NewUser;
+
 import org.apache.rave.portal.model.PageType;
+
 import org.apache.rave.portal.model.Person;
 import org.apache.rave.portal.model.User;
 import org.apache.rave.portal.model.util.SearchResult;
 import org.apache.rave.portal.repository.*;
+import org.apache.rave.portal.service.EmailService;
 import org.apache.rave.portal.service.UserService;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.dao.DataAccessException;
 import org.springframework.security.authentication.AbstractAuthenticationToken;
 import org.springframework.security.core.Authentication;
@@ -36,11 +43,14 @@ import org.springframework.security.core
 import org.springframework.security.core.context.SecurityContextImpl;
 import org.springframework.security.core.userdetails.UserDetails;
 import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.security.crypto.codec.Base64;
+import org.springframework.security.crypto.password.PasswordEncoder;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
-import java.util.ArrayList;
-import java.util.List;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.util.*;
 
 /**
  *
@@ -56,6 +66,27 @@ public class DefaultUserService implemen
     private final WidgetRepository widgetRepository;
 
     @Autowired
+    private PasswordEncoder passwordEncoder;
+
+    @Autowired
+    private EmailService emailService;
+
+    @Value("${portal.mail.passwordservice.subject}")
+    private String passwordReminderSubject;
+
+    @Value("${portal.mail.passwordservice.template}")
+    private String passwordReminderTemplate;
+
+    @Value("${portal.mail.username.subject}")
+    private String userNameReminderSubject;
+
+    @Value("${portal.mail.username.template}")
+    private String userNameReminderTemplate;
+
+    @Value("${portal.mail.service.baseurl}")
+    private String baseUrl;
+
+    @Autowired
     public DefaultUserService(UserRepository userRepository,
                               PageRepository pageRepository,
                               WidgetRatingRepository widgetRatingRepository,
@@ -107,6 +138,8 @@ public class DefaultUserService implemen
     private SecurityContext createContext(final User user) {
         SecurityContext securityContext = new SecurityContextImpl();
         securityContext.setAuthentication(new AbstractAuthenticationToken(user.getAuthorities()) {
+            private static final long serialVersionUID = 1L;
+
             @Override
             public Object getCredentials() {
                 return "N/A";
@@ -181,7 +214,7 @@ public class DefaultUserService implemen
             log.warn("unable to find userId " + userId + " to delete");
             return;
         }
-        
+
         final String username = user.getUsername();
 
         // delete all User type pages
@@ -194,7 +227,7 @@ public class DefaultUserService implemen
         int numWidgetsOwned = widgetRepository.unassignWidgetOwner(userId);
         // finally delete the user
         userRepository.delete(user);
-        log.info("Deleted user [" + userId + "," + username + "] - numPages: " + numDeletedPages +
+        log.info("Deleted user [" + userId + ',' + username + "] - numPages: " + numDeletedPages +
                  ", numWidgetComments: " + numWidgetComments + ", numWidgetRatings: " + numWidgetRatings +
                  ", numWidgetsOwned: " + numWidgetsOwned);
     }
@@ -208,4 +241,84 @@ public class DefaultUserService implemen
         }
         return persons;
     }
+
+    @Override
+    public void updatePassword(NewUser newUser) {
+        log.debug("Changing password  for user {}", newUser);
+        User user = userRepository.getByForgotPasswordHash(newUser.getForgotPasswordHash());
+        if (user == null) {
+            throw new IllegalArgumentException("Could not find user for forgotPasswordHash " + newUser.getForgotPasswordHash());
+        }
+        String saltedHashedPassword = passwordEncoder.encode(newUser.getPassword());
+        user.setPassword(saltedHashedPassword);
+        // reset password hash and time
+        user.setForgotPasswordHash(null);
+        user.setForgotPasswordTime(null);
+        userRepository.save(user);
+
+    }
+
+
+    @Override
+    public void sendUserNameReminder(NewUser newUser) {
+        log.debug("Calling send username  {}", newUser);
+        User user = userRepository.getByUserEmail(newUser.getEmail());
+        if (user == null) {
+            throw new IllegalArgumentException("Could not find user for email " + newUser.getEmail());
+        }
+        String to = user.getUsername() + " <" + user.getEmail() + '>';
+        Map<String, Object> templateData = new HashMap<String, Object>();
+        templateData.put("user", user);
+        emailService.sendEmail(to, userNameReminderSubject, userNameReminderTemplate, templateData);
+
+    }
+
+
+    @Override
+    public void sendPasswordReminder(NewUser newUser) {
+        log.debug("Calling send password change link for user {}", newUser);
+        User user = userRepository.getByUserEmail(newUser.getEmail());
+        if (user == null) {
+            throw new IllegalArgumentException("Could not find user for email " + newUser.getEmail());
+        }
+        // create user hash:
+        String input = user.getEmail() + user.getUsername() + String.valueOf(user.getEntityId()) + System.nanoTime();
+        // hash needs to be URL friendly:
+        String safeString = new String(Base64.encode(passwordEncoder.encode(input).getBytes()));
+        String  hashedInput = safeString.replaceAll("[/=]", "A");
+        user.setForgotPasswordHash(hashedInput);
+        user.setForgotPasswordTime(Calendar.getInstance().getTime());
+        userRepository.save(user);
+        String to = user.getUsername() + " <" + user.getEmail() + '>';
+        Map<String, Object> templateData = new HashMap<String, Object>();
+        templateData.put("user", user);
+        templateData.put("reminderUrl", baseUrl + hashedInput);
+        emailService.sendEmail(to, passwordReminderSubject, passwordReminderTemplate, templateData);
+    }
+
+
+    @Override
+    public boolean isValidReminderRequest(String forgotPasswordHash, int nrOfMinutesValid) {
+        if (StringUtils.isBlank(forgotPasswordHash)) {
+            return false;
+        }
+
+        User userForHash = userRepository.getByForgotPasswordHash(forgotPasswordHash);
+        if (userForHash == null) {
+            return false;
+        }
+        Date requestTime = userForHash.getForgotPasswordTime();
+        Calendar expiredDate = Calendar.getInstance();
+        expiredDate.add(Calendar.MINUTE, nrOfMinutesValid);
+
+        if (requestTime == null || requestTime.after(expiredDate.getTime())) {
+            // reset,  this is invalid state
+            userForHash.setForgotPasswordHash(null);
+            userForHash.setForgotPasswordTime(null);
+            userRepository.save(userForHash);
+            return false;
+        }
+        return true;
+    }
+
 }
\ No newline at end of file

Modified: incubator/rave/trunk/rave-components/rave-core/src/main/resources/org/apache/rave/core-applicationContext.xml
URL: http://svn.apache.org/viewvc/incubator/rave/trunk/rave-components/rave-core/src/main/resources/org/apache/rave/core-applicationContext.xml?rev=1292365&r1=1292364&r2=1292365&view=diff
==============================================================================
--- incubator/rave/trunk/rave-components/rave-core/src/main/resources/org/apache/rave/core-applicationContext.xml (original)
+++ incubator/rave/trunk/rave-components/rave-core/src/main/resources/org/apache/rave/core-applicationContext.xml Wed Feb 22 16:05:57 2012
@@ -24,12 +24,11 @@
        xmlns:context="http://www.springframework.org/schema/context"
        xmlns:tx="http://www.springframework.org/schema/tx"
        xmlns:p="http://www.springframework.org/schema/p"
-       xmlns:util="http://www.springframework.org/schema/util"
        xmlns:aop="http://www.springframework.org/schema/aop"
        xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
         http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
         http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
-        http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.0.xsd
+
         http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">
 
     <!-- make the the portal.properties props available to autowire injectors, location of the properties can
@@ -44,7 +43,7 @@
     <context:annotation-config/>
 
     <!-- enable the use of the @AspectJ style of Spring AOP -->
-    <aop:aspectj-autoproxy />
+    <aop:aspectj-autoproxy/>
 
     <!-- rave-common component base-package scan (maybe move to a separate common-applicationContext.xml?) -->
     <context:component-scan base-package="org.apache.rave.service"/>
@@ -55,18 +54,18 @@
     <context:component-scan base-package="org.apache.rave.portal.repository"/>
     <context:component-scan base-package="org.apache.rave.portal.service"/>
     <context:component-scan base-package="org.apache.rave.portal.security"/>
-    
+
     <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
         <property name="entityManagerFactory" ref="entityManagerFactory"/>
     </bean>
 
-    <tx:annotation-driven transaction-manager="transactionManager" />
+    <tx:annotation-driven transaction-manager="transactionManager"/>
 
     <bean id="entityManagerFactory"
           class="org.apache.rave.persistence.jpa.PopulatedLocalContainerEntityManagerFactory">
-        <property name="populator" ref="dataSourcePopulator" />
+        <property name="populator" ref="dataSourcePopulator"/>
         <property name="loadTimeWeaver">
-          <bean class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver"/>
+            <bean class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver"/>
         </property>
         <property name="persistenceUnitName" value="ravePersistenceUnit"/>
         <property name="dataSource" ref="dataSource"/>
@@ -94,10 +93,49 @@
         <property name="username" value="${portal.dataSource.username}"/>
         <property name="password" value="${portal.dataSource.password}"/>
     </bean>
-    
+
     <!-- Password encoding -->
     <bean class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder" id="passwordEncoder">
         <!--<constructor-arg index="0" value="10"/>-->
     </bean>
 
+    <!-- email settings -->
+    <bean id="emailServiceMailMessage" class="org.springframework.mail.SimpleMailMessage">
+        <property name="from" value="${portal.mail.sender}"/>
+        <property name="replyTo" value="${portal.mail.replyto}"/>
+    </bean>
+
+    <bean id="freemarkerMailConfiguration" class="org.springframework.ui.freemarker.FreeMarkerConfigurationFactoryBean">
+        <property name="templateLoaderPath" value="/WEB-INF/mailtemplates"/>
+    </bean>
+    <bean id="mailSender" class="org.springframework.mail.javamail.JavaMailSenderImpl">
+        <property name="host" value="${portal.mail.host}"/>
+        <property name="password" value="${portal.mail.password}"/>
+        <property name="username" value="${portal.mail.username}"/>
+        <property name="port" value="${portal.mail.port}"/>
+        <property name="protocol" value="${portal.mail.protocol}"/>
+        <!-- NOTE: if using Gmail, you'll need following properties-->
+        <!--<property name="javaMailProperties">
+            <props>
+                <prop key="mail.smtp.auth">true</prop>
+                <prop key="mail.smtp.starttls.enable">true</prop>
+                <prop key="mail.smtp.timeout">8500</prop>
+            </props>
+        </property>-->
+    </bean>
+    <!--
+     NOTE: to use mail session you'll need to configure following within catalina_home/conf/context.xml
+        <Resource name="mail/Session" auth="Container" type="javax.mail.Session" mail.smtp.host="my.mail.host"/>
+
+        Further, activation & mail jars needs to be placed within catalina_home/lib folder
+     -->
+  <!--
+  <bean id="mailSender" class="org.springframework.mail.javamail.JavaMailSenderImpl">
+        <property name="session" ref="mailSession"/>
+    </bean>
+    <bean id="mailSession" class="org.springframework.jndi.JndiObjectFactoryBean">
+        <property name="jndiName" value="java:comp/env/mail/Session"/>
+    </bean>
+    -->
+
 </beans>
\ No newline at end of file

Modified: incubator/rave/trunk/rave-components/rave-core/src/test/resources/portal.properties
URL: http://svn.apache.org/viewvc/incubator/rave/trunk/rave-components/rave-core/src/test/resources/portal.properties?rev=1292365&r1=1292364&r2=1292365&view=diff
==============================================================================
--- incubator/rave/trunk/rave-components/rave-core/src/test/resources/portal.properties (original)
+++ incubator/rave/trunk/rave-components/rave-core/src/test/resources/portal.properties Wed Feb 22 16:05:57 2012
@@ -43,4 +43,19 @@ portal.captcha.enabled=false
 portal.captcha.key.private=
 portal.captcha.key.public=
 portal.captcha.usenoscript=false
-portal.captcha.invalid.configuration=<label class="error">ReCaptcha service is not properly configured.</label>
\ No newline at end of file
+portal.captcha.invalid.configuration=<label class="error">ReCaptcha service is not properly configured.</label>
+
+#mail settings
+portal.mail.sender=
+portal.mail.replyto=
+portal.mail.host=
+portal.mail.password=
+portal.mail.username=
+portal.mail.protocol=smtp
+portal.mail.port=25
+portal.mail.username.subject=Rave username reminder service
+portal.mail.username.template=username_reminder.ftl
+portal.mail.passwordservice.subject=Rave password reminder service
+portal.mail.passwordservice.template=password_reminder.ftl
+portal.mail.passwordservice.valid.minutes=30
+portal.mail.service.baseurl=http://localhost:8080/portal/app/changepassword/

Added: incubator/rave/trunk/rave-components/rave-web/src/main/java/org/apache/rave/portal/web/controller/ChangePasswordController.java
URL: http://svn.apache.org/viewvc/incubator/rave/trunk/rave-components/rave-web/src/main/java/org/apache/rave/portal/web/controller/ChangePasswordController.java?rev=1292365&view=auto
==============================================================================
--- incubator/rave/trunk/rave-components/rave-web/src/main/java/org/apache/rave/portal/web/controller/ChangePasswordController.java (added)
+++ incubator/rave/trunk/rave-components/rave-web/src/main/java/org/apache/rave/portal/web/controller/ChangePasswordController.java Wed Feb 22 16:05:57 2012
@@ -0,0 +1,93 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.rave.portal.web.controller;
+
+import org.apache.rave.portal.model.NewUser;
+import org.apache.rave.portal.service.UserService;
+import org.apache.rave.portal.web.util.ModelKeys;
+import org.apache.rave.portal.web.util.ViewNames;
+import org.apache.rave.portal.web.validator.ChangePasswordValidator;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.validation.BindingResult;
+import org.springframework.web.bind.annotation.ModelAttribute;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.servlet.mvc.support.RedirectAttributes;
+
+/**
+ * Controller which processes change password requests.
+ * Only requests that have a valid (matched) forgotPasswordHash will be honored
+ *
+ * @version "$Id$"
+ * @see org.apache.rave.portal.model.User#forgotPasswordHash
+ */
+@Controller
+public class ChangePasswordController {
+
+
+    private static Logger log = LoggerFactory.getLogger(ChangePasswordController.class);
+    private final UserService userService;
+    private final ChangePasswordValidator passwordValidator;
+
+
+    @Autowired
+    public ChangePasswordController(UserService userService, ChangePasswordValidator passwordValidator) {
+        this.userService = userService;
+        this.passwordValidator = passwordValidator;
+    }
+
+
+    @RequestMapping(value = {"/changepassword/{passwordHash:.*}"}, method = RequestMethod.GET)
+    public String initialize(Model model, @PathVariable("passwordHash") String passwordHash, RedirectAttributes redirectAttributes) {
+        log.debug("Requesting user for hash: {}", passwordHash);
+        NewUser user = new NewUser();
+        model.addAttribute(ModelKeys.NEW_USER, user);
+        user.setForgotPasswordHash(passwordHash);
+        return ViewNames.PASSWORD_CHANGE;
+    }
+
+
+    @RequestMapping(value = {"/changepassword", "/changepassword/**"}, method = RequestMethod.POST)
+    public String update(@ModelAttribute NewUser newUser, BindingResult results, Model model) {
+        log.debug("updating user password for hash {}", newUser.getForgotPasswordHash());
+        model.addAttribute(ModelKeys.NEW_USER, newUser);
+        passwordValidator.validate(newUser, results);
+
+        if (results.hasErrors()) {
+            log.info("changepassword, request contains validation errors");
+            return ViewNames.PASSWORD_CHANGE;
+        }
+        try {
+            log.debug("Submitted passwords were valid");
+            userService.updatePassword(newUser);
+            return ViewNames.REDIRECT;
+        } catch (Exception ex) {
+            results.reject("Unable to change password:" + ex.getMessage(), "Unable to change password.");
+            return ViewNames.PASSWORD_CHANGE;
+        }
+
+    }
+}

Modified: incubator/rave/trunk/rave-components/rave-web/src/main/java/org/apache/rave/portal/web/controller/NewAccountController.java
URL: http://svn.apache.org/viewvc/incubator/rave/trunk/rave-components/rave-web/src/main/java/org/apache/rave/portal/web/controller/NewAccountController.java?rev=1292365&r1=1292364&r2=1292365&view=diff
==============================================================================
--- incubator/rave/trunk/rave-components/rave-web/src/main/java/org/apache/rave/portal/web/controller/NewAccountController.java (original)
+++ incubator/rave/trunk/rave-components/rave-web/src/main/java/org/apache/rave/portal/web/controller/NewAccountController.java Wed Feb 22 16:05:57 2012
@@ -57,7 +57,7 @@ public class NewAccountController {
     @RequestMapping(value = "/newaccount.jsp")
     public void setUpForm(ModelMap model, HttpServletRequest request) {
         logger.debug("Initializing form");
-        model.addAttribute("captchaHtml", captchaService.createHtml(request));
+        model.addAttribute(ModelKeys.CAPTCHA_HTML, captchaService.createHtml(request));
         model.addAttribute(ModelKeys.NEW_USER, new NewUser());
     }
 
@@ -108,6 +108,6 @@ public class NewAccountController {
     }
 
     private void initializeCaptcha(Model model, HttpServletRequest request) {
-        model.addAttribute("captchaHtml", captchaService.createHtml(request));
+        model.addAttribute(ModelKeys.CAPTCHA_HTML, captchaService.createHtml(request));
     }
 }
\ No newline at end of file

Added: incubator/rave/trunk/rave-components/rave-web/src/main/java/org/apache/rave/portal/web/controller/ReminderController.java
URL: http://svn.apache.org/viewvc/incubator/rave/trunk/rave-components/rave-web/src/main/java/org/apache/rave/portal/web/controller/ReminderController.java?rev=1292365&view=auto
==============================================================================
--- incubator/rave/trunk/rave-components/rave-web/src/main/java/org/apache/rave/portal/web/controller/ReminderController.java (added)
+++ incubator/rave/trunk/rave-components/rave-web/src/main/java/org/apache/rave/portal/web/controller/ReminderController.java Wed Feb 22 16:05:57 2012
@@ -0,0 +1,150 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.rave.portal.web.controller;
+
+import org.apache.rave.portal.model.NewUser;
+import org.apache.rave.portal.service.CaptchaService;
+import org.apache.rave.portal.service.UserService;
+import org.apache.rave.portal.web.util.ModelKeys;
+import org.apache.rave.portal.web.util.ViewNames;
+import org.apache.rave.portal.web.validator.NewPasswordValidator;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.ui.ModelMap;
+import org.springframework.validation.BindingResult;
+import org.springframework.web.bind.annotation.ModelAttribute;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.servlet.mvc.support.RedirectAttributes;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * Handles password ans username reminder requests
+ *
+ * @version "$Id$"
+ */
+@Controller
+public class ReminderController {
+
+    private static Logger log = LoggerFactory.getLogger(ReminderController.class);
+
+    private final UserService userService;
+    private final NewPasswordValidator passwordValidator;
+    private final CaptchaService captchaService;
+
+
+    @Autowired
+    protected ReminderController(UserService userService, NewPasswordValidator passwordValidator, CaptchaService captchaService) {
+        this.userService = userService;
+        this.passwordValidator = passwordValidator;
+        this.captchaService = captchaService;
+    }
+
+
+    @RequestMapping(value = {"/retrieveusername", "/newpassword"})
+    public void initialize(ModelMap model, HttpServletRequest request) {
+        model.addAttribute(ModelKeys.CAPTCHA_HTML, captchaService.createHtml(request));
+        model.addAttribute(ModelKeys.NEW_USER, new NewUser());
+    }
+
+
+    /**
+     * Processes username requests
+     */
+    @RequestMapping(value = {"/retrieveusername"}, method = RequestMethod.POST)
+    public String requestUsername(@ModelAttribute NewUser newUser, BindingResult results, Model model,
+                                  HttpServletRequest request, RedirectAttributes redirectAttributes) {
+        log.debug("Requesting username reminder");
+        if (!validateEmail(newUser, results, model, request)) {
+            return captchaRequest(model, request, ViewNames.USERNAME_REQUEST);
+        }
+        try {
+            userService.sendUserNameReminder(newUser);
+            populateRedirect(newUser, redirectAttributes);
+            return ViewNames.REDIRECT_RETRIEVE_USERNAME;
+        } catch (Exception e) {
+            if (log.isDebugEnabled()) {
+                log.error("Exception while sending username reminder  {}", e);
+            }
+            results.reject("Unable to send username reminder :" + e.getMessage(), "Unable to send username reminder.");
+            return captchaRequest(model, request, ViewNames.USERNAME_REQUEST);
+        }
+    }
+
+
+    /**
+     * Processes new password requests
+     */
+    @RequestMapping(value = {"/newpassword"}, method = RequestMethod.POST)
+    public String requestPassword(@ModelAttribute NewUser newUser, BindingResult results, Model model,
+                                  HttpServletRequest request, RedirectAttributes redirectAttributes) {
+        log.debug("Requesting password reminder");
+        if (!validateEmail(newUser, results, model, request)) {
+            return captchaRequest(model, request, ViewNames.NEW_PASSWORD_REQUEST);
+        }
+        try {
+            userService.sendPasswordReminder(newUser);
+            populateRedirect(newUser, redirectAttributes);
+            return ViewNames.REDIRECT_NEW_PASSWORD;
+        } catch (Exception e) {
+            if (log.isDebugEnabled()) {
+                log.error("Exception while sending password reminder  {}", e);
+            }
+            results.reject("Unable to send password reminder :" + e.getMessage(), "Unable to send password reminder.");
+            return captchaRequest(model, request, ViewNames.NEW_PASSWORD_REQUEST);
+        }
+
+
+    }
+
+
+    private boolean validateEmail(NewUser newUser, BindingResult results, Model model, HttpServletRequest request) {
+        model.addAttribute(ModelKeys.NEW_USER, newUser);
+        passwordValidator.validate(newUser, results);
+        if (results.hasErrors()) {
+            log.info("newpassword request contains validation errors");
+            return false;
+        }
+
+        String email = newUser.getEmail();
+        log.debug("Submitted email {} is valid", email);
+        if (!captchaService.isValid(request)) {
+            log.debug("Captcha was invalid for user with email {}", email);
+            return false;
+        }
+        return true;
+    }
+
+
+    private void populateRedirect(NewUser newUser, RedirectAttributes redirectAttributes) {
+        redirectAttributes.addFlashAttribute("success", true);
+        redirectAttributes.addFlashAttribute("email", newUser.getEmail());
+    }
+
+    private String captchaRequest(Model model, HttpServletRequest request, String view) {
+        model.addAttribute(ModelKeys.CAPTCHA_HTML, captchaService.createHtml(request));
+        return view;
+    }
+
+}

Modified: incubator/rave/trunk/rave-components/rave-web/src/main/java/org/apache/rave/portal/web/util/ModelKeys.java
URL: http://svn.apache.org/viewvc/incubator/rave/trunk/rave-components/rave-web/src/main/java/org/apache/rave/portal/web/util/ModelKeys.java?rev=1292365&r1=1292364&r2=1292365&view=diff
==============================================================================
--- incubator/rave/trunk/rave-components/rave-web/src/main/java/org/apache/rave/portal/web/util/ModelKeys.java (original)
+++ incubator/rave/trunk/rave-components/rave-web/src/main/java/org/apache/rave/portal/web/util/ModelKeys.java Wed Feb 22 16:05:57 2012
@@ -49,4 +49,5 @@ public class ModelKeys {
     public static final String CATEGORIES = "categories";
     public static final String SELECTED_CATEGORY = "selectedCategory";
     public static final String DEFAULT_TAG_PAGE = "defaultTagPage";
+    public static final String CAPTCHA_HTML = "captchaHtml";
 }
\ No newline at end of file

Modified: incubator/rave/trunk/rave-components/rave-web/src/main/java/org/apache/rave/portal/web/util/ViewNames.java
URL: http://svn.apache.org/viewvc/incubator/rave/trunk/rave-components/rave-web/src/main/java/org/apache/rave/portal/web/util/ViewNames.java?rev=1292365&r1=1292364&r2=1292365&view=diff
==============================================================================
--- incubator/rave/trunk/rave-components/rave-web/src/main/java/org/apache/rave/portal/web/util/ViewNames.java (original)
+++ incubator/rave/trunk/rave-components/rave-web/src/main/java/org/apache/rave/portal/web/util/ViewNames.java Wed Feb 22 16:05:57 2012
@@ -22,7 +22,7 @@ package org.apache.rave.portal.web.util;
 /**
  * Defines constants representing the available view names in the system
  */
-public class ViewNames {
+public final class ViewNames {
     private ViewNames() {}
     private static final String USER_PREFIX = "templates.user.";
     private static final String ADMIN_PREFIX = "templates.admin.";
@@ -47,16 +47,27 @@ public class ViewNames {
     public static final String ADMIN_CATEGORIES = ADMIN_PREFIX + "categories";
     public static final String ADMIN_CATEGORY_DETAIL = ADMIN_PREFIX + "categoryDetail";
 
+    // password reminder / changing
+    public static final String NEW_PASSWORD_REQUEST = USER_PREFIX + "newpassword";
+    public static final String USERNAME_REQUEST = USER_PREFIX + "retrieveusername";
+    public static final String PASSWORD_CHANGE = USER_PREFIX + "changepassword";
+
+
+
     public static final String REDIRECT = "redirect:/";
 
     public static final String POSTS_TAG_PAGE = "postsTagPage";
     public static final String ABOUT_TAG_PAGE = "aboutTagPage";
 
+    public static final String REDIRECT_NEW_PASSWORD = REDIRECT + "app/newpassword";
+    public static final String REDIRECT_RETRIEVE_USERNAME = REDIRECT + "app/retrieveusername";
+
+
     public static String getPageView(String layoutName) {
-        return new StringBuilder(PAGE).append(".").append(layoutName).toString();
+        return new StringBuilder(PAGE).append('.').append(layoutName).toString();
     }
 
     public static String getPersonPageView(String layoutName) {
         return new StringBuilder(PERSON_PROFILE).append(".").append(layoutName).toString();
     }
-}
+}
\ No newline at end of file

Added: incubator/rave/trunk/rave-components/rave-web/src/main/java/org/apache/rave/portal/web/validator/ChangePasswordValidator.java
URL: http://svn.apache.org/viewvc/incubator/rave/trunk/rave-components/rave-web/src/main/java/org/apache/rave/portal/web/validator/ChangePasswordValidator.java?rev=1292365&view=auto
==============================================================================
--- incubator/rave/trunk/rave-components/rave-web/src/main/java/org/apache/rave/portal/web/validator/ChangePasswordValidator.java (added)
+++ incubator/rave/trunk/rave-components/rave-web/src/main/java/org/apache/rave/portal/web/validator/ChangePasswordValidator.java Wed Feb 22 16:05:57 2012
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.rave.portal.web.validator;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.rave.portal.model.NewUser;
+import org.apache.rave.portal.service.UserService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import org.springframework.validation.Errors;
+
+/**
+ * Validates  new password changes
+ * @version "$Id$"
+ */
+@Component
+public class ChangePasswordValidator extends NewAccountValidator {
+
+
+    private static Logger log = LoggerFactory.getLogger(ChangePasswordValidator.class);
+
+    @Value("${portal.mail.passwordservice.valid.minutes}")
+    private int minutesValid;
+
+    @Autowired
+    public ChangePasswordValidator(UserService userService) {
+        super(userService);
+    }
+
+    @Override
+    public void validate(Object target, Errors errors) {
+        log.debug("ChangePasswordValidator validator called");
+        NewUser newUser = (NewUser) target;
+        boolean validHash = getUserService().isValidReminderRequest(newUser.getForgotPasswordHash(), minutesValid);
+        if (!validHash) {
+            errors.rejectValue(FIELD_PASSWORD, "page.changepassword.expired");
+            // skip further validating anything else, does not make sense
+            return;
+        }
+
+        // we only check for password:
+        validatePassword(errors, newUser);
+        validateConfirmPassword(errors, newUser);
+
+    }
+
+}

Modified: incubator/rave/trunk/rave-components/rave-web/src/main/java/org/apache/rave/portal/web/validator/NewAccountValidator.java
URL: http://svn.apache.org/viewvc/incubator/rave/trunk/rave-components/rave-web/src/main/java/org/apache/rave/portal/web/validator/NewAccountValidator.java?rev=1292365&r1=1292364&r2=1292365&view=diff
==============================================================================
--- incubator/rave/trunk/rave-components/rave-web/src/main/java/org/apache/rave/portal/web/validator/NewAccountValidator.java (original)
+++ incubator/rave/trunk/rave-components/rave-web/src/main/java/org/apache/rave/portal/web/validator/NewAccountValidator.java Wed Feb 22 16:05:57 2012
@@ -30,17 +30,20 @@ import org.springframework.validation.Er
 import org.springframework.validation.ObjectError;
 import org.springframework.validation.Validator;
 
+import java.util.regex.Pattern;
+
 @Component
 public class NewAccountValidator implements Validator {
 
     private static final int MINIMUM_PASSWORD_LENGTH = 4;
     private static final String FIELD_USERNAME = "username";
-    private static final String FIELD_PASSWORD = "password";
+    protected static final String FIELD_PASSWORD = "password";
     private static final String FIELD_CONFIRM_PASSWORD = "confirmPassword";
-    private static final String FIELD_EMAIL = "email";
+    protected static final String FIELD_EMAIL = "email";
     private final Logger logger = LoggerFactory.getLogger(getClass());
+    private static final String USERNAME_MASK = "[\\w\\+\\-\\.@]{2,}";
 
-    private static final String USERNAME_PATTERN = "[\\w\\+\\-\\.@]{2,}";
+    private static final Pattern USERNAME_PATTERN = Pattern.compile(USERNAME_MASK);
     private UserService userService;
 
     @Autowired
@@ -48,7 +51,7 @@ public class NewAccountValidator impleme
         this.userService = userService;
     }
 
-    public boolean supports(Class aClass) {
+    public boolean supports(Class<?> aClass) {
         return NewUser.class.isAssignableFrom(aClass);
     }
 
@@ -69,7 +72,7 @@ public class NewAccountValidator impleme
         if (StringUtils.isBlank(username)) {
             errors.rejectValue(FIELD_USERNAME, "username.required");
             logger.info("Username required");
-        } else if (!username.matches(USERNAME_PATTERN)) {
+        } else if (!USERNAME_PATTERN.matcher(username).matches()) {
             errors.rejectValue(FIELD_USERNAME, "username.invalid.pattern");
             logger.info("Username has invalid pattern");
         } else if (isExistingUsername(username)) {
@@ -82,7 +85,7 @@ public class NewAccountValidator impleme
         return userService.getUserByUsername(username) != null;
     }
 
-    private void validatePassword(Errors errors, NewUser newUser) {
+    protected void validatePassword(Errors errors, NewUser newUser) {
         if (StringUtils.isBlank(newUser.getPassword())) {
             errors.rejectValue(FIELD_PASSWORD, "password.required");
             logger.info("Password required");
@@ -92,7 +95,7 @@ public class NewAccountValidator impleme
         }
     }
 
-    private void validateConfirmPassword(Errors errors, NewUser newUser) {
+    protected void validateConfirmPassword(Errors errors, NewUser newUser) {
         if (StringUtils.isBlank(newUser.getConfirmPassword())) {
             errors.rejectValue(FIELD_CONFIRM_PASSWORD, "confirmPassword.required");
             logger.info("Confirm Password required");
@@ -116,15 +119,15 @@ public class NewAccountValidator impleme
         }
     }
 
-    private boolean isInvalidEmailAddress(String emailAddress) {
+    protected boolean isInvalidEmailAddress(String emailAddress) {
         return !EmailValidator.getInstance().isValid(emailAddress);
     }
 
-    private boolean isExistingEmailAddress(String email) {
+    protected boolean isExistingEmailAddress(String email) {
         return userService.getUserByEmail(email) != null;
     }
 
-    private void writeResultToLog(Errors errors) {
+    protected void writeResultToLog(Errors errors) {
         if (errors.hasErrors()) {
             if (logger.isInfoEnabled()) {
                 for (ObjectError error : errors.getAllErrors()) {
@@ -135,4 +138,8 @@ public class NewAccountValidator impleme
             logger.debug("Validation successful");
         }
     }
+
+    public UserService getUserService() {
+        return userService;
+    }
 }
\ No newline at end of file

Added: incubator/rave/trunk/rave-components/rave-web/src/main/java/org/apache/rave/portal/web/validator/NewPasswordValidator.java
URL: http://svn.apache.org/viewvc/incubator/rave/trunk/rave-components/rave-web/src/main/java/org/apache/rave/portal/web/validator/NewPasswordValidator.java?rev=1292365&view=auto
==============================================================================
--- incubator/rave/trunk/rave-components/rave-web/src/main/java/org/apache/rave/portal/web/validator/NewPasswordValidator.java (added)
+++ incubator/rave/trunk/rave-components/rave-web/src/main/java/org/apache/rave/portal/web/validator/NewPasswordValidator.java Wed Feb 22 16:05:57 2012
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.rave.portal.web.validator;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.rave.portal.model.NewUser;
+import org.apache.rave.portal.model.User;
+import org.apache.rave.portal.service.UserService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import org.springframework.validation.Errors;
+
+/**
+ * Validates password change requests. Valid requests are only ones for users we can find by email address
+ *
+ * @version "$Id$"
+ */
+@Component
+public class NewPasswordValidator extends NewAccountValidator {
+
+    private static Logger log = LoggerFactory.getLogger(NewPasswordValidator.class);
+
+
+    @Autowired
+    public NewPasswordValidator(UserService userService) {
+        super(userService);
+    }
+
+    @Override
+    public void validate(Object target, Errors errors) {
+        log.debug("Password validator called");
+        NewUser newUser = (NewUser) target;
+        // we only check for existing (and valid) email
+        String email = newUser.getEmail();
+        validateEmail(errors, email);
+        if (errors.hasErrors()) {
+            return;
+        }
+        // check if account exists and if it is locked or expired:
+        User user = getUserService().getUserByEmail(email);
+        if (user == null) {
+            errors.rejectValue(FIELD_EMAIL, "account.invalid");
+            log.info("Couldn't find user for email {}", email);
+            return;
+        }
+        if (user.isLocked() || user.isExpired() || !user.isEnabled()) {
+            errors.rejectValue(FIELD_EMAIL, "account.invalid");
+        }
+
+    }
+
+    private void validateEmail(Errors errors, String email) {
+        if (StringUtils.isBlank(email)) {
+            errors.rejectValue(FIELD_EMAIL, "email.required");
+        } else if (isInvalidEmailAddress(email)) {
+            errors.rejectValue(FIELD_EMAIL, "email.invalid");
+        } else if (!isExistingEmailAddress(email)) {
+            errors.rejectValue(FIELD_EMAIL, "email.doesnot.exist");
+        }
+    }
+
+
+}

Added: incubator/rave/trunk/rave-components/rave-web/src/test/java/org/apache/rave/portal/web/controller/ChangePasswordControllerTest.java
URL: http://svn.apache.org/viewvc/incubator/rave/trunk/rave-components/rave-web/src/test/java/org/apache/rave/portal/web/controller/ChangePasswordControllerTest.java?rev=1292365&view=auto
==============================================================================
--- incubator/rave/trunk/rave-components/rave-web/src/test/java/org/apache/rave/portal/web/controller/ChangePasswordControllerTest.java (added)
+++ incubator/rave/trunk/rave-components/rave-web/src/test/java/org/apache/rave/portal/web/controller/ChangePasswordControllerTest.java Wed Feb 22 16:05:57 2012
@@ -0,0 +1,114 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.rave.portal.web.controller;
+
+import org.apache.rave.portal.model.NewUser;
+import org.apache.rave.portal.service.UserService;
+import org.apache.rave.portal.web.util.ModelKeys;
+import org.apache.rave.portal.web.util.ViewNames;
+import org.apache.rave.portal.web.validator.ChangePasswordValidator;
+import org.hamcrest.CoreMatchers;
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.ui.Model;
+import org.springframework.validation.BindingResult;
+import org.springframework.validation.DirectFieldBindingResult;
+import org.springframework.web.servlet.mvc.support.RedirectAttributes;
+
+import javax.servlet.http.HttpServletRequest;
+
+import static org.easymock.EasyMock.createNiceMock;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.replay;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+/**
+ * @version "$Id$"
+ */
+public class ChangePasswordControllerTest {
+
+    private ChangePasswordValidator passwordValidator;
+    private ChangePasswordController controller;
+
+    @Before
+    public void setUp() throws Exception {
+        UserService userService = createNiceMock(UserService.class);
+
+        expect(userService.isValidReminderRequest(null, 0)).andReturn(true).anyTimes();
+        replay(userService);
+        passwordValidator = new ChangePasswordValidator(userService);
+        controller = new ChangePasswordController(userService, passwordValidator);
+    }
+
+
+    @Test
+    public void testInitialize() throws Exception {
+        final Model model = createNiceMock(Model.class);
+        RedirectAttributes redirectAttributes = createNiceMock(RedirectAttributes.class);
+        replay(redirectAttributes);
+        replay(model);
+        String viewName = controller.initialize(model, null, redirectAttributes);
+        assertThat(viewName, CoreMatchers.equalTo(ViewNames.PASSWORD_CHANGE));
+    }
+
+    @Test
+    public void testUpdate() throws Exception {
+        final Model model = createNiceMock(Model.class);
+        RedirectAttributes redirectAttributes = createNiceMock(RedirectAttributes.class);
+        NewUser newUser = new NewUser();
+        replay(redirectAttributes);
+        replay(model);
+        BindingResult results = new DirectFieldBindingResult(newUser, ModelKeys.NEW_USER);
+        String viewName = controller.update(newUser, results, redirectAttributes);
+        assertThat(viewName, CoreMatchers.equalTo(ViewNames.PASSWORD_CHANGE));
+        assertThat(results.hasErrors(), CoreMatchers.equalTo(true));
+        assertThat(results.getErrorCount(), CoreMatchers.equalTo(2));
+        // invalid password, to short:
+        newUser.setPassword("123");
+        results = new DirectFieldBindingResult(newUser, ModelKeys.NEW_USER);
+        controller.update(newUser, results, redirectAttributes);
+        assertEquals("Expected password errors", 2, results.getErrorCount());
+        assertEquals("Expected password errors", "password.invalid.length", results.getFieldError().getCode());
+        // missing password confirm:
+        newUser.setPassword("1234");
+        results = new DirectFieldBindingResult(newUser, ModelKeys.NEW_USER);
+        controller.update(newUser, results, redirectAttributes);
+        assertEquals("Expected password errors", 1, results.getErrorCount());
+        assertEquals("Expected password errors", "confirmPassword.required", results.getFieldError().getCode());
+        // password confirm not equal:
+        newUser.setPassword("1234");
+        newUser.setConfirmPassword("12345");
+        results = new DirectFieldBindingResult(newUser, ModelKeys.NEW_USER);
+        controller.update(newUser, results, redirectAttributes);
+        assertEquals("Expected password errors", 1, results.getErrorCount());
+        assertEquals("Expected password errors", "confirmPassword.mismatch", results.getFieldError().getCode());
+
+        // ok request
+        newUser.setPassword("1234");
+        newUser.setConfirmPassword("1234");
+        results = new DirectFieldBindingResult(newUser, ModelKeys.NEW_USER);
+        controller.update(newUser, results, redirectAttributes);
+        assertEquals("Expected password errors", 0, results.getErrorCount());
+
+
+    }
+}

Added: incubator/rave/trunk/rave-components/rave-web/src/test/java/org/apache/rave/portal/web/controller/ReminderControllerTest.java
URL: http://svn.apache.org/viewvc/incubator/rave/trunk/rave-components/rave-web/src/test/java/org/apache/rave/portal/web/controller/ReminderControllerTest.java?rev=1292365&view=auto
==============================================================================
--- incubator/rave/trunk/rave-components/rave-web/src/test/java/org/apache/rave/portal/web/controller/ReminderControllerTest.java (added)
+++ incubator/rave/trunk/rave-components/rave-web/src/test/java/org/apache/rave/portal/web/controller/ReminderControllerTest.java Wed Feb 22 16:05:57 2012
@@ -0,0 +1,136 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.rave.portal.web.controller;
+
+import org.apache.rave.portal.model.NewUser;
+import org.apache.rave.portal.service.CaptchaService;
+import org.apache.rave.portal.service.UserService;
+import org.apache.rave.portal.service.impl.ReCaptchaService;
+import org.apache.rave.portal.web.util.ModelKeys;
+import org.apache.rave.portal.web.util.ViewNames;
+import org.apache.rave.portal.web.validator.NewPasswordValidator;
+import org.hamcrest.CoreMatchers;
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.ui.Model;
+import org.springframework.ui.ModelMap;
+import org.springframework.validation.BindingResult;
+import org.springframework.validation.DirectFieldBindingResult;
+import org.springframework.web.servlet.mvc.support.RedirectAttributes;
+
+import javax.servlet.http.HttpServletRequest;
+
+import static org.easymock.EasyMock.createNiceMock;
+import static org.easymock.EasyMock.replay;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+/**
+ * @version "$Id$"
+ */
+public class ReminderControllerTest {
+
+    private CaptchaService captchaService;
+    private HttpServletRequest request;
+    private ReminderController controller;
+
+    @Before
+    public void setUp() throws Exception {
+
+        UserService userService = createNiceMock(UserService.class);
+        replay(userService);
+        request = new MockHttpServletRequest();
+        NewPasswordValidator passwordValidator = new NewPasswordValidator(userService);
+        captchaService = new ReCaptchaService(false, null, null, false, "error message");
+        controller = new ReminderController(userService, passwordValidator, captchaService);
+    }
+
+
+    @Test
+    public void testInitialize() throws Exception {
+        // test is model is added:
+        final ModelMap model = new ModelMap();
+        controller.initialize(model, request);
+        assertThat(model, CoreMatchers.notNullValue());
+        // captcha & user mode should be
+        assertEquals("Expected Captcha and NewUser model", 2, model.size());
+        assertThat(model.containsAttribute(ModelKeys.NEW_USER), CoreMatchers.equalTo(true));
+        assertThat(model.containsAttribute(ModelKeys.CAPTCHA_HTML), CoreMatchers.equalTo(true));
+        assertThat(model.get(ModelKeys.NEW_USER), CoreMatchers.notNullValue());
+        assertThat(model.get(ModelKeys.CAPTCHA_HTML), CoreMatchers.notNullValue());
+        assertThat(captchaService.isValid(request), CoreMatchers.equalTo(true));
+
+    }
+
+    @Test
+    public void testCreate() throws Exception {
+        Model model = createNiceMock(Model.class);
+        NewUser newUser = new NewUser();
+        BindingResult results = new DirectFieldBindingResult(newUser, ModelKeys.NEW_USER);
+        RedirectAttributes redirectAttributes = createNiceMock(RedirectAttributes.class);
+        replay(redirectAttributes);
+        replay(model);
+
+        // user part
+        // required email
+        controller.requestUsername(newUser, results, model, request, redirectAttributes);
+        assertThat(captchaService.isValid(request), CoreMatchers.equalTo(true));
+        assertEquals("Expected email errors", 1, results.getErrorCount());
+        assertEquals("Expected email errors", "email.required", results.getFieldError().getCode());
+        // invalid email
+        results = new DirectFieldBindingResult(newUser, ModelKeys.NEW_USER);
+        newUser.setEmail("test_email");
+        controller.requestUsername(newUser, results, model, request, redirectAttributes);
+        assertEquals("Expected email errors", "email.invalid", results.getFieldError().getCode());
+        // does not exists
+        results = new DirectFieldBindingResult(newUser, ModelKeys.NEW_USER);
+        newUser.setEmail("test@mail.com");
+        String viewResult = controller.requestUsername(newUser, results, model, request, redirectAttributes);
+        assertEquals("Expected email errors", 1, results.getErrorCount());
+        assertEquals("Expected email errors", "email.doesnot.exist", results.getFieldError().getCode());
+        assertThat(viewResult, CoreMatchers.equalTo(ViewNames.USERNAME_REQUEST));
+        // password part:
+        model = createNiceMock(Model.class);
+        newUser = new NewUser();
+        results = new DirectFieldBindingResult(newUser, ModelKeys.NEW_USER);
+        redirectAttributes = createNiceMock(RedirectAttributes.class);
+        replay(redirectAttributes);
+        replay(model);
+        // required email
+        controller.requestPassword(newUser, results, model, request, redirectAttributes);
+        assertThat(captchaService.isValid(request), CoreMatchers.equalTo(true));
+        assertEquals("Expected email errors", 1, results.getErrorCount());
+        assertEquals("Expected email errors", "email.required", results.getFieldError().getCode());
+        // invalid email
+        results = new DirectFieldBindingResult(newUser, ModelKeys.NEW_USER);
+        newUser.setEmail("test_email");
+        controller.requestPassword(newUser, results, model, request, redirectAttributes);
+        assertEquals("Expected email errors", "email.invalid", results.getFieldError().getCode());
+        // does not exists
+        results = new DirectFieldBindingResult(newUser, ModelKeys.NEW_USER);
+        newUser.setEmail("test@mail.com");
+        viewResult = controller.requestPassword(newUser, results, model, request, redirectAttributes);
+        assertEquals("Expected email errors", 1, results.getErrorCount());
+        assertEquals("Expected email errors", "email.doesnot.exist", results.getFieldError().getCode());
+        assertThat(viewResult, CoreMatchers.equalTo(ViewNames.NEW_PASSWORD_REQUEST));
+
+    }
+}

Modified: incubator/rave/trunk/rave-portal-resources/src/main/resources/messages.properties
URL: http://svn.apache.org/viewvc/incubator/rave/trunk/rave-portal-resources/src/main/resources/messages.properties?rev=1292365&r1=1292364&r2=1292365&view=diff
==============================================================================
--- incubator/rave/trunk/rave-portal-resources/src/main/resources/messages.properties (original)
+++ incubator/rave/trunk/rave-portal-resources/src/main/resources/messages.properties Wed Feb 22 16:05:57 2012
@@ -32,11 +32,16 @@ confirmPassword.mismatch=Password mismat
 email.required=Email required
 email.invalid=This is not a valid email address
 email.exists=This email address already exists for a user
-
+email.doesnot.exist=This email address doesn't exists
+account.invalid=Invalid account. Please contact administrator.
 form.some.fields.required=Field marked with * are required
 form.all.fields.required=All fields are required.
 form.field.error.required=This field is required
 
+page.changepassword.button=Change password
+page.changepassword.title=Change password
+page.changepassword.expired=Request to change your password has been expired. Please request another password reminder.
+
 page.error.title=Rave has suffered a brief meltdown
 page.error.message=Please bear with us while we fetch some ice cubes.  In the meantime please try
 page.error.reload=reloading
@@ -103,10 +108,25 @@ page.login.rememberme=Remember me
 page.login.usernamepassword=Username and Password
 page.login.usernamepassword.fail=The username or password is incorrect.
 page.login.usernamepassword.login=Login
+page.login.forgot.password=Forgot password
+page.login.forgot.password.label=Password  reminder
+page.login.forgot.password.button=Request new password
+page.login.forgot.username=Username  reminder
+page.login.forgot.username.label=Username  reminder
+page.login.forgot.username.button=Request username
 
 page.newaccount.title=New Account Application
 page.newaccount.button=Create Account
 
+page.newpassword.title=Request new password
+page.newpassword.email.sent=An email has been sent to {0}.
+page.newpassword.email.sent.login=Click here to login
+page.newpassword.password.button=Request new password
+page.newpassword.password.title=Request new password
+page.newpassword.username.title=Request username
+
+page.retrieveusername.title=Request your username
+
 page.store.title=Widget Store
 page.store.search=Search in widget store
 page.store.search.button=Search
@@ -137,6 +157,8 @@ page.widget.rate.likes=Likes: 
 page.widget.rate.dislikes=Dislikes:
 page.widget.tags.title=Tags:
 page.widget.tags.add=Add New Tag:
+page.widget.tags.or=--or--
+page.widget.tags.select=Select from list:
 
 page.addwidget.title=Add new widget
 page.addwidget.form.header=Widget

Modified: incubator/rave/trunk/rave-portal-resources/src/main/resources/messages_nl.properties
URL: http://svn.apache.org/viewvc/incubator/rave/trunk/rave-portal-resources/src/main/resources/messages_nl.properties?rev=1292365&r1=1292364&r2=1292365&view=diff
==============================================================================
--- incubator/rave/trunk/rave-portal-resources/src/main/resources/messages_nl.properties (original)
+++ incubator/rave/trunk/rave-portal-resources/src/main/resources/messages_nl.properties Wed Feb 22 16:05:57 2012
@@ -32,6 +32,13 @@ confirmPassword.mismatch=Wachtwoord komt
 email.required=Email verplicht
 email.invalid=Dit is geen geldig email adres
 email.exists=Dit email adres bestaat al voor een gebruiker
+email.doesnot.exist=Dit email adres bestaat niet
+account.invalid=Invalid account. Please contact administrator.
+
+page.changepassword.button=Verander password
+page.changepassword.title=Verander password
+page.changepassword.expired=Request to change your password is either invalid or has been expired. Please request another password reminder.
+
 
 form.some.fields.required=Velden met een * zijn verplicht
 form.all.fields.required=Alle velden zijn verplicht
@@ -112,10 +119,25 @@ page.login.rememberme=Onthoud mij
 page.login.usernamepassword=Gebruikersnaam en wachtwoord
 page.login.usernamepassword.fail=De gebruikersnaam of het wachtwoord is incorrect.
 page.login.usernamepassword.login=Inloggen
+page.login.forgot.password=Password  reminder
+page.login.forgot.password.label=Password  reminder
+page.login.forgot.password.button=Request new password
+page.login.forgot.username=Username  reminder
+page.login.forgot.username.label=Username  reminder
+page.login.forgot.username.button=Request username
 
 page.newaccount.title=Nieuw account applicatie
 page.newaccount.button=Cre\u00EBer account
 
+page.newpassword.title=Request new password
+page.newpassword.email.sent=An email has been sent to {0}.
+page.newpassword.email.sent.login=Click here to login
+page.newpassword.password.button=Request new password
+page.newpassword.password.title=Request new password
+page.newpassword.username.title=Request username
+
+page.retrieveusername.title=Request your username
+
 page.store.title=Widgetwinkel
 page.store.search=Zoek in de widgetwinkel
 page.store.search.button=Zoek

Modified: incubator/rave/trunk/rave-portal-resources/src/main/resources/portal.properties
URL: http://svn.apache.org/viewvc/incubator/rave/trunk/rave-portal-resources/src/main/resources/portal.properties?rev=1292365&r1=1292364&r2=1292365&view=diff
==============================================================================
--- incubator/rave/trunk/rave-portal-resources/src/main/resources/portal.properties (original)
+++ incubator/rave/trunk/rave-portal-resources/src/main/resources/portal.properties Wed Feb 22 16:05:57 2012
@@ -57,4 +57,19 @@ portal.captcha.enabled=false
 portal.captcha.key.private=
 portal.captcha.key.public=
 portal.captcha.usenoscript=false
-portal.captcha.invalid.configuration=<label class="error">ReCaptcha service is not properly configured.</label>
\ No newline at end of file
+portal.captcha.invalid.configuration=<label class="error">ReCaptcha service is not properly configured.</label>
+
+#mail settings
+portal.mail.sender=m@mitre.org
+portal.mail.replyto=mfranklin@mitre.org
+portal.mail.host=smtp-bedford.mitre.org
+portal.mail.password=
+portal.mail.username=
+portal.mail.protocol=smtp
+portal.mail.port=25
+portal.mail.username.subject=Rave username reminder service
+portal.mail.username.template=username_reminder.ftl
+portal.mail.passwordservice.subject=Rave password reminder service
+portal.mail.passwordservice.template=password_reminder.ftl
+portal.mail.passwordservice.valid.minutes=30
+portal.mail.service.baseurl=http://localhost:8080/portal/app/changepassword/
\ No newline at end of file