/** * The contents of this file are subject to the OpenMRS Public License * Version 1.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://license.openmrs.org * * Software distributed under the License is distributed on an "AS IS" * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the * License for the specific language governing rights and limitations * under the License. * * Copyright (C) OpenMRS, LLC. All Rights Reserved. */ package org.openmrs.module.sync.web.controller; import java.lang.reflect.Method; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import javax.servlet.ServletException; import javax.servlet.http.HttpSession; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.openmrs.Person; import org.openmrs.PersonName; import org.openmrs.Role; import org.openmrs.User; import org.openmrs.api.APIAuthenticationException; import org.openmrs.api.APIException; import org.openmrs.api.context.Context; import org.openmrs.messagesource.MessageSourceService; import org.openmrs.module.sync.SyncClass; import org.openmrs.module.sync.SyncConstants; import org.openmrs.module.sync.SyncServerClass; import org.openmrs.module.sync.api.SyncService; import org.openmrs.module.sync.serialization.TimestampNormalizer; import org.openmrs.module.sync.server.RemoteServer; import org.openmrs.module.sync.server.RemoteServerType; import org.openmrs.module.sync.server.ServerConnectionState; import org.openmrs.scheduler.SchedulerException; import org.openmrs.scheduler.TaskDefinition; import org.openmrs.web.WebConstants; import org.openmrs.util.OpenmrsConstants; import org.springframework.stereotype.Controller; import org.springframework.ui.ModelMap; import org.springframework.util.StringUtils; import org.springframework.validation.Errors; 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; /** * Controller for the page that lets the user configure a child server, a parent server, or this * server. */ @Controller public class ConfigServerFormController { /** Logger for this class and subclasses */ protected static transient final Log log = LogFactory.getLog(ConfigServerFormController.class); @RequestMapping(value = "/module/sync/configServer", method = RequestMethod.POST, params = "action=saveNewChild") protected String onSaveNewChild(@RequestParam String nickname, @RequestParam String uuid, @RequestParam(required = false) String username, @RequestParam(required = false) String password, @RequestParam String passwordRetype, @RequestParam(required = false) Boolean shouldEmail, @RequestParam String adminEmail, HttpSession httpSession, @ModelAttribute("server") RemoteServer server, Errors errors, @RequestParam(required = false) List<String> notSendTo, @RequestParam(required = false) List<String> notReceiveFrom) throws Exception { if (!Context.isAuthenticated()) throw new APIAuthenticationException("Not authenticated!"); //if the user provided a username, then the passwords are required if (StringUtils.hasText(username)) { if (!StringUtils.hasText(password) || !StringUtils.hasText(passwordRetype)) { errors.rejectValue("password", "sync.config.server.error.passwordRequired"); } else if (!password.equals(passwordRetype)) errors.rejectValue("password", "error.password.match"); } if (!StringUtils.hasLength(nickname)) errors.rejectValue("nickname", "sync.config.server.error.nicknameRequired"); if (errors.hasErrors()) return "/module/sync/configServerForm"; log.debug("in onSave for new child"); MessageSourceService mss = Context.getMessageSourceService(); SyncService syncService = Context.getService(SyncService.class); server.setServerType(RemoteServerType.CHILD); server.setNickname(nickname); server.setUuid(uuid); if (StringUtils.hasText(username)) { // create a new user in either A) 1.5.x or B) 1.6+ User user = null; if (Person.class.isAssignableFrom(User.class)) { // if we're in a pre-1.6 environment, User extends Person user = new User(); // if 1.6+ the User.setGender method does not exist, so if we // don't do this by reflection we will have a compile-time error // (and gender is a required field) Method setGenderMethod = User.class.getMethod("setGender", String.class); setGenderMethod.invoke(user, SyncConstants.DEFAULT_CHILD_SERVER_USER_GENDER); user.setUsername(username); PersonName name = new PersonName(); name.setFamilyName(nickname); name.setGivenName(mss.getMessage(SyncConstants.DEFAULT_CHILD_SERVER_USER_NAME)); user.addName(name); } else { // create a new user in a 1.6+ environemnt where // User does NOT extend Person Person person = new Person(); person.setGender(SyncConstants.DEFAULT_CHILD_SERVER_USER_GENDER); person.setBirthdate(new Date()); PersonName name = new PersonName(); name.setFamilyName(nickname); name.setGivenName(mss.getMessage(SyncConstants.DEFAULT_CHILD_SERVER_USER_NAME)); person.addName(name); Context.getPersonService().savePerson(person); user = new User(person); user.setUsername(username); } String defaultRole = Context.getAdministrationService().getGlobalProperty("sync.default_role"); if (defaultRole != null) { String[] roles = defaultRole.split(","); for (String role : roles) { Role r = Context.getUserService().getRole(role.trim()); if (r != null) user.addRole(r); } } // create in database try { Context.getUserService().saveUser(user, password); server.setChildUsername(user.getUsername()); } catch (Exception e) { log.error("Unable to create new user to associate with child server", e); //Am using the exception message because it is already localized. //You can look at OpenmrsUtil.validatePassword() errors.rejectValue("username", e.getMessage()); return "/module/sync/configServerForm"; } } server.setAddress("N/A"); server.setPassword("N/A"); server.setUsername("N/A"); saveOrUpdateServerClasses(server, notSendTo, notReceiveFrom); server = syncService.saveRemoteServer(server); httpSession.setAttribute(WebConstants.OPENMRS_MSG_ATTR, "sync.config.server.saved"); if (server.getServerId() != null) return "redirect:/module/sync/configServer.form?serverId=" + server.getServerId(); else return "redirect:/module/sync/config.list"; } @RequestMapping(value = "/module/sync/configServer", method = RequestMethod.POST, params = "action=editChild") protected String onSaveCurrentChild(@RequestParam String nickname, @RequestParam String uuid, HttpSession httpSession, @ModelAttribute("server") RemoteServer server, Errors errors, @RequestParam(required = false) List<String> notSendTo, @RequestParam(required = false) List<String> notReceiveFrom) throws Exception { if (!Context.isAuthenticated()) throw new APIAuthenticationException("Not authenticated!"); if (!StringUtils.hasLength(nickname)) errors.rejectValue("nickname", "sync.config.server.error.nicknameRequired"); if (errors.hasErrors()) return "/module/sync/configServerForm"; server.setNickname(nickname); server.setUuid(uuid); saveOrUpdateServerClasses(server, notSendTo, notReceiveFrom); server = Context.getService(SyncService.class).saveRemoteServer(server); httpSession.setAttribute(WebConstants.OPENMRS_MSG_ATTR, "sync.config.server.saved"); if (server.getServerId() != null) return "redirect:/module/sync/configServer.form?serverId=" + server.getServerId(); else return "redirect:/module/sync/config.list"; } @RequestMapping(value = "/module/sync/configServer", method = RequestMethod.POST, params = "action=saveParent") protected String onSaveParent(@RequestParam String nickname, @RequestParam String address, @RequestParam String username, @RequestParam String password, @RequestParam(required = false) Boolean started, @RequestParam(required = false) Integer repeatInterval, HttpSession httpSession, @ModelAttribute("server") RemoteServer server, Errors errors, @RequestParam(required = false) List<String> notSendTo, @RequestParam(required = false) List<String> notReceiveFrom) throws Exception { if (!Context.isAuthenticated()) throw new APIAuthenticationException("Not authenticated!"); if (!StringUtils.hasLength(nickname)) errors.rejectValue("nickname", "sync.config.server.error.nicknameRequired"); if (!StringUtils.hasLength(address)) errors.rejectValue("address", "sync.config.server.error.addressRequired"); if (started == null) { started = false; // if they didn't check the box, the value is false repeatInterval = 0; } if (started && repeatInterval < 1) errors.rejectValue("address", "sync.config.server.error.invalidRepeat"); if (errors.hasErrors()) return "/module/sync/configServerForm"; // interval needs to be in seconds, but we asked the user for minutes. multiply by 60 to convert to minutes repeatInterval = repeatInterval * 60; server.setServerType(RemoteServerType.PARENT); server.setNickname(nickname); // just in case - we want to make sure there is ONLY ever 1 parent if (server.getServerType().equals(RemoteServerType.PARENT)) { RemoteServer parent = Context.getService(SyncService.class).getParentServer(); if (parent != null && parent.getServerId() != server.getServerId()) { throw new APIException( "Oh no! There is another server already stored in the database as the parent: server id : " + parent.getServerId()); } } server.setAddress(address); server.setPassword(password); server.setUsername(username); saveOrUpdateServerClasses(server, notSendTo, notReceiveFrom); server = Context.getService(SyncService.class).saveRemoteServer(server); saveOrUpdateTask(server, started, repeatInterval); httpSession.setAttribute(WebConstants.OPENMRS_MSG_ATTR, "sync.config.parent.saved"); if (server.getServerId() != null) return "redirect:/module/sync/configServer.form?serverId=" + server.getServerId(); else return "redirect:/module/sync/config.list"; } /** * @param server * @param notSendTo * @param notReceiveFrom */ protected void saveOrUpdateServerClasses(RemoteServer server, List<String> notSendTo, List<String> notReceiveFrom) { if (notSendTo == null) notSendTo = Collections.emptyList(); if (notReceiveFrom == null) notReceiveFrom = Collections.emptyList(); log.debug("sendto: " + notSendTo.size()); log.debug("receiveFrom: " + notReceiveFrom.size()); SyncService syncService = Context.getService(SyncService.class); Set<SyncServerClass> currentServerClasses = server.getServerClasses(); // mark all current serverClasses that are not in the lists for (SyncServerClass syncClass : currentServerClasses) { if (!notSendTo.contains(syncClass.getSyncClass().getName()) && !syncClass.getSendTo()) { syncClass.setSendTo(true); } if (!notReceiveFrom.contains(syncClass.getSyncClass().getName()) && !syncClass.getReceiveFrom()) { syncClass.setReceiveFrom(true); } } // unmark all currentSyncClasses that are in the sendTo list for (String className : notSendTo) { boolean foundClass = false; className = className.trim(); for (SyncServerClass currentClass : currentServerClasses) { if (currentClass.getSyncClass().getName().equals(className)) { foundClass = true; currentClass.setSendTo(false); } } // we need to add a new item to the list if (!foundClass) { SyncClass defaultSyncClass = syncService.getSyncClassByName(className); if (defaultSyncClass == null) { defaultSyncClass = new SyncClass(); defaultSyncClass.setName(className); syncService.saveSyncClass(defaultSyncClass); } SyncServerClass newSyncClass = new SyncServerClass(); newSyncClass.setSyncClass(defaultSyncClass); newSyncClass.setSendTo(false); newSyncClass.setSyncServer(server); // we must add this to the list of current classes so that the receiveFrom list picks it up instead of creating a new one currentServerClasses.add(newSyncClass); } } // unmark all currentSyncClasses that are in the sendTo list for (String className : notReceiveFrom) { boolean foundClass = false; className = className.trim(); for (SyncServerClass currentClass : currentServerClasses) { if (currentClass.getSyncClass().getName().equals(className)) { foundClass = true; currentClass.setReceiveFrom(false); } } // we need to add a new item to the list if (!foundClass) { SyncClass defaultSyncClass = syncService.getSyncClassByName(className); if (defaultSyncClass == null) { defaultSyncClass = new SyncClass(); defaultSyncClass.setName(className); syncService.saveSyncClass(defaultSyncClass); } SyncServerClass newSyncServerClass = new SyncServerClass(); newSyncServerClass.setSyncClass(defaultSyncClass); newSyncServerClass.setReceiveFrom(false); newSyncServerClass.setSyncServer(server); // must put this on the currentServerClasses so it gets saved currentServerClasses.add(newSyncServerClass); } } } /** * Creates a task for a parent server if that task doesn't exist. If the task does exist, update * it. * * @param server * @param started * @param repeatInterval */ protected void saveOrUpdateTask(RemoteServer server, Boolean started, Integer repeatInterval) throws SchedulerException { Integer serverId = server.getServerId(); MessageSourceService mss = Context.getMessageSourceService(); try { //Add privilege to enable us access the registered tasks Context.addProxyPrivilege(OpenmrsConstants.PRIV_MANAGE_SCHEDULER); TaskDefinition serverSchedule = null; Collection<TaskDefinition> tasks = Context.getSchedulerService().getRegisteredTasks(); if (tasks != null) { for (TaskDefinition task : tasks) { if (task.getTaskClass().equals(SyncConstants.SCHEDULED_TASK_CLASS)) { if (serverId.toString().equals(task.getProperty(SyncConstants.SCHEDULED_TASK_PROPERTY_SERVER_ID))) { serverSchedule = task; } else { log.warn("not equal comparing " + serverId + " to " + task.getProperty(SyncConstants.SCHEDULED_TASK_PROPERTY_SERVER_ID)); } } else { log.warn("not equal comparing " + task.getTaskClass() + " to " + SyncConstants.SCHEDULED_TASK_CLASS); } } } else { log.warn("tasks is null"); } Map<String, String> props = new HashMap<String, String>(); props.put(SyncConstants.SCHEDULED_TASK_PROPERTY_SERVER_ID, serverId.toString()); if (serverSchedule != null) { if (log.isInfoEnabled()) log.info("Sync scheduled task exists, and started is " + started + " and interval is " + repeatInterval); try { Context.getSchedulerService().shutdownTask(serverSchedule); } catch (Exception e) { log.warn("Sync task had run wild, couldn't stop it because it wasn't really running", e); // nothing to do - means something was wrong or not yet started //TODO: is this right? should we report error here on 'STRICT'? } serverSchedule.setStarted(started); serverSchedule.setRepeatInterval((long) repeatInterval); serverSchedule.setStartOnStartup(started); serverSchedule.setProperties(props); if (started) { serverSchedule.setStartTime(new Date()); } Context.getSchedulerService().saveTask(serverSchedule); if (started) { Context.getSchedulerService().scheduleTask(serverSchedule); } } else { if (log.isInfoEnabled()) log.info("Sync scheduled task does not exists, and started is " + started + " and interval is " + repeatInterval); if (started) { serverSchedule = new TaskDefinition(); serverSchedule.setName(server.getNickname() + " " + mss.getMessage("sync.config.server.scheduler")); serverSchedule.setDescription(mss.getMessage("sync.config.server.scheduler.description")); serverSchedule.setRepeatInterval((long) repeatInterval); serverSchedule.setStartTime(new Date()); serverSchedule.setTaskClass(SyncConstants.SCHEDULED_TASK_CLASS); serverSchedule.setStarted(started); serverSchedule.setStartOnStartup(started); serverSchedule.setProperties(props); Context.getSchedulerService().saveTask(serverSchedule); Context.getSchedulerService().scheduleTask(serverSchedule); } } } finally { //We no longer need this privilege. Context.removeProxyPrivilege(OpenmrsConstants.PRIV_MANAGE_SCHEDULER); } } /** * This is called prior to displaying a form for the first time. It tells Spring * {@link RemoteServer} to use. This same object is rebuilt on form submission and passed into * the onSave* methods. * * @see org.springframework.web.servlet.mvc.AbstractFormController#formBackingObject(javax.servlet.http.HttpServletRequest) */ @ModelAttribute("server") protected RemoteServer formBackingObject(@RequestParam(value = "type", required = false) String serverType, @RequestParam(value = "serverId", required = false) Integer serverId) throws ServletException { RemoteServer server = null; log.debug("IN FORMBACKING, type is " + serverType); // only fill the Object if the user has authenticated properly if (Context.isAuthenticated()) { if (serverId != null) server = Context.getService(SyncService.class).getRemoteServer(serverId); if (server == null && serverType != null) { server = new RemoteServer(); server.setServerType(RemoteServerType.valueOf(serverType)); // set the classes from the defaults Set<SyncServerClass> serverClasses = new HashSet<SyncServerClass>(); List<SyncClass> classes = Context.getService(SyncService.class).getSyncClasses(); if (classes != null) { for (SyncClass syncClass : classes) { SyncServerClass serverClass = new SyncServerClass(server, syncClass); serverClasses.add(serverClass); } } server.setServerClasses(serverClasses); } } return server; } @SuppressWarnings("unchecked") @RequestMapping(value = "/module/sync/configServer", method = RequestMethod.GET) protected String showPage(ModelMap modelMap, @ModelAttribute("server") RemoteServer server, @RequestParam(value = "type", required = false) String serverType) throws Exception { if (Context.isAuthenticated()) { if (serverType == null || RemoteServerType.PARENT.equals(serverType) || RemoteServerType.PARENT.equals(server.getServerType())) { // testConnection error messages MessageSourceService mss = Context.getMessageSourceService(); Map<String, String> connectionState = new HashMap<String, String>(); connectionState.put(ServerConnectionState.OK.toString(), mss.getMessage("sync.config.server.connection.status.ok")); connectionState.put(ServerConnectionState.AUTHORIZATION_FAILED.toString(), mss.getMessage("sync.config.server.connection.status.noAuth")); connectionState.put(ServerConnectionState.CONNECTION_FAILED.toString(), mss.getMessage("sync.config.server.connection.status.noConnection")); connectionState.put(ServerConnectionState.CERTIFICATE_FAILED.toString(), mss.getMessage("sync.config.server.connection.status.noCertificate")); connectionState.put(ServerConnectionState.MALFORMED_URL.toString(), mss.getMessage("sync.config.server.connection.status.badUrl")); connectionState.put(ServerConnectionState.NO_ADDRESS.toString(), mss.getMessage("sync.config.server.connection.status.noAddress")); try { //Add privilege to enable us access the registered tasks Context.addProxyPrivilege(OpenmrsConstants.PRIV_MANAGE_SCHEDULER); // get repeatInterval for tasks taskConfig for automated syncing TaskDefinition serverSchedule = new TaskDefinition(); String repeatInterval = ""; if (server != null) { if (server.getServerId() != null) { Collection<TaskDefinition> tasks = Context.getSchedulerService().getRegisteredTasks(); if (tasks != null) { String serverId = server.getServerId().toString(); for (TaskDefinition task : tasks) { if (task.getTaskClass().equals(SyncConstants.SCHEDULED_TASK_CLASS)) { if (serverId.equals(task.getProperty(SyncConstants.SCHEDULED_TASK_PROPERTY_SERVER_ID))) { serverSchedule = task; Long repeat = serverSchedule.getRepeatInterval() / 60; repeatInterval = repeat.toString(); if (repeatInterval.indexOf(".") > -1) repeatInterval = repeatInterval.substring(0, repeatInterval.indexOf(".")); } } } } } } modelMap.put("connectionState", connectionState.entrySet()); modelMap.put("serverSchedule", serverSchedule); modelMap.put("repeatInterval", repeatInterval); } finally { //We no longer need this privilege. Context.removeProxyPrivilege(OpenmrsConstants.PRIV_MANAGE_SCHEDULER); } } modelMap.put("syncDateDisplayFormat", TimestampNormalizer.DATETIME_DISPLAY_FORMAT); modelMap.put("type", serverType); //sync status stuff for this server SyncService syncService = Context.getService(SyncService.class); modelMap.put("localServerUuid", syncService.getServerUuid()); modelMap.put("localServerName", syncService.getServerName()); modelMap.put("localServerAdminEmail", syncService.getAdminEmail()); } return "/module/sync/configServerForm"; } }