/* ==================================================================
* NodeAssociationController.java - Sep 6, 2011 1:34:13 PM
*
* Copyright 2007-2011 SolarNetwork.net Dev Team
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
* 02111-1307 USA
* ==================================================================
* $Id$
* ==================================================================
*/
package net.solarnetwork.node.setup.web;
import java.io.IOException;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.Future;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.keygen.KeyGenerators;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
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.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.multipart.MultipartFile;
import net.solarnetwork.domain.NetworkAssociation;
import net.solarnetwork.domain.NetworkAssociationDetails;
import net.solarnetwork.domain.NetworkCertificate;
import net.solarnetwork.node.backup.Backup;
import net.solarnetwork.node.backup.BackupInfo;
import net.solarnetwork.node.backup.BackupManager;
import net.solarnetwork.node.backup.BackupService;
import net.solarnetwork.node.setup.InvalidVerificationCodeException;
import net.solarnetwork.node.setup.PKIService;
import net.solarnetwork.node.setup.SetupException;
import net.solarnetwork.node.setup.UserProfile;
import net.solarnetwork.node.setup.UserService;
import net.solarnetwork.node.setup.web.support.AssociateNodeCommand;
import net.solarnetwork.util.OptionalService;
import net.solarnetwork.web.domain.Response;
/**
* Controller used to associate a node with a SolarNet account.
*
* @author maxieduncan
* @version 1.2
*/
@Controller
@SessionAttributes({ NodeAssociationController.KEY_DETAILS, NodeAssociationController.KEY_IDENTITY })
@RequestMapping("/associate")
public class NodeAssociationController extends BaseSetupController {
private static final String BACKUP_KEY_SESSION_KEY = "restoreBackupKey";
private static final String PAGE_ENTER_CODE = "associate/enter-code";
private static final String PAGE_IMPORT_FROM_BACKUP = "associate/import-from-backup";
private static final String PAGE_RESTORE_FROM_BACKUP = "associate/restore-from-backup";
/** The model attribute for the network association details. */
public static final String KEY_DETAILS = "details";
/** The model attribute for the network identity details. */
public static final String KEY_IDENTITY = "association";
/** The model attribute for the network identity details. */
public static final String KEY_NETWORK_URL_MAP = "networkLinks";
/**
* The model attribute for a {@link UserProfile} instance.
*
* @since 1.1
*/
public static final String KEY_USER = "user";
@Autowired
private MessageSource messageSource;
@Autowired
private PKIService pkiService;
@Autowired
private UserService userService;
@Resource(name = "authenticationManager")
private AuthenticationManager authenticationManager;
@Resource(name = "backupManager")
private OptionalService<BackupManager> backupManagerTracker;
@Resource(name = "networkLinks")
private Map<String, String> networkURLs = new HashMap<String, String>(4);
/**
* Node association entry point.
*
* @param model
* the model
* @return the view name
*/
@RequestMapping(value = "", method = RequestMethod.GET)
public String setupForm(Model model) {
model.addAttribute("command", new AssociateNodeCommand());
model.addAttribute(KEY_NETWORK_URL_MAP, networkURLs);
return PAGE_ENTER_CODE;
}
/**
* Decode the invitation code, and present the decoded information for the
* user to verify.
*
* @param command
* the command
* @param errors
* the binding result
* @param model
* the model
* @return the view name
*/
@RequestMapping(value = "/preview", method = RequestMethod.POST)
public String previewInvitation(@ModelAttribute("command") AssociateNodeCommand command,
Errors errors, Model model) {
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "verificationCode", "field.required");
if ( errors.hasErrors() ) {
return PAGE_ENTER_CODE;
}
try {
NetworkAssociationDetails details = getSetupBiz()
.decodeVerificationCode(command.getVerificationCode());
model.addAttribute(KEY_DETAILS, details);
} catch ( InvalidVerificationCodeException e ) {
errors.rejectValue("verificationCode", "verificationCode.invalid", null, null);
return PAGE_ENTER_CODE;
}
return "associate/preview-invitation";
}
/**
* Decodes the supplied verification code storing the details for the user
* to validation.
*
* @param command
* the associate comment, used only for reporting errors
* @param errors
* the errors associated with the command
* @param details
* the session details objects
* @param model
* the view model
* @return the view name
*/
@RequestMapping(value = "/verify", method = RequestMethod.POST)
public String verifyCode(@ModelAttribute("command") AssociateNodeCommand command, Errors errors,
@ModelAttribute(KEY_DETAILS) NetworkAssociationDetails details, Model model) {
// Check expiration date
if ( details.getExpiration().getTime() < System.currentTimeMillis() ) {
errors.rejectValue("verificationCode", "verificationCode.expired", null, null);
return PAGE_ENTER_CODE;
}
try {
// Retrieve the identity from the server
NetworkAssociation na = getSetupBiz().retrieveNetworkAssociation(details);
model.addAttribute(KEY_IDENTITY, na);
} catch ( SetupException e ) {
errors.reject("node.setup.identity.error", new Object[] { details.getHost() }, null);
return setupForm(model);
} catch ( RuntimeException e ) {
log.error("Unexpected exception processing /setup/verify", e);
// We are assuming any exception thrown here is caused by the server being down,
// but there's no guarantee this is the case
errors.reject("node.setup.identity.error", new Object[] { details.getHost() }, null);
return PAGE_ENTER_CODE;
}
return "associate/verify-identity";
}
/**
* Confirms the node association with the SolarNet server supplied in the
* verification code.
*
* @param command
* the associate comment, used only for reporting errors
* @param errors
* the errors associated with the command
* @param details
* the session details objects
* @param model
* the view model
* @return the view name
*/
@RequestMapping(value = "/confirm", method = RequestMethod.POST)
public String confirmIdentity(@ModelAttribute("command") AssociateNodeCommand command, Errors errors,
@ModelAttribute(KEY_DETAILS) NetworkAssociationDetails details, Model model) {
try {
// now that the association has been confirmed get send confirmation to the server
NetworkAssociationDetails req = new NetworkAssociationDetails(details);
req.setUsername(details.getUsername());
req.setKeystorePassword(command.getKeystorePassword());
NetworkCertificate cert = getSetupBiz().acceptNetworkAssociation(req);
details.setNetworkId(cert.getNetworkId());
if ( cert.getNetworkCertificateStatus() != null ) {
details.setNetworkCertificateStatus(cert.getNetworkCertificateStatus());
details.setNetworkCertificateSubjectDN(cert.getNetworkCertificateSubjectDN());
details.setNetworkCertificate(cert.getNetworkCertificate());
} else {
// generate certificate request
model.addAttribute("csr", pkiService.generateNodePKCS10CertificateRequestString());
}
if ( !userService.someUserExists() ) {
// create a new user now, using the username from SolarNet and a random password
UserProfile user = new UserProfile();
user.setUsername(details.getUsername());
user.setPassword(KeyGenerators.string().generateKey());
user.setPasswordAgain(user.getPassword());
log.debug("Creating initial user {} with password {}", user.getUsername(),
user.getPassword());
userService.storeUserProfile(user);
model.addAttribute(KEY_USER, user);
// and automatically log in as the new user
UsernamePasswordAuthenticationToken loginReq = new UsernamePasswordAuthenticationToken(
user.getUsername(), user.getPassword());
Authentication auth = authenticationManager.authenticate(loginReq);
SecurityContextHolder.getContext().setAuthentication(auth);
}
return "associate/setup-success";
} catch ( Exception e ) {
errors.reject("node.setup.success.error", new Object[] { details.getHost() }, null);
return PAGE_ENTER_CODE;
}
}
@RequestMapping(value = "/restore", method = RequestMethod.GET)
public String restoreFromBackup() {
return PAGE_IMPORT_FROM_BACKUP;
}
@RequestMapping(value = "/importBackup", method = RequestMethod.POST)
public String importBackup(@RequestParam("file") MultipartFile file, HttpServletRequest request)
throws IOException {
final BackupManager manager = backupManagerTracker.service();
if ( manager == null ) {
request.getSession(true).setAttribute("errorMessageKey",
"node.setup.restore.error.noBackupManager");
return "redirect:/associate";
}
Map<String, String> props = new HashMap<String, String>();
props.put(BackupManager.BACKUP_KEY, file.getOriginalFilename());
try {
Future<Backup> backupFuture = manager.importBackupArchive(file.getInputStream(), props);
Backup backup = backupFuture.get();
if ( backup != null ) {
request.getSession(true).setAttribute(BACKUP_KEY_SESSION_KEY, backup.getKey());
return PAGE_RESTORE_FROM_BACKUP;
}
request.getSession(true).setAttribute("errorMessageKey", "node.setup.restore.error.unknown");
request.getSession(true).setAttribute("errorMessageParam0", "Backup not imported");
return "redirect:/associate";
} catch ( Exception e ) {
log.error("Exception restoring backup archive", e);
Throwable root = e;
while ( root.getCause() != null ) {
root = root.getCause();
}
request.getSession(true).setAttribute("errorMessageKey", "node.setup.restore.error.unknown");
request.getSession(true).setAttribute("errorMessageParam0", root.getMessage());
return "redirect:/associate";
}
}
@RequestMapping(value = "/importedBackup", method = RequestMethod.GET)
@ResponseBody
public Response<BackupInfo> importedBackup(Locale locale, HttpServletRequest request)
throws IOException {
final BackupManager manager = backupManagerTracker.service();
if ( manager == null ) {
return new Response<BackupInfo>(false, "500", "No backup manager available.", null);
}
final String backupKey = (String) request.getSession(true).getAttribute(BACKUP_KEY_SESSION_KEY);
if ( backupKey == null ) {
return new Response<BackupInfo>(false, "404", "No imported backup available.", null);
}
try {
BackupInfo info = manager.infoForBackup(backupKey, locale);
return Response.response(info);
} catch ( Exception e ) {
log.error("Exception importing backup archive", e);
Throwable root = e;
while ( root.getCause() != null ) {
root = root.getCause();
}
return new Response<BackupInfo>(false, "500", e.getMessage(), null);
}
}
@RequestMapping(value = "/restoreBackup", method = RequestMethod.POST)
@ResponseBody
public Response<?> restoreBackup(BackupOptions options, Locale locale, HttpServletRequest request) {
final BackupManager manager = backupManagerTracker.service();
if ( manager == null ) {
return new Response<Backup>(false, "500", "No backup manager available.", null);
}
final BackupService backupService = manager.activeBackupService();
if ( backupService == null ) {
return new Response<Backup>(false, "500", "No backup service available.", null);
}
final String backupKey = (String) request.getSession(true).getAttribute(BACKUP_KEY_SESSION_KEY);
if ( backupKey == null ) {
return new Response<Object>(false, "404", "No imported backup available.", null);
}
Backup backup = manager.activeBackupService().backupForKey(backupKey);
if ( backup == null ) {
return new Response<Object>(false, "404", "Imported backup not available.", null);
}
Map<String, String> props = options.asBackupManagerProperties();
manager.restoreBackup(backup, props);
request.getSession().removeAttribute(BACKUP_KEY_SESSION_KEY);
shutdownSoon();
return new Response<Object>(true, null,
messageSource.getMessage("node.setup.restore.success", null, locale), null);
}
public void setPkiService(PKIService pkiService) {
this.pkiService = pkiService;
}
public void setBackupManagerTracker(OptionalService<BackupManager> backupManagerTracker) {
this.backupManagerTracker = backupManagerTracker;
}
public void setNetworkURLs(Map<String, String> networkURLs) {
this.networkURLs = networkURLs;
}
}