/** * 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.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; import java.io.StringWriter; import java.util.Date; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.io.IOUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.openmrs.api.context.Context; import org.openmrs.module.sync.SyncConstants; import org.openmrs.module.sync.SyncTransmission; import org.openmrs.module.sync.SyncTransmissionState; import org.openmrs.module.sync.SyncUtil; import org.openmrs.module.sync.SyncUtilTransmission; import org.openmrs.module.sync.api.SyncIngestService; import org.openmrs.module.sync.api.SyncService; import org.openmrs.module.sync.ingest.SyncDeserializer; import org.openmrs.module.sync.ingest.SyncImportRecord; import org.openmrs.module.sync.ingest.SyncTransmissionResponse; import org.openmrs.module.sync.server.ConnectionRequest; import org.openmrs.module.sync.server.ConnectionResponse; import org.openmrs.module.sync.server.RemoteServer; import org.springframework.validation.BindException; import org.springframework.web.bind.ServletRequestDataBinder; import org.springframework.web.bind.ServletRequestUtils; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartHttpServletRequest; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.mvc.SimpleFormController; public class ImportListController extends SimpleFormController { /** Logger for this class and subclasses */ protected final Log log = LogFactory.getLog(getClass()); /** * @see org.springframework.web.servlet.mvc.BaseCommandController#initBinder(javax.servlet.http.HttpServletRequest, * org.springframework.web.bind.ServletRequestDataBinder) */ protected void initBinder(HttpServletRequest request, ServletRequestDataBinder binder) throws Exception { super.initBinder(request, binder); } @Override protected ModelAndView processFormSubmission(HttpServletRequest request, HttpServletResponse response, Object obj, BindException errors) throws Exception { log.info("***********************************************************\n"); log.info("Inside SynchronizationImportListController"); // There are 3 ways to come to this point, so we'll handle all of them: // 1) uploading a file (results in a file attachment as response) // 2) posting data to page (results in pure XML output) // 3) remote connection (with username + password, also posting data) (results in pure XML) // none of these result in user-friendly - so no comfy, user-friendly stuff needed here //outputing statistics: debug only! log.info("HttpServletRequest INFO:"); log.info("ContentType: " + request.getContentType()); log.info("CharacterEncoding: " + request.getCharacterEncoding()); log.info("ContentLength: " + request.getContentLength()); log.info("checksum: " + request.getParameter("checksum")); log.info("syncData: " + request.getParameter("syncData")); log.info("syncDataResponse: " + request.getParameter("syncDataResponse")); long checksum = 0; Integer serverId = 0; boolean isResponse = false; boolean isUpload = false; boolean useCompression = false; String contents = ""; String username = ""; String password = ""; //file-based upload, and multi-part form submission if (request instanceof MultipartHttpServletRequest) { log.info("Processing contents of syncDataFile multipart request parameter"); MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request; serverId = ServletRequestUtils.getIntParameter(multipartRequest, "serverId", 0); isResponse = ServletRequestUtils.getBooleanParameter(multipartRequest, "isResponse", false); useCompression = ServletRequestUtils.getBooleanParameter(multipartRequest, "compressed", false); isUpload = ServletRequestUtils.getBooleanParameter(multipartRequest, "upload", false); username = ServletRequestUtils.getStringParameter(multipartRequest, "username", ""); password = ServletRequestUtils.getStringParameter(multipartRequest, "password", ""); log.info("Request class: " + request.getClass()); log.info("serverId: " + serverId); log.info("upload = " + isUpload); log.info("compressed = " + useCompression); log.info("response = " + isResponse); log.info("username = " + username); log.info("Request content length: " + request.getContentLength()); MultipartFile multipartFile = multipartRequest.getFile("syncDataFile"); if (multipartFile != null && !multipartFile.isEmpty()) { InputStream inputStream = null; try { // Decompress content in file ConnectionResponse syncResponse = new ConnectionResponse(new ByteArrayInputStream( multipartFile.getBytes()), useCompression); log.info("Content to decompress: " + multipartFile.getBytes()); log.info("Content received: " + syncResponse.getResponsePayload()); log.info("Decompression Checksum: " + syncResponse.getChecksum()); contents = syncResponse.getResponsePayload(); checksum = syncResponse.getChecksum(); log.info("Final content: " + contents); } catch (Exception e) { log.warn("Unable to read in sync data file", e); } finally { IOUtils.closeQuietly(inputStream); } } } else { log.debug("seems we DO NOT have a file object"); } // prepare to process the input: contents now contains decompressed request ready to be processed SyncTransmissionResponse str = new SyncTransmissionResponse(); str.setErrorMessage(SyncConstants.ERROR_TX_NOT_UNDERSTOOD); str.setFileName(SyncConstants.FILENAME_TX_NOT_UNDERSTOOD); str.setUuid(SyncConstants.UUID_UNKNOWN); str.setSyncSourceUuid(SyncConstants.UUID_UNKNOWN); str.setSyncTargetUuid(SyncConstants.UUID_UNKNOWN); str.setState(SyncTransmissionState.TRANSMISSION_NOT_UNDERSTOOD); str.setTimestamp(new Date()); //set the timestamp of the response if (log.isInfoEnabled()) { log.info("CONTENT IN IMPORT CONTROLLER: " + contents); } //if no content, nothing to process just send back response if (contents == null || contents.length() < 0) { log.info("returning from ingest: nothing to process."); this.sendResponse(str, isUpload, response); return null; } // if this is option 3 (posting from remote server), we need to authenticate if (!Context.isAuthenticated()) { try { Context.authenticate(username, password); } catch (Exception e) {} } // Could not authenticate user: send back error if (!Context.isAuthenticated()) { str.setErrorMessage(SyncConstants.ERROR_AUTH_FAILED); str.setFileName(SyncConstants.FILENAME_AUTH_FAILED); str.setState(SyncTransmissionState.AUTH_FAILED); this.sendResponse(str, isUpload, response); return null; } //Fill-in the server uuid for the response: since request was authenticated we can start letting callers //know about us str.setSyncTargetUuid(Context.getService(SyncService.class).getServerUuid()); //Checksum check before doing anything at all: on unreliable networks we can get seemingly //valid HTTP POST but content is messed up, defend against it with custom checksums long checksumReceived = ServletRequestUtils.getLongParameter(request, "checksum", -1); log.info("checksum value received in POST: " + checksumReceived); log.info("checksum value of payload: " + checksum); log.info("SIZE of payload: " + contents.length()); if (checksumReceived > 0 && (checksumReceived != checksum)) { log.error("ERROR: FAILED CHECKSUM!"); str.setState(SyncTransmissionState.TRANSMISSION_NOT_UNDERSTOOD); this.sendResponse(str, isUpload, response); return null; } //Test message. Test message was sent (i.e. using 'test connection' button on server screen) //just send empty acknowledgment if (SyncConstants.TEST_MESSAGE.equals(contents)) { str.setErrorMessage(""); str.setState(SyncTransmissionState.OK); str.setUuid(""); str.setFileName(SyncConstants.FILENAME_TEST); this.sendResponse(str, isUpload, response); return null; } if (SyncConstants.CLONE_MESSAGE.equals(contents)) { try { log.info("CLONE MESSAGE RECEIVED, TRYING TO CLONE THE DB"); File file = Context.getService(SyncService.class).generateDataFile(); StringWriter writer = new StringWriter(); IOUtils.copy(new FileInputStream(file), writer); this.sendCloneResponse(writer.toString(), response, false); boolean clonedDBLog = Boolean.parseBoolean(Context.getAdministrationService() .getGlobalProperty(SyncConstants.PROPERTY_SYNC_CLONED_DATABASE_LOG_ENABLED, "true")); if (!clonedDBLog){ file.delete(); } } catch (Exception ex) { log.warn(ex.toString()); ex.printStackTrace(); } return null; } /************************************************************************************************************************* * This is a real transmission: - user was properly authenticated - checksums match - it is * not a test transmission Start processing! 1. Deserialize what was sent; it can be either * SyncTransmssion, or SyncTransmissionResponse 2. If it is a response, *************************************************************************************************************************/ SyncTransmission st = null; if (!isResponse) { //this is not 'response' to something we sent out; thus the contents should contain plan SyncTransmission try { log.info("xml to sync transmission with contents: " + contents); st = SyncDeserializer.xmlToSyncTransmission(contents); } catch (Exception e) { log.error("Unable to deserialize the following: " + contents, e); str.setErrorMessage("Unable to deserialize transmission contents into SyncTansmission."); str.setState(SyncTransmissionState.TRANSMISSION_NOT_UNDERSTOOD); this.sendResponse(str, isUpload, response); return null; } } else { log.info("Processing a response, not a transmission"); SyncTransmissionResponse priorResponse = null; try { // this is the confirmation of receipt of previous transmission priorResponse = SyncDeserializer.xmlToSyncTransmissionResponse(contents); log.info("This is a response from a previous transmission. Uuid is: " + priorResponse.getUuid()); } catch (Exception e) { log.error("Unable to deserialize the following: " + contents, e); str.setErrorMessage("Unable to deserialize transmission contents into SyncTransmissionResponse."); str.setState(SyncTransmissionState.TRANSMISSION_NOT_UNDERSTOOD); this.sendResponse(str, isUpload, response); return null; } // figure out where this came from: // for responses, the target ID contains the server that generated the response String sourceUuid = priorResponse.getSyncTargetUuid(); log.info("SyncTransmissionResponse has a sourceUuid of " + sourceUuid); RemoteServer origin = Context.getService(SyncService.class).getRemoteServer(sourceUuid); if (origin == null) { log.error("Source server not registered locally. Unable to find source server by uuid: " + sourceUuid); str.setErrorMessage("Source server not registered locally. Unable to find source server by uuid " + sourceUuid); str.setState(SyncTransmissionState.INVALID_SERVER); this.sendResponse(str, isUpload, response); return null; } else { log.info("Found source server by uuid: " + sourceUuid + " = " + origin.getNickname()); log.info("Source server is " + origin.getNickname()); } if (priorResponse == null) {} // process response that was sent to us; the sync response normally contains: //a) results of the records that we sent out //b) new records from 'source' to be applied against this server if (priorResponse.getSyncImportRecords() == null) { log.debug("No records to process in response"); } else { // now process each incoming syncImportRecord, this is just status update for (SyncImportRecord importRecord : priorResponse.getSyncImportRecords()) { Context.getService(SyncIngestService.class).processSyncImportRecord(importRecord, origin); } } // now pull out the data that originated on the 'source' server and try to process it st = priorResponse.getSyncTransmission(); } // now process the syncTransmission if one was received if (st != null) { str = SyncUtilTransmission.processSyncTransmission(st, SyncUtil.getGlobalPropetyValueAsInteger(SyncConstants.PROPERTY_NAME_MAX_RECORDS_WEB)); } else log.info("st was null"); //send response this.sendResponse(str, isUpload, response); // never a situation where we want to actually use the model/view - either file download or http request return null; } /** * This is called prior to displaying a form for the first time. It tells Spring the * form/command object to load into the request * * @see org.springframework.web.servlet.mvc.AbstractFormController#formBackingObject(javax.servlet.http.HttpServletRequest) */ protected Object formBackingObject(HttpServletRequest request) throws ServletException { // default empty Object return ""; } private void sendResponse(SyncTransmissionResponse str, boolean isUpload, HttpServletResponse response) throws Exception { String content = null; try { str.createFile(false); content = str.getFileOutput(); } catch (Exception e) { log.error("Could not get output while writing file. In case problem writing file, trying again to just get output."); } if (content.length() == 0) { try { str.createFile(false); content = str.getFileOutput(); } catch (Exception e) { log.error("Could not get output while writing file. In case problem writing file, trying again to just get output."); } } // If the file was uploaded manually, we'll send back an XML response if (isUpload) { response.setHeader("Content-Disposition", "attachment; filename=" + str.getFileName() + ".xml"); InputStream in = new ByteArrayInputStream(content.getBytes()); IOUtils.copy(in, response.getOutputStream()); return; } // We're sending back a new sync transmission (an update). // We need to check the local server about whether we should apply compression. boolean useCompression = Boolean.parseBoolean(Context.getAdministrationService().getGlobalProperty( SyncConstants.PROPERTY_ENABLE_COMPRESSION, "true")); log.debug("Global property sychronization.enable_compression = " + useCompression); // Otherwise, all other requests are compressed and sent back to the client ConnectionRequest syncRequest = new ConnectionRequest(content, useCompression); log.info("Compressed content length: " + syncRequest.getContentLength()); log.info("Compression Checksum: " + syncRequest.getChecksum()); log.info("Full Content to send: " + content); response.setContentLength((int) syncRequest.getContentLength()); response.addHeader("Enable-Compression", String.valueOf(useCompression)); response.addHeader("Content-Checksum", String.valueOf(syncRequest.getChecksum())); response.addHeader("Content-Encoding", "gzip"); // Write compressed sync data to response InputStream in = new ByteArrayInputStream(syncRequest.getBytes()); IOUtils.copy(in, response.getOutputStream()); return; } private void sendCloneResponse(String content, HttpServletResponse response, boolean isUpload) throws Exception { boolean useCompression = Boolean.parseBoolean(Context.getAdministrationService().getGlobalProperty( SyncConstants.PROPERTY_ENABLE_COMPRESSION, "true")); log.debug("Global property sychronization.enable_compression = " + useCompression); // Otherwise, all other requests are compressed and sent back to the // client ConnectionRequest syncRequest = new ConnectionRequest(content, useCompression); log.info("Compressed content length: " + syncRequest.getContentLength()); log.info("Compression Checksum: " + syncRequest.getChecksum()); response.setContentLength((int) syncRequest.getContentLength()); response.addHeader("Enable-Compression", String.valueOf(useCompression)); response.addHeader("Content-Checksum", String.valueOf(syncRequest.getChecksum())); response.addHeader("Content-Encoding", "gzip"); // Write compressed sync data to response InputStream in = new ByteArrayInputStream(syncRequest.getBytes()); IOUtils.copy(in, response.getOutputStream()); return; } }