diff --git a/.travis.yml b/.travis.yml index bb594944c..96aa89ee2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,5 @@ language: java +dist: trusty jdk: - oraclejdk8 @@ -20,13 +21,11 @@ script: - sudo chmod +x ./run_build.sh - ./run_build.sh +before_cache: + - rm -r ~/.m2/repository/org/motechproject/* + cache: directories: - $HOME/.m2 + - $HOME/.motech/bundles -notifications: - email: - recipients: - - travis-test-maniek@googlegroups.com - on_success: never - on_failure: always \ No newline at end of file diff --git a/atom-client/src/main/java/org/motechproject/atomclient/domain/FeedRecord.java b/atom-client/src/main/java/org/motechproject/atomclient/domain/FeedRecord.java index caa889dca..f45c48059 100644 --- a/atom-client/src/main/java/org/motechproject/atomclient/domain/FeedRecord.java +++ b/atom-client/src/main/java/org/motechproject/atomclient/domain/FeedRecord.java @@ -42,4 +42,11 @@ public String getUrl() { public String getData() { return data; } + + public Integer getPage(String data) { + int beginIndex = data.indexOf("via"); + beginIndex = data.indexOf("patient", beginIndex) + "patient/".length(); + int endIndex = data.indexOf('/', beginIndex) - "\" ".length(); + return Integer.parseInt(data.substring(beginIndex, endIndex)); + } } diff --git a/atom-client/src/main/java/org/motechproject/atomclient/service/AtomClientConfigService.java b/atom-client/src/main/java/org/motechproject/atomclient/service/AtomClientConfigService.java index 9c824f638..137bf26f1 100644 --- a/atom-client/src/main/java/org/motechproject/atomclient/service/AtomClientConfigService.java +++ b/atom-client/src/main/java/org/motechproject/atomclient/service/AtomClientConfigService.java @@ -52,4 +52,13 @@ public interface AtomClientConfigService { * @return a regex or an empty string */ String getRegexForFeedUrl(String url); + + /** + * Sets the module's feed configurations, based on pages that should be consumed. + * + * @param currentPage last page that is already loaded to module + * @param recentPage recent available page that can be consumed + * @param feedUrl the feed URL + */ + void readNewFeeds(int currentPage, int recentPage, String feedUrl); } diff --git a/atom-client/src/main/java/org/motechproject/atomclient/service/AtomClientService.java b/atom-client/src/main/java/org/motechproject/atomclient/service/AtomClientService.java index 558ea95dc..51af981bd 100644 --- a/atom-client/src/main/java/org/motechproject/atomclient/service/AtomClientService.java +++ b/atom-client/src/main/java/org/motechproject/atomclient/service/AtomClientService.java @@ -22,4 +22,11 @@ public interface AtomClientService { * changed since the last time the feed was fetched. */ void fetch(); + + /** + * Reads the atom feed(s) specified in task action. + * @param currentUrl last url that is already fetched + * @param lastUrl last known url that can be fetched + */ + void read(String currentUrl, String lastUrl); } diff --git a/atom-client/src/main/java/org/motechproject/atomclient/service/Constants.java b/atom-client/src/main/java/org/motechproject/atomclient/service/Constants.java index d2cabb8b4..8a4233838 100644 --- a/atom-client/src/main/java/org/motechproject/atomclient/service/Constants.java +++ b/atom-client/src/main/java/org/motechproject/atomclient/service/Constants.java @@ -7,6 +7,7 @@ public final class Constants { public static final String FETCH_CRON_PROPERTY = "atomclient.feed.cron"; public static final String BASE_ATOMCLIENT_SUBJECT = "org.motechproject.atomclient"; public static final String FETCH_MESSAGE = BASE_ATOMCLIENT_SUBJECT + ".fetch"; + public static final String READ_MESSAGE = BASE_ATOMCLIENT_SUBJECT + ".read"; public static final String FEED_CHANGE_MESSAGE = BASE_ATOMCLIENT_SUBJECT + ".feedchange"; public static final String RESCHEDULE_FETCH_JOB = BASE_ATOMCLIENT_SUBJECT + ".reschedulefetchjob"; } diff --git a/atom-client/src/main/java/org/motechproject/atomclient/service/impl/AtomClientConfigServiceImpl.java b/atom-client/src/main/java/org/motechproject/atomclient/service/impl/AtomClientConfigServiceImpl.java index 11b572626..16604a7ea 100644 --- a/atom-client/src/main/java/org/motechproject/atomclient/service/impl/AtomClientConfigServiceImpl.java +++ b/atom-client/src/main/java/org/motechproject/atomclient/service/impl/AtomClientConfigServiceImpl.java @@ -8,9 +8,9 @@ import org.motechproject.atomclient.service.Constants; import org.motechproject.atomclient.service.FeedConfig; import org.motechproject.atomclient.service.FeedConfigs; +import org.motechproject.config.SettingsFacade; import org.motechproject.event.MotechEvent; import org.motechproject.event.listener.EventRelay; -import org.motechproject.config.SettingsFacade; import org.quartz.CronExpression; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -22,6 +22,9 @@ import java.io.IOException; import java.io.InputStream; import java.text.ParseException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; @Service("atomClientConfigService") @@ -134,4 +137,13 @@ public String getRegexForFeedUrl(String url) { } return ""; } + + public void readNewFeeds (int currentPage, int recentPage, String feedUrl) { + List newFeeds = new ArrayList<>(); + String url = feedUrl.substring(0, feedUrl.lastIndexOf('/') + 1); + for (int i = currentPage; i <= recentPage; i++) { + newFeeds.add(new FeedConfig(url + Integer.toString(i), "/([0-9a-f-]*)\\?")); + } + this.setFeedConfigs(new FeedConfigs(new HashSet<>(newFeeds))); + } } diff --git a/atom-client/src/main/java/org/motechproject/atomclient/service/impl/AtomClientEventHandler.java b/atom-client/src/main/java/org/motechproject/atomclient/service/impl/AtomClientEventHandler.java index 4b248a62b..cc8a714ca 100644 --- a/atom-client/src/main/java/org/motechproject/atomclient/service/impl/AtomClientEventHandler.java +++ b/atom-client/src/main/java/org/motechproject/atomclient/service/impl/AtomClientEventHandler.java @@ -12,6 +12,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import java.util.Map; + /** * Handles: * - (raw or text) config changes by triggering the reload of the appropriate configuration items @@ -22,10 +24,11 @@ public class AtomClientEventHandler { private static final Logger LOGGER = LoggerFactory.getLogger(AtomClientEventHandler.class); + private static final String CURRENT_URL = "currentFeedUrl"; + private static final String LAST_URL = "lastFeedUrl"; private AtomClientConfigService atomClientConfigService; private AtomClientService atomClientService; - @Autowired public void setAtomClientConfigService(AtomClientConfigService atomClientConfigService) { this.atomClientConfigService = atomClientConfigService; @@ -72,4 +75,15 @@ public void handleFetch(MotechEvent event) { atomClientService.fetch(); } + + @MotechListener(subjects = { Constants.READ_MESSAGE }) + public void handleRead(MotechEvent event) { + LOGGER.trace("handleRead {}", event); + Map params = event.getParameters(); + + String currentUrl = (String) params.get(CURRENT_URL); + String lastUrl = (String) params.get(LAST_URL); + + atomClientService.read(currentUrl, lastUrl); + } } diff --git a/atom-client/src/main/java/org/motechproject/atomclient/service/impl/AtomClientServiceImpl.java b/atom-client/src/main/java/org/motechproject/atomclient/service/impl/AtomClientServiceImpl.java index e7e927218..045c54b9b 100644 --- a/atom-client/src/main/java/org/motechproject/atomclient/service/impl/AtomClientServiceImpl.java +++ b/atom-client/src/main/java/org/motechproject/atomclient/service/impl/AtomClientServiceImpl.java @@ -4,11 +4,13 @@ import com.rometools.fetcher.FetcherException; import com.rometools.fetcher.impl.HttpURLFeedFetcher; import com.rometools.rome.io.FeedException; +import org.motechproject.atomclient.domain.FeedRecord; import org.motechproject.atomclient.repository.FeedRecordDataService; import org.motechproject.atomclient.service.AtomClientConfigService; import org.motechproject.atomclient.service.AtomClientService; import org.motechproject.atomclient.service.Constants; import org.motechproject.atomclient.service.FeedConfig; +import org.motechproject.atomclient.service.FeedConfigs; import org.motechproject.event.MotechEvent; import org.motechproject.event.listener.EventRelay; import org.motechproject.scheduler.contract.CronJobId; @@ -24,6 +26,9 @@ import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; @Service("atomClientService") @@ -34,6 +39,7 @@ public class AtomClientServiceImpl implements AtomClientService { private AtomClientConfigService configService; private MotechSchedulerService motechSchedulerService; private FeedFetcher feedFetcher; + private FeedRecordDataService feedRecordDataService; @Autowired @@ -42,6 +48,7 @@ public AtomClientServiceImpl(FeedRecordDataService feedRecordDataService, EventR feedFetcher = new HttpURLFeedFetcher(new FeedCache(feedRecordDataService, eventRelay, configService)); this.configService = configService; this.motechSchedulerService = motechSchedulerService; + this.feedRecordDataService = feedRecordDataService; } @@ -65,6 +72,29 @@ public void scheduleFetchJob(String cronExpression) { } + @Override + @Transactional + public void read(String currentUrl, String lastUrl) { + List feeds = new ArrayList<>(); + String regex = "/([0-9a-f-]*)\\?"; + + feeds.add(new FeedConfig(currentUrl, regex)); + feeds.add(new FeedConfig(lastUrl, regex)); + configService.setFeedConfigs(new FeedConfigs(new HashSet<>(feeds))); + fetch(); + + FeedRecord feedRecord = feedRecordDataService.findByURL(currentUrl); + String currenData = feedRecord.getData(); + int currentPage = feedRecord.getPage(currenData); + + feedRecord = feedRecordDataService.findByURL(lastUrl); + String lastData = feedRecord.getData(); + int lastPage = feedRecord.getPage(lastData); + + configService.readNewFeeds(currentPage, lastPage, currentUrl); + fetch(); + } + @Override @Transactional public void fetch() { diff --git a/atom-client/src/main/resources/task-channel.json b/atom-client/src/main/resources/task-channel.json index f6897444e..f5bb50911 100644 --- a/atom-client/src/main/resources/task-channel.json +++ b/atom-client/src/main/resources/task-channel.json @@ -35,6 +35,20 @@ "subject" : "org.motechproject.atomclient.fetch", "displayName" : "atomclient.task.feed.fetch", "actionParameters" : [ ] + }, + { + "subject" : "org.motechproject.atomclient.read", + "displayName" : "atomclient.task.feed.OpenMRS.read", + "actionParameters" : [ + { + "key" : "currentFeedUrl", + "displayName" : "atomclient.task.feed.OpenMRS.currentUrl" + }, + { + "key" : "lastFeedUrl", + "displayName" : "atomclient.task.feed.OpenMRS.lastUrl" + } + ] } ] } diff --git a/atom-client/src/main/resources/webapp/messages/messages.properties b/atom-client/src/main/resources/webapp/messages/messages.properties index 1218f015c..c844d7cfe 100644 --- a/atom-client/src/main/resources/webapp/messages/messages.properties +++ b/atom-client/src/main/resources/webapp/messages/messages.properties @@ -10,3 +10,6 @@ atomclient.task.feed.change.updated_date=Updated Date atomclient.task.feed.change.raw_content=Raw Content atomclient.task.feed.change.extracted_content=Extracted Content atomclient.task.feed.fetch=Fetch! +atomclient.task.feed.OpenMRS.read = Read OpenMRS Feeds +atomclient.task.feed.OpenMRS.currentUrl= Current OpenMRS Feed Url +atomclient.task.feed.OpenMRS.lastUrl= Recent OpenMRS Feed Url diff --git a/atom-client/src/test/java/org/motechproject/atomclient/it/AtomClientServiceBundleIT.java b/atom-client/src/test/java/org/motechproject/atomclient/it/AtomClientServiceBundleIT.java index 20471e0da..529535e28 100644 --- a/atom-client/src/test/java/org/motechproject/atomclient/it/AtomClientServiceBundleIT.java +++ b/atom-client/src/test/java/org/motechproject/atomclient/it/AtomClientServiceBundleIT.java @@ -1,5 +1,8 @@ package org.motechproject.atomclient.it; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; import org.apache.http.HttpStatus; import org.junit.Before; import org.junit.Test; @@ -19,6 +22,9 @@ import org.ops4j.pax.exam.spi.reactors.PerSuite; import javax.inject.Inject; +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; import java.util.Arrays; import java.util.HashSet; @@ -172,4 +178,190 @@ public void verifySuccessiveFetches() { String data3 = feedRecord.getData(); assertThat(data3, is(not(data2))); } -} + + @Test + public void verifyPagination() { + + final String atomResponse = + "\r\n" + + "\r\n" + + " Patient AOP\r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + " OpenMRS\r\n" + + " \r\n" + + " bec795b1-3d17-451d-b43e-a094019f6984+37\r\n" + + " 2016-03-03T23:51:45Z\r\n" + + " 2016-03-03T23:51:45Z\r\n" + + " \r\n" + + " Patient\r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + " tag:atomfeed.ict4h.org:72deaf8e-039d-49ec-b041-4da9a35c95a2\r\n" + + " 2016-03-03T23:51:45Z\r\n" + + " 2016-03-03T23:51:45Z\r\n" + + " \r\n" + + " 2016-03-03T23:51:45Z\r\n" + + " \r\n" + + "\r\n"; + + try + { + HttpServer server = HttpServer.create(new InetSocketAddress("localhost", 8081), 0); + server.createContext("/openmrs/ws/atomfeed/patient/36", new atom1()); + server.createContext("/openmrs/ws/atomfeed/patient/37", new atom2()); + server.createContext("/openmrs/ws/atomfeed/patient/38", new atom3()); + server.setExecutor(null); // creates a default executor + server.start(); + + String currentUrl = "http:/" + server.getAddress().toString() + "/openmrs/ws/atomfeed/patient/36"; + String recentUrl = "http:/" + server.getAddress().toString() + "/openmrs/ws/atomfeed/patient/38"; + atomClientService.read(currentUrl, recentUrl); + + String fetchedUrl = "http:/" + server.getAddress().toString() + "/openmrs/ws/atomfeed/patient/37"; + FeedRecord feedRecord = feedRecordDataService.findByURL(fetchedUrl); + assertNotNull(feedRecord); + String data = feedRecord.getData(); + assertThat(data, is(atomResponse)); + server.stop(0); + } catch (IOException ex) { + ex.getCause(); + } + } + + static class atom1 implements HttpHandler { + @Override + public void handle(HttpExchange t) throws IOException { + final String atomResponse1 = + "\n" + + "\n" + + " Patient AOP\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " OpenMRS\n" + + " \n" + + " bec795b1-3d17-451d-b43e-a094019f6984+36\n" + + " OpenMRS Feed Publisher\n" + + " 2016-03-03T23:46:14Z\n" + + " \n" + + " Patient\n" + + " \n" + + " tag:atomfeed.ict4h.org:fbd3d76b-a23e-44d7-8df6-c0da95ab34ad\n" + + " 2016-03-03T23:39:59Z\n" + + " 2016-03-03T23:39:59Z\n" + + " \n" + + " \n" + + " \n" + + " Patient\n" + + " \n" + + " tag:atomfeed.ict4h.org:e321ea5d-14d6-41ee-8727-9e5291bbcbb6\n" + + " 2016-03-03T23:41:20Z\n" + + " 2016-03-03T23:41:20Z\n" + + " \n" + + " \n" + + " \n" + + " Patient\n" + + " \n" + + " tag:atomfeed.ict4h.org:91b6e147-39eb-4dd1-a983-ec95fc99a9fb\n" + + " 2016-03-03T23:42:34Z\n" + + " 2016-03-03T23:42:34Z\n" + + " \n" + + " \n" + + " \n" + + " Patient\n" + + " \n" + + " tag:atomfeed.ict4h.org:76843fee-721d-472c-a849-25c292e3d3b5\n" + + " 2016-03-03T23:43:57Z\n" + + " 2016-03-03T23:43:57Z\n" + + " \n" + + " \n" + + " \n" + + " Patient\n" + + " \n" + + " tag:atomfeed.ict4h.org:e12ca14d-7fdb-44cf-9efc-f8298df6ce0b\n" + + " 2016-03-03T23:46:14Z\n" + + " 2016-03-03T23:46:14Z\n" + + " \n" + + " \n" + + "\n"; + + t.sendResponseHeaders(200, atomResponse1.length()); + OutputStream os = t.getResponseBody(); + os.write(atomResponse1.getBytes()); + os.close(); + } + } + + static class atom2 implements HttpHandler { + @Override + public void handle(HttpExchange t) throws IOException { + final String atomResponse2 = + "\n" + + "\n" + + " Patient AOP\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " OpenMRS\n" + + " \n" + + " bec795b1-3d17-451d-b43e-a094019f6984+37\n" + + " OpenMRS Feed Publisher\n" + + " 2016-03-03T23:51:45Z\n" + + " \n" + + " Patient\n" + + " \n" + + " tag:atomfeed.ict4h.org:72deaf8e-039d-49ec-b041-4da9a35c95a2\n" + + " 2016-03-03T23:51:45Z\n" + + " 2016-03-03T23:51:45Z\n" + + " \n" + + " \n" + + "\n"; + + t.sendResponseHeaders(200, atomResponse2.length()); + OutputStream os = t.getResponseBody(); + os.write(atomResponse2.getBytes()); + os.close(); + } + } + + static class atom3 implements HttpHandler { + @Override + public void handle(HttpExchange t) throws IOException { + final String atomResponse3 = + "\n" + + "\n" + + " Patient AOP\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " OpenMRS\n" + + " \n" + + " bec795b1-3d17-451d-b43e-a094019f6984+37\n" + + " OpenMRS Feed Publisher\n" + + " 2016-03-03T23:51:45Z\n" + + " \n" + + " Patient\n" + + " \n" + + " tag:atomfeed.ict4h.org:72deaf8e-039d-49ec-b041-4da9a35c95a2\n" + + " 2016-03-03T23:51:45Z\n" + + " 2016-03-03T23:51:45Z\n" + + " \n" + + " \n" + + "\n"; + + t.sendResponseHeaders(200, atomResponse3.length()); + OutputStream os = t.getResponseBody(); + os.write(atomResponse3.getBytes()); + os.close(); + } + } + +} \ No newline at end of file diff --git a/atom-client/src/test/java/org/motechproject/atomclient/unit/EventTest.java b/atom-client/src/test/java/org/motechproject/atomclient/unit/EventTest.java index ef5bd8da4..b2b332a85 100644 --- a/atom-client/src/test/java/org/motechproject/atomclient/unit/EventTest.java +++ b/atom-client/src/test/java/org/motechproject/atomclient/unit/EventTest.java @@ -65,6 +65,28 @@ public class EventTest { " \n" + "\n"; + static final String ATOM_FEED_DATA_2 = "\n" + + "\n" + + " Patient AOP\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " OpenMRS\n" + + " \n" + + " bec795b1-3d17-451d-b43e-a094019f6984+35\n" + + " OpenMRS Feed Publisher\n" + + " 2016-02-23T03:31:13Z\n" + + " \n" + + " Patient\n" + + " \n" + + " tag:atomfeed.ict4h.org:9cc95d86-1b8e-4582-a774-83cf4b73c23e\n" + + " 2016-02-23T03:31:13Z\n" + + " 2016-02-23T03:31:13Z\n" + + " \n" + + " \n" + + "\n"; + @Before public void setup() { @@ -108,4 +130,17 @@ public void verifyEventNotSentForExistingData() { verify(eventRelay, times(0)).sendEventMessage(event.capture()); } + + @Test + public void verifyRead() { + String feedURL2 = SimpleHttpServer.getInstance().start("foo2", HttpStatus.SC_OK, ATOM_FEED_DATA_2); + + when(feedRecordDataService.findByURL(feedURL)).thenReturn(new FeedRecord(feedURL, ATOM_FEED_DATA)); + when(feedRecordDataService.findByURL(feedURL2)).thenReturn(new FeedRecord(feedURL2, ATOM_FEED_DATA_2)); + + atomClientService.read(feedURL, feedURL2); + + verify(atomClientConfigService).readNewFeeds(35, 36, feedURL); + } + } diff --git a/commcare/pom.xml b/commcare/pom.xml index 001a2b8aa..be6a193ae 100644 --- a/commcare/pom.xml +++ b/commcare/pom.xml @@ -208,7 +208,7 @@ org.motechproject.commcare.builder;version=${project.version}, org.motechproject.commcare.config;version=${project.version}, - org.motechproject.commcare.domain;version=${project.version}, + org.motechproject.commcare.domain.*;version=${project.version}, org.motechproject.commcare.events;version=${project.version}, org.motechproject.commcare.events.constants;version=${project.version}, org.motechproject.commcare.exception;version=${project.version}, diff --git a/commcare/src/main/java/org/motechproject/commcare/client/CommCareAPIHttpClient.java b/commcare/src/main/java/org/motechproject/commcare/client/CommCareAPIHttpClient.java index 1f11959cf..ae9c40bb5 100644 --- a/commcare/src/main/java/org/motechproject/commcare/client/CommCareAPIHttpClient.java +++ b/commcare/src/main/java/org/motechproject/commcare/client/CommCareAPIHttpClient.java @@ -142,14 +142,39 @@ public String formRequest(AccountConfig accountConfig, String formId) { /** * Executes a HTTP get request to the form list API endpoint. * - * @param accountConfig the account configuration to use - * @param formListRequest the request that will be used for creating the HTTP request + * @param accountConfig the account configuration to use + * @param formListRequest the request that will be used for creating the HTTP request * @return the response as a String (JSON expected) */ public String formListRequest(AccountConfig accountConfig, FormListRequest formListRequest) { return this.getRequest(accountConfig, commcareFormListUrl(accountConfig, formListRequest), null); } + /** + * Executes a HTTP get request to the report data API endpoint. + * + * @param accountConfig the account configuration to use + * @param reportId the ID of the report + * @return the response as a String (JSON expected) + */ + public String singleReportDataRequest(AccountConfig accountConfig, String reportId){ + return this.getRequest(accountConfig, commcareReportDataUrl(accountConfig, reportId), null); + } + + public String singleReportDataRequestWithFilters(AccountConfig accountConfig, String reportId, String filters) { + return this.getRequest(accountConfig, commcareReportDataUrlWithFilters(accountConfig, reportId, filters), null); + } + + /** + * Executes a HTTP get request to the reports list API endpoint. + * + * @param accountConfig the account configuration to use + * @return the response as a String (JSON expected) + */ + public String reportsListMetadataRequest(AccountConfig accountConfig) { + return this.getRequest(accountConfig, commcareReportsMetadataUrl(accountConfig), null); + } + /** * Retrieves a list of the cases from the CommCare server. The given {@code caseRequest} will be used for fetching * data from the server. @@ -159,7 +184,7 @@ public String formListRequest(AccountConfig accountConfig, FormListRequest formL * @return the JSON string representation of the cases */ public String casesRequest(AccountConfig accountConfig, CaseRequest caseRequest) { - return this.getRequest(accountConfig, commcareCasesUrl(accountConfig), caseRequest); + return this.getRequest(accountConfig, commcareCasesUrl(accountConfig, caseRequest), caseRequest); } /** @@ -348,7 +373,7 @@ private HttpMethod buildRequest(AccountConfig accountConfig, String url, Request HttpMethod requestMethod = new GetMethod(url); authenticate(accountConfig); - if (request != null) { + if (request != null && requestMethod.getQueryString() == null) { requestMethod.setQueryString(request.toQueryString()); } @@ -510,6 +535,21 @@ String commcareFixturesUrl(AccountConfig accountConfig, Integer pageSize, Intege buildPaginationParams(pageSize, pageNumber)); } + String commcareCasesUrl(AccountConfig accountConfig, CaseRequest caseRequest) { + try { + URIBuilder uriBuilder = new URIBuilder(String.format("%s/%s/api/v%s/case/", getCommcareBaseUrl(accountConfig.getBaseUrl()), + accountConfig.getDomain(), API_VERSION)); + + if (caseRequest != null) { + caseRequest.addQueryParams(uriBuilder); + } + + return uriBuilder.build().toString(); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Unable to build form list url", e); + } + } + String commcareCasesUrl(AccountConfig accountConfig) { return String.format("%s/%s/api/v%s/case/", getCommcareBaseUrl(accountConfig.getBaseUrl()), accountConfig.getDomain(), API_VERSION); @@ -524,6 +564,20 @@ String commcareCaseUrl(AccountConfig accountConfig, String caseId) { accountConfig.getDomain(), API_VERSION, caseId); } + String commcareReportDataUrl(AccountConfig accountConfig, String reportId) { + return String.format("%s/%s/api/v%s/configurablereportdata/%s/?format=json", + getCommcareBaseUrl(accountConfig.getBaseUrl()), accountConfig.getDomain(), API_VERSION, reportId); + } + + String commcareReportDataUrlWithFilters(AccountConfig accountConfig, String reportId, String filter) { + return commcareReportDataUrl(accountConfig, reportId).concat(filter); + } + + String commcareReportsMetadataUrl(AccountConfig accountConfig) { + return String.format("%s/%s/api/v%s/simplereportconfiguration/?format=json", getCommcareBaseUrl(accountConfig.getBaseUrl()), + accountConfig.getDomain(), API_VERSION); + } + String commcareStockTransactionsUrl(AccountConfig accountConfig) { return String.format("%s/%s/api/v%s/stock_transaction/?format=json", getCommcareBaseUrl(accountConfig.getBaseUrl()), accountConfig.getDomain(), API_VERSION); diff --git a/commcare/src/main/java/org/motechproject/commcare/config/manager/ConfigurationManager.java b/commcare/src/main/java/org/motechproject/commcare/config/manager/ConfigurationManager.java index b4d752ef6..ba4db685c 100644 --- a/commcare/src/main/java/org/motechproject/commcare/config/manager/ConfigurationManager.java +++ b/commcare/src/main/java/org/motechproject/commcare/config/manager/ConfigurationManager.java @@ -1,8 +1,11 @@ package org.motechproject.commcare.config.manager; import org.motechproject.commcare.domain.CommcareApplicationJson; +import org.motechproject.commcare.domain.report.ReportsMetadataInfo; import org.motechproject.commcare.service.CommcareAppStructureService; import org.motechproject.commcare.service.CommcareApplicationDataService; +import org.motechproject.commcare.service.CommcareReportService; +import org.motechproject.commcare.service.ReportsMetadataDataService; import org.motechproject.commcare.tasks.CommcareTasksNotifier; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -25,9 +28,15 @@ public class ConfigurationManager { @Autowired private CommcareAppStructureService appStructureService; + @Autowired + private CommcareReportService reportService; + @Autowired private CommcareApplicationDataService commcareApplicationDataService; + @Autowired + private ReportsMetadataDataService reportsMetadataDataService; + @Autowired private CommcareTasksNotifier commcareTasksNotifier; @@ -43,8 +52,8 @@ public void configUpdated(String configName) { } @Transactional - public void configUpdated(String configName, boolean downloadApplications) { - if (downloadApplications) { + public void configUpdated(String configName, boolean downloadApplicationsAndReports) { + if (downloadApplicationsAndReports) { LOGGER.info("Configuration [{}] updated, fetching Commcare schema, {}", configName); reloadConfig(configName); } else { @@ -62,15 +71,20 @@ public synchronized void configDeleted(String configName) { private synchronized void reloadConfig(String configName) { List applications = appStructureService.getAllApplications(configName); + ReportsMetadataInfo reportsMetadata = reportService.getReportsList(configName); // clear the configuration before saving the new one clearApps(configName); + clearReports(configName); for (CommcareApplicationJson app : applications) { app.setConfigName(configName); commcareApplicationDataService.create(app); } + reportsMetadata.setConfigName(configName); + reportsMetadataDataService.create(reportsMetadata); + commcareTasksNotifier.updateTasksInfo(); } @@ -79,4 +93,10 @@ private void clearApps(String configName) { commcareApplicationDataService.delete(application); } } + + private void clearReports(String configName) { + for (ReportsMetadataInfo report : reportsMetadataDataService.bySourceConfiguration(configName)) { + reportsMetadataDataService.delete(report); + } + } } diff --git a/commcare/src/main/java/org/motechproject/commcare/domain/CaseInfo.java b/commcare/src/main/java/org/motechproject/commcare/domain/CaseInfo.java index ddae4e3d3..a26b23a77 100644 --- a/commcare/src/main/java/org/motechproject/commcare/domain/CaseInfo.java +++ b/commcare/src/main/java/org/motechproject/commcare/domain/CaseInfo.java @@ -12,6 +12,7 @@ public class CaseInfo { private String caseId; private String userId; private String dateClosed; + private String dateModified; private String domain; private List xformIds; private String version; @@ -50,6 +51,14 @@ public void setDateClosed(String dateClosed) { this.dateClosed = dateClosed; } + public String getDateModified() { + return dateModified; + } + + public void setDateModified(String dateModified) { + this.dateModified = dateModified; + } + public String getDomain() { return this.domain; } diff --git a/commcare/src/main/java/org/motechproject/commcare/domain/CommcareForm.java b/commcare/src/main/java/org/motechproject/commcare/domain/CommcareForm.java index 0f965cd1a..b5c02508b 100644 --- a/commcare/src/main/java/org/motechproject/commcare/domain/CommcareForm.java +++ b/commcare/src/main/java/org/motechproject/commcare/domain/CommcareForm.java @@ -26,6 +26,9 @@ public class CommcareForm { @SerializedName("resource_uri") private String resourceUri; + @SerializedName("app_id") + private String appId; + private String type; private String uiversion; @@ -84,6 +87,14 @@ public void setResourceUri(String resourceUri) { this.resourceUri = resourceUri; } + public String getAppId() { + return appId; + } + + public void setAppId(String appId) { + this.appId = appId; + } + public String getType() { return type; } diff --git a/commcare/src/main/java/org/motechproject/commcare/domain/CommcareMetadataInfo.java b/commcare/src/main/java/org/motechproject/commcare/domain/CommcareMetadataInfo.java index 0fe414045..1e83513f1 100644 --- a/commcare/src/main/java/org/motechproject/commcare/domain/CommcareMetadataInfo.java +++ b/commcare/src/main/java/org/motechproject/commcare/domain/CommcareMetadataInfo.java @@ -1,16 +1,48 @@ package org.motechproject.commcare.domain; +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +import java.io.Serializable; +import java.util.Objects; + /** * Domain class representing case metadata, retrieved from CommCareHQ server. */ -public class CommcareMetadataInfo { +public class CommcareMetadataInfo implements Serializable { + + private static final long serialVersionUID = -3427706967092034501L; + @Expose + @SerializedName("limit") private int limit; + + @Expose + @SerializedName("next") private String nextPageQueryString; + + @Expose + @SerializedName("offset") private int offset; + + @Expose + @SerializedName("previous") private String previousPageQueryString; + + @Expose + @SerializedName("total_count") private int totalCount; + public CommcareMetadataInfo() { } + + public CommcareMetadataInfo(int limit, String nextPageQueryString, int offset, String previousPageQueryString, int totalCount) { + this.limit = limit; + this.nextPageQueryString = nextPageQueryString; + this.offset = offset; + this.previousPageQueryString = previousPageQueryString; + this.totalCount = totalCount; + } + public String getPreviousPageQueryString() { return previousPageQueryString; } @@ -50,4 +82,26 @@ public void setNextPageQueryString(String nextPageQueryString) { public void setPreviousPageQueryString(String previousPageQueryString) { this.previousPageQueryString = previousPageQueryString; } + + @Override + public int hashCode() { + return Objects.hash(limit, nextPageQueryString, offset, previousPageQueryString, totalCount); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (!(o instanceof CommcareMetadataInfo)) { + return false; + } + + CommcareMetadataInfo other = (CommcareMetadataInfo) o; + + return Objects.equals(limit, other.limit) && Objects.equals(nextPageQueryString, other.nextPageQueryString) + && Objects.equals(offset, other.offset) && Objects.equals(previousPageQueryString, other.previousPageQueryString) + && Objects.equals(totalCount, other.totalCount); + } } diff --git a/commcare/src/main/java/org/motechproject/commcare/domain/report/ReportDataColumn.java b/commcare/src/main/java/org/motechproject/commcare/domain/report/ReportDataColumn.java new file mode 100644 index 000000000..4a33541e6 --- /dev/null +++ b/commcare/src/main/java/org/motechproject/commcare/domain/report/ReportDataColumn.java @@ -0,0 +1,73 @@ +package org.motechproject.commcare.domain.report; + +import com.google.gson.annotations.SerializedName; + +import java.util.Objects; + +/** + * Represents a single column report data. + */ +public class ReportDataColumn { + + @SerializedName("header") + private String header; + + @SerializedName("slug") + private String slug; + + @SerializedName("expand_column_value") + private String expandColumnValue; + + public ReportDataColumn() { } + + public ReportDataColumn(String header, String slug, String expandColumnValue) { + this.header = header; + this.slug = slug; + this.expandColumnValue = expandColumnValue; + } + + public String getHeader() { + return header; + } + + public String getSlug() { + return slug; + } + + public String getExpandColumnValue() { + return expandColumnValue; + } + + public void setHeader(String header) { + this.header = header; + } + + public void setSlug(String slug) { + this.slug = slug; + } + + public void setExpandColumnValue(String expandColumnValue) { + this.expandColumnValue = expandColumnValue; + } + + @Override + public int hashCode() { + return Objects.hash(header, slug, expandColumnValue); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (!(o instanceof ReportDataColumn)) { + return false; + } + + ReportDataColumn other = (ReportDataColumn) o; + + return Objects.equals(header, other.header) && Objects.equals(slug, other.slug) + && Objects.equals(expandColumnValue, other.expandColumnValue); + } +} diff --git a/commcare/src/main/java/org/motechproject/commcare/domain/report/ReportDataInfo.java b/commcare/src/main/java/org/motechproject/commcare/domain/report/ReportDataInfo.java new file mode 100644 index 000000000..b5cbee16d --- /dev/null +++ b/commcare/src/main/java/org/motechproject/commcare/domain/report/ReportDataInfo.java @@ -0,0 +1,79 @@ +package org.motechproject.commcare.domain.report; + +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * Represents a single CommCareHQ report data. It's a part of the MOTECH model. + */ +public class ReportDataInfo { + + private List columns; + private List> data; + private String nextPage; + private Integer totalRecords; + + public ReportDataInfo() { } + + public ReportDataInfo(List columns, List> data, String nextPage, Integer totalRecords) { + this.columns = columns; + this.data = data; + this.nextPage = nextPage; + this.totalRecords = totalRecords; + } + + public List getColumns() { + return columns; + } + + public void setColumns(List columns) { + this.columns = columns; + } + + public List> getData() { + return data; + } + + public void setData(List> data) { + this.data = data; + } + + public String getNextPage() { + return nextPage; + } + + public void setNextPage(String nextPage) { + this.nextPage = nextPage; + } + + public Integer getTotalRecords() { + return totalRecords; + } + + public void setTotalRecords(Integer totalRecords) { + this.totalRecords = totalRecords; + } + + @Override + public int hashCode() { + return Objects.hash(columns, data, nextPage, totalRecords); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (!(o instanceof ReportDataInfo)) { + return false; + } + + ReportDataInfo other = (ReportDataInfo) o; + + return Objects.equals(columns, other.columns) && Objects.equals(data, other.data) + && Objects.equals(nextPage, other.nextPage) && Objects.equals(totalRecords, other.totalRecords); + } + +} diff --git a/commcare/src/main/java/org/motechproject/commcare/domain/report/ReportDataResponseJson.java b/commcare/src/main/java/org/motechproject/commcare/domain/report/ReportDataResponseJson.java new file mode 100644 index 000000000..9bc6e40c6 --- /dev/null +++ b/commcare/src/main/java/org/motechproject/commcare/domain/report/ReportDataResponseJson.java @@ -0,0 +1,57 @@ +package org.motechproject.commcare.domain.report; + +import com.google.gson.annotations.SerializedName; + +import java.util.List; +import java.util.Map; + +/** + * Represents a single CommCareHQ report data. It's a part of the CommCareHQ model. + */ +public class ReportDataResponseJson { + + @SerializedName("columns") + private List columns; + + @SerializedName("data") + private List> data; + + @SerializedName("next_page") + private String nextPage; + + @SerializedName("total_records") + private Integer totalRecords; + + public List getColumns() { + return columns; + } + + public List> getData() { + return data; + } + + public String getNextPage() { + return nextPage; + } + + public Integer getTotalRecords() { + return totalRecords; + } + + public void setColumns(List reportDataColumns) { + this.columns = reportDataColumns; + } + + public void setData(List> reportDataList) { + this.data = reportDataList; + } + + public void setNextPage(String nextPage) { + this.nextPage = nextPage; + } + + public void setTotalRecords(Integer totalRecords) { + this.totalRecords = totalRecords; + } + +} diff --git a/commcare/src/main/java/org/motechproject/commcare/domain/report/ReportMetadataColumn.java b/commcare/src/main/java/org/motechproject/commcare/domain/report/ReportMetadataColumn.java new file mode 100644 index 000000000..833cb6798 --- /dev/null +++ b/commcare/src/main/java/org/motechproject/commcare/domain/report/ReportMetadataColumn.java @@ -0,0 +1,80 @@ +package org.motechproject.commcare.domain.report; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; +import org.motechproject.commcare.domain.report.constants.ColumnType; + +import java.io.Serializable; +import java.util.Objects; + +/** + * Represents a single CommCareHQ {@link ReportMetadataInfo} column. + */ +public class ReportMetadataColumn implements Serializable { + + private static final long serialVersionUID = - 5218432575178076712L; + + @Expose + @SerializedName("column_id") + private String id; + + @Expose + @SerializedName("display") + private String display; + + @Expose + @SerializedName("type") + private ColumnType type; + + public ReportMetadataColumn() { } + + public ReportMetadataColumn(String id, String display, ColumnType type) { + this.id = id; + this.display = display; + this.type = type; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getDisplay() { + return display; + } + + public void setDisplay(String display) { + this.display = display; + } + + public ColumnType getType() { + return type; + } + + public void setType(ColumnType type) { + this.type = type; + } + + @Override + public int hashCode() { + return Objects.hash(id, display, type); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (!(o instanceof ReportMetadataColumn)) { + return false; + } + + ReportMetadataColumn other = (ReportMetadataColumn) o; + + return Objects.equals(id, other.id) && Objects.equals(display, other.display) && Objects.equals(type, other.type); + } +} diff --git a/commcare/src/main/java/org/motechproject/commcare/domain/report/ReportMetadataFilter.java b/commcare/src/main/java/org/motechproject/commcare/domain/report/ReportMetadataFilter.java new file mode 100644 index 000000000..adb6c8f88 --- /dev/null +++ b/commcare/src/main/java/org/motechproject/commcare/domain/report/ReportMetadataFilter.java @@ -0,0 +1,82 @@ +package org.motechproject.commcare.domain.report; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; +import org.motechproject.commcare.domain.report.constants.FilterDataType; +import org.motechproject.commcare.domain.report.constants.FilterType; + +import java.io.Serializable; +import java.util.Objects; + +/** + * Represents a single CommCareHQ {@link ReportMetadataInfo} filter. + */ +public class ReportMetadataFilter implements Serializable { + + private static final long serialVersionUID = 8994777814607875504L; + + @Expose + @SerializedName("datatype") + private FilterDataType datatype; + + @Expose + @SerializedName("slug") + private String slug; + + @Expose + @SerializedName("type") + private FilterType type; + + public ReportMetadataFilter() { } + + public ReportMetadataFilter(FilterDataType datatype, String slug, FilterType type) { + this.datatype = datatype; + this.slug = slug; + this.type = type; + } + + public FilterDataType getDatatype() { + return datatype; + } + + public void setDatatype(FilterDataType datatype) { + this.datatype = datatype; + } + + public String getSlug() { + return slug; + } + + public void setSlug(String slug) { + this.slug = slug; + } + + public FilterType getType() { + return type; + } + + public void setType(FilterType type) { + this.type = type; + } + + @Override + public int hashCode() { + return Objects.hash(datatype, slug, type); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (!(o instanceof ReportMetadataFilter)) { + return false; + } + + ReportMetadataFilter other = (ReportMetadataFilter) o; + + return Objects.equals(datatype, other.datatype) && Objects.equals(slug, other.slug) && Objects.equals(type, other.type); + } + +} diff --git a/commcare/src/main/java/org/motechproject/commcare/domain/report/ReportMetadataInfo.java b/commcare/src/main/java/org/motechproject/commcare/domain/report/ReportMetadataInfo.java new file mode 100644 index 000000000..981401bde --- /dev/null +++ b/commcare/src/main/java/org/motechproject/commcare/domain/report/ReportMetadataInfo.java @@ -0,0 +1,111 @@ +package org.motechproject.commcare.domain.report; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +import java.io.Serializable; +import java.util.List; +import java.util.Objects; + +/** + * Represents a single CommCareHQ report. It's a part of the MOTECH model. + */ +public class ReportMetadataInfo implements Serializable { + + private static final long serialVersionUID = 1616570156650333507L; + + @Expose + @SerializedName("id") + private String id; + + @Expose + @SerializedName("title") + private String title; + + @Expose + @SerializedName("resource") + private String resource; + + @Expose + @SerializedName("columns") + private List columns; + + @Expose + @SerializedName("filters") + private List filters; + + public ReportMetadataInfo() { } + + public ReportMetadataInfo(String id, String title, List columns, List filters) { + this.id = id; + this.title = title; + this.columns = columns; + this.filters = filters; + } + + public ReportMetadataInfo (String id, String title, String resource, List columns, List filters) { + this(id, title, columns, filters); + this.resource = resource; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getResource () { + return resource; + } + + public void setResource (String resource) { + this.resource = resource; + } + + public List getColumns() { + return columns; + } + + public void setColumns(List columns) { + this.columns = columns; + } + + public List getFilters() { + return filters; + } + + public void setFilters(List filters) { + this.filters = filters; + } + + @Override + public int hashCode() { + return Objects.hash(id, title, columns, filters); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (!(o instanceof ReportMetadataInfo)) { + return false; + } + + ReportMetadataInfo other = (ReportMetadataInfo) o; + + return Objects.equals(id, other.id) && Objects.equals(title, other.title) + && Objects.equals(columns, other.columns) && Objects.equals(filters, other.filters); + } +} diff --git a/commcare/src/main/java/org/motechproject/commcare/domain/report/ReportMetadataJson.java b/commcare/src/main/java/org/motechproject/commcare/domain/report/ReportMetadataJson.java new file mode 100644 index 000000000..f1c67744e --- /dev/null +++ b/commcare/src/main/java/org/motechproject/commcare/domain/report/ReportMetadataJson.java @@ -0,0 +1,66 @@ +package org.motechproject.commcare.domain.report; + +import com.google.gson.annotations.SerializedName; + +import java.util.List; + +/** + * Represents a single CommCareHQ report. It's a part of the CommCareHQ model. + */ +public class ReportMetadataJson { + + @SerializedName("title") + private String title; + + @SerializedName("id") + private String id; + + @SerializedName("resource") + private String resource; + + @SerializedName("columns") + private List columns; + + @SerializedName("filters") + private List filters; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getResource() { + return resource; + } + + public void setResource() { + this.resource = resource; + } + + public List getColumns() { + return columns; + } + + public void setColumns(List columns) { + this.columns = columns; + } + + public List getFilters() { + return filters; + } + + public void setFilters(List filters) { + this.filters = filters; + } +} diff --git a/commcare/src/main/java/org/motechproject/commcare/domain/report/ReportsMetadataInfo.java b/commcare/src/main/java/org/motechproject/commcare/domain/report/ReportsMetadataInfo.java new file mode 100644 index 000000000..dcb460f79 --- /dev/null +++ b/commcare/src/main/java/org/motechproject/commcare/domain/report/ReportsMetadataInfo.java @@ -0,0 +1,188 @@ +package org.motechproject.commcare.domain.report; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonParseException; +import com.google.gson.annotations.SerializedName; +import com.google.gson.reflect.TypeToken; +import org.apache.commons.lang.StringEscapeUtils; +import org.motechproject.commcare.domain.CommcareMetadataInfo; +import org.motechproject.commons.api.json.MotechJsonReader; +import org.motechproject.mds.annotations.Access; +import org.motechproject.mds.annotations.CrudEvents; +import org.motechproject.mds.annotations.Entity; +import org.motechproject.mds.annotations.Field; +import org.motechproject.mds.annotations.Ignore; +import org.motechproject.mds.event.CrudEventType; +import org.motechproject.mds.util.SecurityMode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jdo.annotations.NotPersistent; +import java.lang.reflect.Type; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** + * Wrapper class for storing list of instances of the {@link ReportMetadataInfo} class. It's a part of the MOTECH model. + */ +@Entity(name = "Commcare Reports Metadata") +@CrudEvents(CrudEventType.NONE) +@Access(value = SecurityMode.PERMISSIONS, members = {"manageCommcare"}) +public class ReportsMetadataInfo { + + private static final Logger LOGGER = LoggerFactory.getLogger(ReportsMetadataInfo.class); + + @Ignore + @NotPersistent + @SerializedName("objects") + private List reportMetadataInfoList; + + @Ignore + @NotPersistent + @SerializedName("meta") + private CommcareMetadataInfo metadataInfo; + + @Field(displayName = "Source configuration") + private String configName; + + @Field(displayName = "Metadata", type = "text") + private String serializedReportMetadataInfoList; + + @Field(displayName = "Reports", type = "text") + private String serializedMetadataInfo; + + public ReportsMetadataInfo() { } + + public ReportsMetadataInfo(List reportMetadataInfoList, CommcareMetadataInfo metadataInfo) { + this.reportMetadataInfoList = reportMetadataInfoList; + this.metadataInfo = metadataInfo; + serializeMetadataInfo(); + serializeReportMetadataInfoList(); + } + + public List getReportMetadataInfoList() { + if (reportMetadataInfoList == null && serializedReportMetadataInfoList != null) { + deserializeReportMetadataInfoList(); + } + return reportMetadataInfoList; + } + + public void setReportMetadataInfoList(List reportMetadataInfoList) { + this.reportMetadataInfoList = reportMetadataInfoList; + serializeReportMetadataInfoList(); + } + + public CommcareMetadataInfo getMetadataInfo() { + if (metadataInfo == null && serializedMetadataInfo != null) { + deserializeMetadataInfo(); + } + return metadataInfo; + } + + public void setMetadataInfo(CommcareMetadataInfo metadataInfo) { + this.metadataInfo = metadataInfo; + serializeMetadataInfo(); + } + + public String getConfigName () { + return configName; + } + + public void setConfigName (String configName) { + this.configName = configName; + } + + public String getSerializedReportMetadataInfoList () { + if (serializedReportMetadataInfoList == null && reportMetadataInfoList != null) { + serializeReportMetadataInfoList(); + } + return serializedReportMetadataInfoList; + } + + public void setSerializedReportMetadataInfoList (String serializedReportMetadataInfoList) { + this.serializedReportMetadataInfoList = serializedReportMetadataInfoList; + deserializeReportMetadataInfoList(); + } + + public String getSerializedMetadataInfo () { + if (serializedMetadataInfo == null && metadataInfo != null) { + serializeMetadataInfo(); + } + return serializedMetadataInfo; + } + + public void setSerializedMetadataInfo (String serializedMetadataInfo) { + this.serializedMetadataInfo = serializedMetadataInfo; + serializeMetadataInfo(); + } + + @Override + public int hashCode() { + return Objects.hash(reportMetadataInfoList, metadataInfo, serializedMetadataInfo, serializedReportMetadataInfoList, configName); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (!(o instanceof ReportsMetadataInfo)) { + return false; + } + + ReportsMetadataInfo other = (ReportsMetadataInfo) o; + + return Objects.equals(reportMetadataInfoList, other.reportMetadataInfoList) + && Objects.equals(metadataInfo, other.metadataInfo) + && Objects.equals(serializedMetadataInfo, other.serializedMetadataInfo) + && Objects.equals(serializedReportMetadataInfoList, other.serializedReportMetadataInfoList) + && Objects.equals(configName, other.configName); + } + + private void serializeMetadataInfo () { + if (metadataInfo != null) { + Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create(); + serializedMetadataInfo = StringEscapeUtils.escapeJava(gson.toJson(metadataInfo)); + } + } + + private void deserializeMetadataInfo () { + if (serializedMetadataInfo != null) { + Type deserializeType = new TypeToken() { } .getType(); + MotechJsonReader motechJsonReader = new MotechJsonReader(); + + try { + metadataInfo = (CommcareMetadataInfo) motechJsonReader.readFromStringOnlyExposeAnnotations( + StringEscapeUtils.unescapeJava(serializedMetadataInfo), deserializeType); + } catch (JsonParseException e) { + LOGGER.error("Failed to deserialize CommCare schema from its JSON representation in the database. Fix the errors in the schema or force Commcare module to download the fresh schema.", e); + metadataInfo = null; + } + } + } + + private void serializeReportMetadataInfoList () { + if (reportMetadataInfoList != null) { + Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create(); + serializedReportMetadataInfoList = StringEscapeUtils.escapeJava(gson.toJson(reportMetadataInfoList)); + } + } + + private void deserializeReportMetadataInfoList () { + if (serializedReportMetadataInfoList != null) { + Type deserializeType = new TypeToken>() { } .getType(); + MotechJsonReader motechJsonReader = new MotechJsonReader(); + + try { + reportMetadataInfoList = (List) motechJsonReader.readFromStringOnlyExposeAnnotations( + StringEscapeUtils.unescapeJava(serializedReportMetadataInfoList), deserializeType); + } catch (JsonParseException e) { + LOGGER.error("Failed to deserialize CommCare schema from its JSON representation in the database. Fix the errors in the schema or force Commcare module to download the fresh schema.", e); + reportMetadataInfoList = Collections.emptyList(); + } + } + } +} diff --git a/commcare/src/main/java/org/motechproject/commcare/domain/report/ReportsMetadataResponseJson.java b/commcare/src/main/java/org/motechproject/commcare/domain/report/ReportsMetadataResponseJson.java new file mode 100644 index 000000000..e08352eec --- /dev/null +++ b/commcare/src/main/java/org/motechproject/commcare/domain/report/ReportsMetadataResponseJson.java @@ -0,0 +1,35 @@ +package org.motechproject.commcare.domain.report; + +import com.google.gson.annotations.SerializedName; +import org.motechproject.commcare.domain.CommcareMetadataJson; + +import java.util.List; + +/** + * Wrapper class for storing list of instances of the {@link ReportMetadataJson} class and their metadata. It's part of the + * CommCareHQ model. + */ +public class ReportsMetadataResponseJson { + + @SerializedName("meta") + private CommcareMetadataJson metadata; + + @SerializedName("objects") + private List reports; + + public CommcareMetadataJson getMetadata() { + return metadata; + } + + public void setMetadata(CommcareMetadataJson metadata) { + this.metadata = metadata; + } + + public List getReports() { + return reports; + } + + public void setReports(List reports) { + this.reports = reports; + } +} diff --git a/commcare/src/main/java/org/motechproject/commcare/domain/report/constants/ColumnType.java b/commcare/src/main/java/org/motechproject/commcare/domain/report/constants/ColumnType.java new file mode 100644 index 000000000..f13356a8a --- /dev/null +++ b/commcare/src/main/java/org/motechproject/commcare/domain/report/constants/ColumnType.java @@ -0,0 +1,28 @@ +package org.motechproject.commcare.domain.report.constants; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; + +import java.lang.reflect.Type; + +public enum ColumnType { + FIELD("field"), + EXPANDED("expanded"), + PERCENT("percent"); + + private final String type; + + ColumnType(String type) { + this.type = type; + } + + public static class ColumnTypeDeserializer implements JsonDeserializer { + + @Override + public ColumnType deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) { + String value = json.getAsString(); + return ColumnType.valueOf(value.toUpperCase()); + } + } +} diff --git a/commcare/src/main/java/org/motechproject/commcare/domain/report/constants/FilterDataType.java b/commcare/src/main/java/org/motechproject/commcare/domain/report/constants/FilterDataType.java new file mode 100644 index 000000000..bfad3dd02 --- /dev/null +++ b/commcare/src/main/java/org/motechproject/commcare/domain/report/constants/FilterDataType.java @@ -0,0 +1,28 @@ +package org.motechproject.commcare.domain.report.constants; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; + +import java.lang.reflect.Type; + +public enum FilterDataType { + STRING("string"), + INTEGER("integer"), + DECIMAL("decimal"); + + private final String datatype; + + FilterDataType(String datatype) { + this.datatype = datatype; + } + + public static class FilterDataTypeDeserializer implements JsonDeserializer { + + @Override + public FilterDataType deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) { + String value = json.getAsString(); + return FilterDataType.valueOf(value.toUpperCase()); + } + } +} diff --git a/commcare/src/main/java/org/motechproject/commcare/domain/report/constants/FilterType.java b/commcare/src/main/java/org/motechproject/commcare/domain/report/constants/FilterType.java new file mode 100644 index 000000000..cf39aed87 --- /dev/null +++ b/commcare/src/main/java/org/motechproject/commcare/domain/report/constants/FilterType.java @@ -0,0 +1,30 @@ +package org.motechproject.commcare.domain.report.constants; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; + +import java.lang.reflect.Type; + +public enum FilterType { + NUMERIC("numeric"), + DATE("date"), + CHOICE_LIST("choice_list"), + DYNAMIC_CHOICE_LIST("dynamic_choice_list"), + PRE("pre"); + + private final String type; + + FilterType(String type) { + this.type = type; + } + + public static class FilterTypeDeserializer implements JsonDeserializer { + + @Override + public FilterType deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) { + String value = json.getAsString(); + return FilterType.valueOf(value.toUpperCase()); + } + } +} diff --git a/commcare/src/main/java/org/motechproject/commcare/events/CaseEvent.java b/commcare/src/main/java/org/motechproject/commcare/events/CaseEvent.java index fc470cf23..0d768e58b 100644 --- a/commcare/src/main/java/org/motechproject/commcare/events/CaseEvent.java +++ b/commcare/src/main/java/org/motechproject/commcare/events/CaseEvent.java @@ -1,5 +1,6 @@ package org.motechproject.commcare.events; +import org.motechproject.commcare.domain.CaseInfo; import org.motechproject.commcare.domain.CaseXml; import org.motechproject.commcare.events.constants.EventDataKeys; import org.motechproject.commcare.events.constants.EventSubjects; @@ -124,6 +125,28 @@ public static CaseEvent fromCaseXml(CaseXml caseInstance, String configName) { return event; } + /** + * Creates an instance of the {@link CaseEvent} class based on the given {@code caseInfo} and + * {@code configName}. + * + * @param caseInfo the case information + * @param configName the configuration name + * @return an instance of the {@link CaseEvent} + */ + public static CaseEvent fromCaseInfo(CaseInfo caseInfo, String configName) { + CaseEvent event = new CaseEvent(caseInfo.getCaseId()); + event.setServerModifiedOn(caseInfo.getServerDateModified()); + event.setUserId(caseInfo.getUserId()); + event.setCaseName(caseInfo.getCaseName()); + event.setCaseType(caseInfo.getCaseType()); + event.setDateModified(caseInfo.getDateModified()); + event.setFieldValues(caseInfo.getFieldValues()); + event.setOwnerId(caseInfo.getOwnerId()); + event.setConfigName(configName); + + return event; + } + public String getServerModifiedOn() { return this.serverModifiedOn; } diff --git a/commcare/src/main/java/org/motechproject/commcare/events/MalformedFormStatusMessageEvent.java b/commcare/src/main/java/org/motechproject/commcare/events/FailedImportStatusMessageEvent.java similarity index 83% rename from commcare/src/main/java/org/motechproject/commcare/events/MalformedFormStatusMessageEvent.java rename to commcare/src/main/java/org/motechproject/commcare/events/FailedImportStatusMessageEvent.java index c471cc75c..b66ddb9f9 100644 --- a/commcare/src/main/java/org/motechproject/commcare/events/MalformedFormStatusMessageEvent.java +++ b/commcare/src/main/java/org/motechproject/commcare/events/FailedImportStatusMessageEvent.java @@ -7,10 +7,10 @@ /** * A wrapper for the event that will trigger a status message signaling - * a failure to receive a form. The status messages will be displayed by the Admin + * a failure to receive a form or a case. The status messages will be displayed by the Admin * module in its UI (if the module is present). */ -public class MalformedFormStatusMessageEvent { +public class FailedImportStatusMessageEvent { /** * The message to display. @@ -20,7 +20,7 @@ public class MalformedFormStatusMessageEvent { /** * @param message the message to display */ - public MalformedFormStatusMessageEvent(String message) { + public FailedImportStatusMessageEvent(String message) { this.message = message; } diff --git a/commcare/src/main/java/org/motechproject/commcare/events/FormActionEventHandler.java b/commcare/src/main/java/org/motechproject/commcare/events/FormActionEventHandler.java index 01b3f30f2..116b242b4 100644 --- a/commcare/src/main/java/org/motechproject/commcare/events/FormActionEventHandler.java +++ b/commcare/src/main/java/org/motechproject/commcare/events/FormActionEventHandler.java @@ -1,6 +1,5 @@ package org.motechproject.commcare.events; -import org.apache.commons.lang.StringUtils; import org.joda.time.DateTime; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; @@ -49,11 +48,9 @@ public class FormActionEventHandler { @MotechListener(subjects = EventSubjects.SUBMIT_FORM + ".*") public void submitForm(MotechEvent event) { String configName = EventSubjects.getConfigName(event.getSubject()); - String xmlns = extractXmlnsFromEventSubject(event.getSubject(), configName); Map parameters = event.getParameters(); FormXml formXml = parseEventParametersToFormXml(parameters); - formXml.setXmlns(xmlns); Map formMetadata = new HashMap<>(); formMetadata.put(INSTANCE_ID_KEY, new MetadataValue(UUID.randomUUID().toString())); @@ -66,18 +63,18 @@ public void submitForm(MotechEvent event) { commcareFormService.uploadForm(formXml, configName); } - private String extractXmlnsFromEventSubject(String subject, String configName) { - return StringUtils.removeEnd(subject.replaceFirst(EventSubjects.SUBMIT_FORM + ".", StringUtils.EMPTY), "." + configName); - } - private FormXml parseEventParametersToFormXml(Map parameters) { FormXml formXml = new FormXml(); for (Map.Entry entry : parameters.entrySet()) { if (entry.getValue() != null) { - List xmlPath = discoverXmlPath(entry.getKey()); - FormValueElement element = formXml.getElementByPath(xmlPath); - element.setValue(entry.getValue().toString()); + if ("xmlns".equals(entry.getKey())) { + formXml.setXmlns(entry.getValue().toString()); + } else { + List xmlPath = discoverXmlPath(entry.getKey()); + FormValueElement element = formXml.getElementByPath(xmlPath); + element.setValue(entry.getValue().toString()); + } } } diff --git a/commcare/src/main/java/org/motechproject/commcare/events/FullCaseFailureEvent.java b/commcare/src/main/java/org/motechproject/commcare/events/FullCaseFailureEvent.java new file mode 100644 index 000000000..f8349da7b --- /dev/null +++ b/commcare/src/main/java/org/motechproject/commcare/events/FullCaseFailureEvent.java @@ -0,0 +1,50 @@ +package org.motechproject.commcare.events; + +import org.motechproject.commcare.service.impl.CommcareCaseEventParser; +import org.motechproject.commons.api.TasksEventParser; +import org.motechproject.event.MotechEvent; + +import java.util.HashMap; +import java.util.Map; + +import static org.motechproject.commcare.events.constants.EventDataKeys.CONFIG_NAME; +import static org.motechproject.commcare.events.constants.EventDataKeys.FAILED_CASE_MESSAGE; +import static org.motechproject.commcare.events.constants.EventSubjects.CASES_FAIL_EVENT; + +/** + * Represents a failure event for a full case received. + */ +public class FullCaseFailureEvent { + /** + * Name of the Motech Commcare configuration this failure affected. + */ + private final String configName; + + /** + * The error message. + */ + private final String errorMessage; + + /** + * @param configName name of the Motech Commcare configuration this failure affected + * @param errorMessage the error message + */ + public FullCaseFailureEvent(String configName, String errorMessage) { + this.configName = configName; + this.errorMessage = errorMessage; + } + + /** + * Builds a {@link MotechEvent} instance to send. + * @return the event to send + */ + public MotechEvent toMotechEvent() { + Map params = new HashMap<>(); + + params.put(TasksEventParser.CUSTOM_PARSER_EVENT_KEY, CommcareCaseEventParser.PARSER_NAME); + params.put(CONFIG_NAME, configName); + params.put(FAILED_CASE_MESSAGE, errorMessage); + + return new MotechEvent(CASES_FAIL_EVENT, params); + } +} diff --git a/commcare/src/main/java/org/motechproject/commcare/events/ReportActionEventHandler.java b/commcare/src/main/java/org/motechproject/commcare/events/ReportActionEventHandler.java new file mode 100644 index 000000000..18fb6c371 --- /dev/null +++ b/commcare/src/main/java/org/motechproject/commcare/events/ReportActionEventHandler.java @@ -0,0 +1,146 @@ +package org.motechproject.commcare.events; + +import org.joda.time.DateTime; +import org.motechproject.commcare.domain.report.constants.FilterDataType; +import org.motechproject.commcare.domain.report.constants.FilterType; +import org.motechproject.commcare.events.constants.DisplayNames; +import org.motechproject.commcare.events.constants.EventDataKeys; +import org.motechproject.commcare.events.constants.EventSubjects; +import org.motechproject.commcare.service.ReportActionService; +import org.motechproject.event.MotechEvent; +import org.motechproject.event.listener.annotations.MotechListener; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * This class serves as the event handler for the task actions, exposed by the Commcare module. + * Respective methods extract the necessary data from the {@link MotechEvent} instance and + * handles all the operations on Commcare reports. + */ +/** + * Class responsible for handling "Report" events. The service will extract the necessary data + * from the {@link MotechEvent} and pass them to the {@link ReportActionService} service that will + * query the CommCare Report UCR API and then send a motech event for selected report. + */ +@Component +public class ReportActionEventHandler { + + private static final String TYPE_KEY_IDENTIFIER = ".type"; + private static final String START_DATE_FILTER_SUFFIX = "-start"; + private static final String END_DATE_FILTER_SUFFIX = "-end"; + private static final String NUMERIC_OPERATOR_FILTER_SUFFIX = "-operator"; + private static final String NUMERIC_OPERAND_FILTER_SUFFIX = "-operand"; + private static final String COMMCARE_REPORT_FILTER_DATE_FORMAT = "yyyy-MM-dd"; + + private ReportActionService reportActionService; + + @Autowired + public ReportActionEventHandler (ReportActionService reportActionService) { + this.reportActionService = reportActionService; + } + + /** + * Handles events, connected with getting Commcare report. The event subject should have the following syntax: + * {@code EventSubjects.REPORT_EVENT.report_name.config_name} + * + * @param event the event, containing parameters necessary to get the Commcare report + */ + @MotechListener(subjects = EventSubjects.REPORT_EVENT + ".*") + public void receiveReport (MotechEvent event) { + String configName = EventSubjects.getConfigName(event.getSubject()); + Map parameters = event.getParameters(); + + String reportId = (String) parameters.get(EventDataKeys.REPORT_ID); + String reportName = (String) parameters.get(EventDataKeys.REPORT_NAME); + List fields = getFieldNamesFromParameters(parameters); + + String parsedFilters = convertFieldsToRequest(fields, parameters); + + reportActionService.queryReport(configName, reportId, reportName, parsedFilters); + } + + private List getFieldNamesFromParameters (Map parameters) { + List fieldNames = new ArrayList<>(); + + for (String key : parameters.keySet()) { + if (key.endsWith(TYPE_KEY_IDENTIFIER)) { + fieldNames.add(key.substring(0, key.indexOf(TYPE_KEY_IDENTIFIER))); + } + } + + return fieldNames; + } + + private String convertFieldsToRequest (List fieldNames, Map parameters) { + StringBuilder urlFilterBuilder = new StringBuilder(); + + for (String fieldName : fieldNames) { + String fieldType = (String) parameters.get(fieldName.concat(TYPE_KEY_IDENTIFIER)); + + if (fieldType.equals(FilterType.CHOICE_LIST.toString())) { + String fieldValue = (String) parameters.get(fieldName); + if (fieldValue != null) { + urlFilterBuilder.append("&") + .append(fieldName) + .append("=") + .append(fieldValue); + } + } else if (fieldType.equals(FilterType.DATE.toString())) { + urlFilterBuilder.append(parseDateField(fieldName, parameters)); + } else if (fieldType.equals(FilterDataType.DECIMAL.toString())) { + urlFilterBuilder.append(parseNumericField(fieldName, parameters)); + } + } + return urlFilterBuilder.toString(); + } + + private String parseDateField (String fieldName, Map parameters) { + StringBuilder parsedDataFilterBuilder = new StringBuilder(); + + DateTime startDate = (DateTime) parameters.get(fieldName); + DateTime endDate = (DateTime) parameters.get(fieldName + "." + DisplayNames.END_DATE); + + if (startDate != null) { + parsedDataFilterBuilder.append("&") + .append(fieldName) + .append(START_DATE_FILTER_SUFFIX) + .append("=") + .append(startDate.toString(COMMCARE_REPORT_FILTER_DATE_FORMAT)); + } + if (endDate != null) { + parsedDataFilterBuilder.append("&") + .append(fieldName) + .append(END_DATE_FILTER_SUFFIX) + .append("=") + .append(endDate.toString(COMMCARE_REPORT_FILTER_DATE_FORMAT)); + } + + return parsedDataFilterBuilder.toString(); + } + + private String parseNumericField (String fieldName, Map parameters) { + StringBuilder parsedNumericFilterBuilder = new StringBuilder(); + + String operator = (String) parameters.get(fieldName); + String operand = (String) parameters.get(fieldName + "." + EventDataKeys.VALUE); + + if (operator != null && operand != null) { + parsedNumericFilterBuilder.append("&") + .append(fieldName) + .append(NUMERIC_OPERATOR_FILTER_SUFFIX) + .append("=") + .append(operator) + .append("&") + .append(fieldName) + .append(NUMERIC_OPERAND_FILTER_SUFFIX) + .append("=") + .append(operand); + } + + return parsedNumericFilterBuilder.toString(); + } +} diff --git a/commcare/src/main/java/org/motechproject/commcare/events/constants/DisplayNames.java b/commcare/src/main/java/org/motechproject/commcare/events/constants/DisplayNames.java index a3a611b91..001cffca1 100644 --- a/commcare/src/main/java/org/motechproject/commcare/events/constants/DisplayNames.java +++ b/commcare/src/main/java/org/motechproject/commcare/events/constants/DisplayNames.java @@ -12,6 +12,7 @@ public final class DisplayNames { public static final String UPDATE_CASE = "Update Case"; public static final String SUBMIT_FORM = "Submit Form"; public static final String IMPORT_FORMS = "Import Forms"; + public static final String CREATE_REPORT = "Create Report"; public static final String CONFIG_NAME = "commcare.configName"; public static final String CASE_ID = "commcare.caseId"; diff --git a/commcare/src/main/java/org/motechproject/commcare/events/constants/EventDataKeys.java b/commcare/src/main/java/org/motechproject/commcare/events/constants/EventDataKeys.java index 77c8bf3c7..6f7a5e34e 100644 --- a/commcare/src/main/java/org/motechproject/commcare/events/constants/EventDataKeys.java +++ b/commcare/src/main/java/org/motechproject/commcare/events/constants/EventDataKeys.java @@ -70,6 +70,11 @@ public final class EventDataKeys { //FullFormsExceptionEvent public static final String FAILED_FORM_MESSAGE = "failedMessage"; + public static final String FAILED_CASE_MESSAGE = "failed case import message"; + + //ReportEvent + public static final String REPORT_ID = "report_id"; + public static final String REPORT_NAME = "report_name"; /** * Utility class, should not be initiated. diff --git a/commcare/src/main/java/org/motechproject/commcare/events/constants/EventSubjects.java b/commcare/src/main/java/org/motechproject/commcare/events/constants/EventSubjects.java index 82694d47f..523c139b0 100644 --- a/commcare/src/main/java/org/motechproject/commcare/events/constants/EventSubjects.java +++ b/commcare/src/main/java/org/motechproject/commcare/events/constants/EventSubjects.java @@ -8,11 +8,13 @@ public final class EventSubjects { private static final String BASE_SUBJECT = "org.motechproject.commcare.api."; public static final String CASE_EVENT = BASE_SUBJECT + "case"; + public static final String REPORT_EVENT = BASE_SUBJECT + "report"; public static final String MALFORMED_CASE_EXCEPTION = BASE_SUBJECT + "exception"; public static final String FORM_STUB_EVENT = BASE_SUBJECT + "formstub"; public static final String FORM_STUB_FAIL_EVENT = FORM_STUB_EVENT + ".failed"; public static final String FORMS_EVENT = BASE_SUBJECT + "forms"; public static final String FORMS_FAIL_EVENT = FORMS_EVENT + ".failed"; + public static final String CASES_FAIL_EVENT = CASE_EVENT + ".failed"; public static final String RECEIVED_STOCK_TRANSACTION = BASE_SUBJECT + "receivedStockTransaction"; public static final String QUERY_STOCK_LEDGER = BASE_SUBJECT + "queryStockLedger"; public static final String IMPORT_FORMS = BASE_SUBJECT + "importForms"; @@ -24,6 +26,7 @@ public final class EventSubjects { public static final String CREATE_CASE = BASE_SUBJECT + "createCase"; public static final String UPDATE_CASE = BASE_SUBJECT + "updateCase"; public static final String SUBMIT_FORM = BASE_SUBJECT + "submitForm"; + public static final String RECEIVED_REPORT = BASE_SUBJECT + "receivedReport"; public static String getConfigName(String subject) { return subject.substring(subject.lastIndexOf('.') + 1); diff --git a/commcare/src/main/java/org/motechproject/commcare/pull/CaseImportStatus.java b/commcare/src/main/java/org/motechproject/commcare/pull/CaseImportStatus.java new file mode 100644 index 000000000..133aa3255 --- /dev/null +++ b/commcare/src/main/java/org/motechproject/commcare/pull/CaseImportStatus.java @@ -0,0 +1,139 @@ +package org.motechproject.commcare.pull; + +/** + * Represents the status of an ongoing case import. + */ +public class CaseImportStatus { + /** + * The number of cases successfully imported. + */ + private int casesImported; + + /** + * The total number of cases to import. + */ + private int totalCases; + + /** + * The date of the last successfully imported case. + */ + private String lastImportDate; + + /** + * The ID of the last successfully imported case. + */ + private String lastImportCaseId; + + /** + * Whether the import ended in error. + */ + private boolean error; + + /** + * The error message, if applicable. + */ + private String errorMsg; + + /** + * Is import in progress currently. + */ + private boolean importInProgress; + + /** + * @return the number of cases successfully imported + */ + public int getCasesImported() { + return casesImported; + } + + /** + * @param casesImported the number of cases successfully imported + */ + public void setCasesImported(int casesImported) { + this.casesImported = casesImported; + } + + /** + * @return the total number of cases to import + */ + public int getTotalCases() { + return totalCases; + } + + /** + * @param totalCases the total number of cases to import + */ + public void setTotalCases(int totalCases) { + this.totalCases = totalCases; + } + + /** + * @return the date of the last successfully imported case + */ + public String getLastImportDate() { + return lastImportDate; + } + + /** + * @param lastImportDate the date of the last successfully imported case + */ + public void setLastImportDate(String lastImportDate) { + this.lastImportDate = lastImportDate; + } + + /** + * @return true if the import ended on error, false otherwise + */ + public boolean isError() { + return error; + } + + /** + * @param error true if the import ended on error, false otherwise + */ + public void setError(boolean error) { + this.error = error; + } + + /** + * @return the message for the error that stopped the import + */ + public String getErrorMsg() { + return errorMsg; + } + + /** + * @param errorMsg the message for the error that stopped the import + */ + public void setErrorMsg(String errorMsg) { + this.errorMsg = errorMsg; + } + + /** + * @return true if import is in progress, false otherwise + */ + public boolean isImportInProgress() { + return importInProgress; + } + + /** + * @param importInProgress true if import is in progress, false otherwise + */ + public void setImportInProgress(boolean importInProgress) { + this.importInProgress = importInProgress; + } + + /** + * @return the ID of the last successfully imported case + */ + public String getLastImportCaseId() { + return lastImportCaseId; + } + + /** + * @param lastImportCaseId the ID of the last successfully imported case + */ + public void setLastImportCaseId(String lastImportCaseId) { + this.lastImportCaseId = lastImportCaseId; + } +} diff --git a/commcare/src/main/java/org/motechproject/commcare/pull/CommcareCaseImporter.java b/commcare/src/main/java/org/motechproject/commcare/pull/CommcareCaseImporter.java new file mode 100644 index 000000000..94074a720 --- /dev/null +++ b/commcare/src/main/java/org/motechproject/commcare/pull/CommcareCaseImporter.java @@ -0,0 +1,88 @@ +package org.motechproject.commcare.pull; + +import org.joda.time.DateTime; +import org.motechproject.commons.api.Range; + +/** + * The interface for the class managing case imports. The import + * can be based on a caseId or a date range. All cases retrieved from Commcare that match + * the provided caseId or date range will be treated as if they would just arrived through + * the case forwarding mechanism - an event will be published for each case. + * In case of failures, the import will be stopped, events for failure will also be fired, + * same as in the case forwarding controller. The import is done asynchronously in a separate thread, + * so this interface also allows checking the status of an ongoing import. + */ +public interface CommcareCaseImporter { + + /** + * Counts how many cases match the provided criteria. A single request to the case list + * API will be made underneath, and the returned total count will be returned by this method. + * @param dateRange the date range for the import + * @param configName the name of the configuration to use, if null is provided the default configuration + * will be used + * @return the total number of cases matching the provided criteria + * @throws IllegalArgumentException if the date range is null or invalid (start date after end date) + * @throws IllegalStateException if there is an already ongoing import in progress + */ + int countForImport(Range dateRange, String configName); + + /** + * Initiates asynchronous import for the provided caseId. Cases will be fetched from Commcare by doing HTTP + * request to the case list API. The import will run in a separate thread. An event will be fired for the + * successfully imported case. If an error occurs import will be stopped. + * @param caseId the uuid of the case to be imported. + * @param configName the name of the configuration to use, if null is provided the default configuration + * will be used + */ + void importSingleCase(final String caseId, final String configName); + + /** + * Initiates asynchronous import for the provided criteria. Cases will be fetched from Commcare by doing HTTP + * request to the case list API. The default fetch size per request is 100, but that can be controlled using the + * {@link #setFetchSize(int)} method. The import will run in a separate thread. An event will be fired for each + * successfully imported case. If an error occurs, the import will be stopped. + * @param dateRange the date range for the import + * @param configName the name of the configuration to use, if null is provided the default configuration + * will be used + * @throws IllegalArgumentException if the date range is null or invalid (start date after end date) + * @throws IllegalStateException if there is an already ongoing import in progress + */ + void startImport(final Range dateRange, final String configName); + + /** + * Stops the ongoing import. The effect is not guaranteed to be immediate. + */ + void stopImport(); + + /** + * Checks whether a case with the given id exists. + * @param caseId the uuid of the case to be imported. + * @param configName the name of the configuration to use, if null is provided the default configuration + * will be used + * @return boolean value + */ + boolean checkCaseIdForImport(final String caseId, final String configName); + + /** + * Checks whether there is an ongoing import in progress. + * @return true if there is an ongoing import, false otherwise + */ + boolean isImportInProgress(); + + /** + * Retrieves an {@link CaseImportStatus} object representing the status of the current, or + * the last import. {@link CaseImportStatus#isImportInProgress()} can be used to check if the import is ongoing, + * {@link CaseImportStatus#isError()} can be checked to see if the import failed. The received_on date for the + * last successfully imported case can be retrieved using {@link CaseImportStatus#getLastImportDate()}. + * @return the status object for the current or last import + */ + CaseImportStatus importStatus(); + + /** + * Sets the fetch size for the queries to the Commcare case list API. In other words, sets how many cases + * will be retrieved per request to the API. The default is 100. Setting a too large number might cause memory + * issues, setting a too low number might result in a large number of HTTP requests. + * @param fetchSize the fetch size for requests + */ + void setFetchSize(int fetchSize); +} diff --git a/commcare/src/main/java/org/motechproject/commcare/pull/CommcareCaseImporterFactory.java b/commcare/src/main/java/org/motechproject/commcare/pull/CommcareCaseImporterFactory.java new file mode 100644 index 000000000..4b4ef8f99 --- /dev/null +++ b/commcare/src/main/java/org/motechproject/commcare/pull/CommcareCaseImporterFactory.java @@ -0,0 +1,77 @@ +package org.motechproject.commcare.pull; + +import org.motechproject.commcare.service.CommcareCaseService; +import org.motechproject.event.listener.EventRelay; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.servlet.http.HttpSession; +import javax.servlet.http.HttpSessionEvent; +import javax.servlet.http.HttpSessionListener; +import java.util.HashMap; +import java.util.Map; + +/** + * This is the factory responsible for providing instances {@link CommcareCaseImporter} to + * the web layer. Importer instances are stored on a per http session basis. The instances are discarded + * once a session becomes invalidated. This class implements the {@link HttpSessionListener} to keep track + * of sessions being discarded (the session created event does not concern it). + */ +@Service("commcareCaseImporterFactory") +public class CommcareCaseImporterFactory implements HttpSessionListener{ + + private static final Logger LOGGER = LoggerFactory.getLogger(CommcareCaseImporterFactory.class); + + @Autowired + private EventRelay eventRelay; + + @Autowired + private CommcareCaseService caseService; + + private final Map importerMap = new HashMap<>(); + + /** + * Retrieves an importer instance for the given session. A new instance will be created if it doesn't yet exist. + * @param session the http session for which the importer should be retrieved. + * @return the importer instance for the session + * @throws IllegalArgumentException if the session is null + */ + public CommcareCaseImporter getImporter(HttpSession session) { + if (session == null) { + throw new IllegalArgumentException("No session provided, importers must be tied with an HTTP session"); + } + + final String sid = session.getId(); + + LOGGER.debug("Retrieving importer for session with ID: {}", sid); + + if (!importerMap.containsKey(sid)) { + LOGGER.debug("No importer available for session with ID: {}. Creating a new one.", sid); + importerMap.put(sid, new CommcareCaseImporterImpl(eventRelay, caseService)); + } + + return importerMap.get(sid); + } + + @Override + public void sessionCreated(HttpSessionEvent se) { + LOGGER.trace("Received session created event, id: {}", se.getSession().getId()); + } + + @Override + public void sessionDestroyed(HttpSessionEvent se) { + final String sid = se.getSession().getId(); + LOGGER.debug("Received sessionDestroyed event, id: {}", sid); + + CommcareCaseImporter removedImporter = importerMap.remove(sid); + + if (removedImporter == null) { + LOGGER.debug("No importer stored for session with id: {}", sid); + } else { + LOGGER.debug("Discarding importer for session with id: {}, import in progress: {}", + sid, removedImporter.isImportInProgress()); + } + } +} diff --git a/commcare/src/main/java/org/motechproject/commcare/pull/CommcareCaseImporterImpl.java b/commcare/src/main/java/org/motechproject/commcare/pull/CommcareCaseImporterImpl.java new file mode 100644 index 000000000..64132fbd1 --- /dev/null +++ b/commcare/src/main/java/org/motechproject/commcare/pull/CommcareCaseImporterImpl.java @@ -0,0 +1,269 @@ +package org.motechproject.commcare.pull; + +import org.joda.time.DateTime; +import org.joda.time.format.DateTimeFormat; +import org.joda.time.format.DateTimeFormatter; +import org.motechproject.commcare.domain.CaseInfo; +import org.motechproject.commcare.domain.CasesInfo; +import org.motechproject.commcare.events.CaseEvent; +import org.motechproject.commcare.events.FullCaseFailureEvent; +import org.motechproject.commcare.events.FailedImportStatusMessageEvent; +import org.motechproject.commcare.service.CommcareCaseService; +import org.motechproject.commons.api.Range; +import org.motechproject.event.listener.EventRelay; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The implementation of {@link CommcareCaseImporter}. Uses the {@link CommcareCaseService} for + * retrieval of cases. + */ +public class CommcareCaseImporterImpl implements CommcareCaseImporter { + + private static final Logger LOGGER = LoggerFactory.getLogger(CommcareCaseImporterImpl.class); + + private static final int PAGE_SIZE_FOR_FETCH = 100; + + private CommcareCaseService caseService; + private EventRelay eventRelay; + + private int fetchSize = PAGE_SIZE_FOR_FETCH; + + private Thread importThread; + private boolean importInProgress = false; + + private int importCount; + private int totalCount; + private int pageCount; + private String lastImportedDate; + private String lastImportedCaseId; + private boolean inError; + private String errorMessage; + + public CommcareCaseImporterImpl(EventRelay eventRelay, CommcareCaseService caseService) { + this.eventRelay = eventRelay; + this.caseService = caseService; + } + + @Override + public int countForImport(Range dateRange, String configName) { + validateDateRange(dateRange); + + LOGGER.info("Counting forms for import for dateRange: {}-{} [config: {}]", dateRange.getMin(), + dateRange.getMax(), configName); + + CasesInfo cases = caseService.getCasesByCasesTimeWithMetadata(dateTimeToString(dateRange.getMin()), + dateTimeToString(dateRange.getMax()), 1, 1, configName); + + int count = cases.getMetadataInfo().getTotalCount(); + LOGGER.info("Case count: {}", count); + + return count; + } + + public void importSingleCase(final String caseId, final String configName) { + LOGGER.info("Initiating import request for case with id : {} [config: {}]", caseId, configName); + importInProgress = true; + totalCount = 1; + + LOGGER.debug("Retrieving case info for case with id : {}", caseId); + // Fetching the case + CaseInfo caseInfo = caseService.getCaseByCaseId(caseId, configName); + + LOGGER.debug("Retrieved the case"); + // Sending event for the fetched case + CaseEvent caseEvent = CaseEvent.fromCaseInfo(caseInfo, configName); + eventRelay.sendEventMessage(caseEvent.toMotechEventWithData()); + + lastImportedCaseId = caseInfo.getCaseId(); + lastImportedDate = caseInfo.getDateModified(); + importCount = 1; + + LOGGER.info("Imported case with ID: {}", caseInfo.getCaseId()); + LOGGER.info("Case import finished for case with id : {}. ", caseId); + + importInProgress = false; + } + + public boolean checkCaseIdForImport(final String caseId, final String configName) { + LOGGER.debug("Checking if the case with id : {} exists", caseId); + CaseInfo caseInfo = caseService.getCaseByCaseId(caseId, configName); + LOGGER.info("Case with id : {} exists", caseId); + + return !(caseInfo == null); + } + + public void startImport(final Range dateRange, final String configName) { + validateDateRange(dateRange); + + LOGGER.info("Initiating case import for historical cases from {} to {} [config: {}]", + dateRange.getMin(), dateRange.getMax(), configName); + + initForImport(dateRange, configName); + + importThread = new Thread(new Runnable() { + @Override + public void run() { + boolean hasMore = false; + int currentPage = 1; + + LOGGER.debug("Import thread started"); + + do { + try { + LOGGER.debug("Retrieving cases from page {}, with page size {}", + pageCount, fetchSize); + // fetching the cases + CasesInfo caseList = caseService.getCasesByCasesTimeWithMetadata(dateTimeToString(dateRange.getMin()), + dateTimeToString(dateRange.getMax()), fetchSize, currentPage, configName); + + LOGGER.debug("Retrieved a list of {} cases", caseList.getCaseInfoList().size()); + + // sending events for each case + importCaseList(caseList, configName); + + LOGGER.debug("Imported {} cases", caseList.getCaseInfoList().size()); + + // the first page is the last one + hasMore = currentPage < pageCount; + + // we decrement the page + if (hasMore) { + LOGGER.debug("Proceeding to the next batch of cases"); + currentPage++; + } + + } catch (RuntimeException e) { + LOGGER.error("Error while importing cases", e); + LOGGER.error("{} of {} cases imported.", importCount, totalCount); + handleImportError(e, configName); + } + } while (importInProgress && hasMore); + + LOGGER.info("Case import finished. {} of {} cases imported. ", importCount, totalCount); + + importInProgress = false; + } + }); + + LOGGER.debug("Starting import thread"); + + importThread.start(); + } + + @Override + public void stopImport() { + LOGGER.info("Stopping import"); + + // will break the loop + importInProgress = false; + // wait for the thread + if (importThread != null) { + try { + LOGGER.debug("Joining the import thread"); + importThread.join(); + } catch (InterruptedException e) { + throw new IllegalStateException("Interrupted while stopping case import", e); + } + } + + LOGGER.info("Import stopped"); + } + + @Override + public boolean isImportInProgress() { + return importInProgress; + } + + @Override + public CaseImportStatus importStatus() { + LOGGER.debug("Retrieving import status"); + + CaseImportStatus status = new CaseImportStatus(); + + status.setTotalCases(totalCount); + status.setCasesImported(importCount); + status.setLastImportDate(lastImportedDate); + status.setLastImportCaseId(lastImportedCaseId); + status.setImportInProgress(importInProgress); + + if (inError) { + status.setErrorMsg(errorMessage); + status.setError(true); + } + + return status; + } + + @Override + public void setFetchSize(int fetchSize) { + this.fetchSize = fetchSize; + } + + private void importCaseList(CasesInfo caseList, String configName) { + for (CaseInfo caseInfo : caseList.getCaseInfoList()) { + CaseEvent caseEvent = CaseEvent.fromCaseInfo(caseInfo, configName); + eventRelay.sendEventMessage(caseEvent.toMotechEventWithData()); + + lastImportedCaseId = caseInfo.getCaseId(); + lastImportedDate = caseInfo.getDateModified(); + importCount++; + + LOGGER.info("Imported case with ID: {}", caseInfo.getCaseId()); + } + } + + private void initForImport(Range dateRange, String configName) { + importInProgress = true; + importCount = 0; + lastImportedDate = null; + lastImportedCaseId = null; + totalCount = countForImport(dateRange, configName); + inError = false; + errorMessage = null; + // calculate the page number, since we are going backwards + pageCount = (int) Math.ceil((double) totalCount / fetchSize); + + LOGGER.debug("Initialized for import"); + } + + private void validateDateRange(Range dateRange) { + if (dateRange == null) { + throw new IllegalArgumentException("Date range cannot be null, provide an empty one for all results"); + } + + DateTime start = dateRange.getMin(); + DateTime end = dateRange.getMax(); + + if (start != null && end != null && end.isBefore(start)) { + throw new IllegalArgumentException( + String.format("Date range start must be before date range end. Got start - %s, end - %s", + start, end)); + } + } + + private String dateTimeToString(DateTime dateTime) { + if (dateTime == null) { + return null; + } else { + DateTimeFormatter formatter = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss"); + return formatter.print(dateTime); + } + } + + private void handleImportError(RuntimeException e, String configName) { + errorMessage = e.getMessage(); + inError = true; + + // stop import + importInProgress = false; + + FullCaseFailureEvent failureEvent = new FullCaseFailureEvent(configName, errorMessage); + eventRelay.sendEventMessage(failureEvent.toMotechEvent()); + + // Trigger a status message in the Admin UI + String msg = "Error while importing case: " + errorMessage; + FailedImportStatusMessageEvent statusMessageEvent = new FailedImportStatusMessageEvent(msg); + eventRelay.sendEventMessage(statusMessageEvent.toMotechEvent()); + } +} diff --git a/commcare/src/main/java/org/motechproject/commcare/pull/CommcareFormImporter.java b/commcare/src/main/java/org/motechproject/commcare/pull/CommcareFormImporter.java index d19ef76a5..460cc783f 100644 --- a/commcare/src/main/java/org/motechproject/commcare/pull/CommcareFormImporter.java +++ b/commcare/src/main/java/org/motechproject/commcare/pull/CommcareFormImporter.java @@ -17,28 +17,52 @@ public interface CommcareFormImporter { /** * Counts how many forms match the provided criteria. A single request to the form list * API will be made underneath, and the returned total count will be returned by this method. - * @param dateRange the date range for the import + * + * @param dateRange the date range for the import * @param configName the name of the configuration to use, if null is provided the default configuration * will be used * @return the total number of forms matching the provided criteria * @throws IllegalArgumentException if the date range is null or invalid (start date after end date) - * @throws IllegalStateException if there is an already ongoing import in progress + * @throws IllegalStateException if there is an already ongoing import in progress */ int countForImport(Range dateRange, String configName); + /** + * Checks if a form exists with select id. A single request to the form list + * API will be made underneath, and a boolean value will be returned by this method. + * + * @param formId the id of the form to be checked + * @param configName the name of the configuration to use, if null is provided the default configuration + * will be used + * @return the boolean value if there exists a form with the provided id + */ + boolean checkFormIdForImport(String formId, String configName); + /** * Initiates asynchronous import for the provided criteria. Forms will be fetched from Commcare by doing HTTP * request to the form list API. The default fetch size per request is 100, but that can be controlled using the * {@link #setFetchSize(int)} method. The import will run in a separate thread. An event will be fired for each * successfully imported form. If an error occurs, the import will be stopped. - * @param dateRange the date range for the import + * + * @param dateRange the date range for the import * @param configName the name of the configuration to use, if null is provided the default configuration * will be used * @throws IllegalArgumentException if the date range is null or invalid (start date after end date) - * @throws IllegalStateException if there is an already ongoing import in progress + * @throws IllegalStateException if there is an already ongoing import in progress */ void startImport(final Range dateRange, final String configName); + /** + * Initiates asynchronous import for the provided criteria. Forms will be fetched from Commcare by doing HTTP + * request to the form list API. An event will be fired after a + * successfully imported form. If an error occurs, the import will be stopped. + * + * @param formId the id of form to be imported + * @param configName the name of the configuration to use, if null is provided the default configuration + * will be used. + */ + void startImportById(final String formId, final String configName); + /** * Stops the ongoing import. The effect is not guaranteed to be immediate. */ @@ -46,6 +70,7 @@ public interface CommcareFormImporter { /** * Checks whether there is an ongoing import in progress. + * * @return true if there is an ongoing import, false otherwise */ boolean isImportInProgress(); @@ -55,6 +80,7 @@ public interface CommcareFormImporter { * the last import. {@link FormImportStatus#isImportInProgress()} can be used to check if the import is ongoing, * {@link FormImportStatus#isError()} can be checked to see if the import failed. The received_on date for the * last successfully imported form can be retrieved using {@link FormImportStatus#getLastImportDate()}. + * * @return the status object for the current or last import */ FormImportStatus importStatus(); @@ -63,6 +89,7 @@ public interface CommcareFormImporter { * Sets the fetch size for the queries to the Commcare form list API. In other words, sets how many forms * will be retrieved per request to the API. The default is 100. Setting a too large number might cause memory * issues, setting a too low number might result in a large number of HTTP requests. + * * @param fetchSize the fetch size for requests */ void setFetchSize(int fetchSize); diff --git a/commcare/src/main/java/org/motechproject/commcare/pull/CommcareFormImporterImpl.java b/commcare/src/main/java/org/motechproject/commcare/pull/CommcareFormImporterImpl.java index 616d03144..58bb0b6fe 100644 --- a/commcare/src/main/java/org/motechproject/commcare/pull/CommcareFormImporterImpl.java +++ b/commcare/src/main/java/org/motechproject/commcare/pull/CommcareFormImporterImpl.java @@ -7,10 +7,11 @@ import org.motechproject.commcare.domain.CommcareFormList; import org.motechproject.commcare.events.FullFormEvent; import org.motechproject.commcare.events.FullFormFailureEvent; -import org.motechproject.commcare.events.MalformedFormStatusMessageEvent; +import org.motechproject.commcare.events.FailedImportStatusMessageEvent; import org.motechproject.commcare.request.FormListRequest; import org.motechproject.commcare.service.CommcareFormService; import org.motechproject.commons.api.Range; +import org.motechproject.event.MotechEvent; import org.motechproject.event.listener.EventRelay; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -31,7 +32,7 @@ public class CommcareFormImporterImpl implements CommcareFormImporter { private int fetchSize = PAGE_SIZE_FOR_FETCH; private Thread importThread; - private boolean importInProgress = false; + private boolean importInProgress; private int importCount; private int totalCount; @@ -63,6 +64,20 @@ public int countForImport(Range dateRange, String configName) { return count; } + @Override + public boolean checkFormIdForImport(String formId, String configName) { + LOGGER.info("Checking form with id {} [config: {}]", formId, configName); + CommcareForm form = formService.retrieveForm(formId, configName); + if (form.getId() != null) { + LOGGER.info("Form with id {} exists", form.getId()); + return true; + } else { + LOGGER.info("Form with id {} doesnot exist", formId); + return false; + } + } + + @Override public void startImport(final Range dateRange, final String configName) { validateNoImportInProgress(); @@ -129,6 +144,22 @@ public void run() { importThread.start(); } + @Override + public void startImportById(final String formId, final String configName) { + LOGGER.debug("Initiating form import with form id {} [config: {}]", + formId, configName); + CommcareForm form = formService.retrieveForm(formId, configName); + form.getForm().addAttribute("app_id", form.getAppId()); + FullFormEvent formEvent = new FullFormEvent(form.getForm(), form.getReceivedOn(), form.getConfigName()); + eventRelay.sendEventMessage(formEvent.toMotechEvent()); + lastFormXMLNSToBeImported = formEvent.getAttributes().get("xmlns"); + lastImportedDate = form.getReceivedOn(); + lastImportedFormId = form.getId(); + totalCount = 1; + importCount = 1; + LOGGER.info("Imported form with id {}", formId); + } + @Override public void stopImport() { LOGGER.info("Stopping import"); @@ -196,11 +227,12 @@ private void initForImport(Range dateRange, String configName) { private void importFormList(CommcareFormList formList) { // iterate backwards for (CommcareForm form : Lists.reverse(formList.getObjects())) { + form.getForm().addAttribute("app_id", form.getAppId()); FullFormEvent formEvent = new FullFormEvent(form.getForm(), form.getReceivedOn(), form.getConfigName()); lastFormXMLNSToBeImported = formEvent.getAttributes().get("xmlns"); - - eventRelay.sendEventMessage(formEvent.toMotechEvent()); + MotechEvent motechEvent = formEvent.toMotechEvent(); + eventRelay.sendEventMessage(motechEvent); lastImportedDate = form.getReceivedOn(); lastImportedFormId = form.getId(); @@ -250,7 +282,7 @@ private void handleImportError(RuntimeException e, String configName) { // Trigger a status message in the Admin UI String msg = "Error while importing form: " + errorMessage; - MalformedFormStatusMessageEvent statusMessageEvent = new MalformedFormStatusMessageEvent(msg); + FailedImportStatusMessageEvent statusMessageEvent = new FailedImportStatusMessageEvent(msg); eventRelay.sendEventMessage(statusMessageEvent.toMotechEvent()); } } diff --git a/commcare/src/main/java/org/motechproject/commcare/request/json/CaseRequest.java b/commcare/src/main/java/org/motechproject/commcare/request/json/CaseRequest.java index ddaf58499..022c2a5c3 100644 --- a/commcare/src/main/java/org/motechproject/commcare/request/json/CaseRequest.java +++ b/commcare/src/main/java/org/motechproject/commcare/request/json/CaseRequest.java @@ -1,5 +1,7 @@ package org.motechproject.commcare.request.json; +import org.apache.http.client.utils.URIBuilder; + import java.util.ArrayList; import java.util.List; @@ -46,6 +48,32 @@ public String toQueryString() { return toQueryString(queryParams); } + public void addQueryParams(URIBuilder uriBuilder) { + if (userId != null) { + uriBuilder.addParameter("user_id", userId); + } + if (ownerId != null) { + uriBuilder.addParameter("owner_id", ownerId); + } + if (caseId != null) { + uriBuilder.addParameter("case_id", caseId); + } + if (type != null) { + uriBuilder.addParameter("type", type); + } + if (caseName != null) { + uriBuilder.addParameter("case_name", caseName); + } + if (dateModifiedStart != null) { + uriBuilder.addParameter("date_modified_start", dateModifiedStart); + } + if (dateModifiedEnd != null) { + uriBuilder.addParameter("date_modified_end", dateModifiedEnd); + } + + addOtherQueryParams(uriBuilder); + } + /** * Sets the ID of the user to {@code userId}. * diff --git a/commcare/src/main/java/org/motechproject/commcare/request/json/Request.java b/commcare/src/main/java/org/motechproject/commcare/request/json/Request.java index bb896d886..a3f0b7f85 100644 --- a/commcare/src/main/java/org/motechproject/commcare/request/json/Request.java +++ b/commcare/src/main/java/org/motechproject/commcare/request/json/Request.java @@ -1,6 +1,7 @@ package org.motechproject.commcare.request.json; import org.apache.commons.lang.StringUtils; +import org.apache.http.client.utils.URIBuilder; import java.util.List; @@ -88,4 +89,9 @@ public boolean equals(Object o) { public int hashCode() { return toQueryString().hashCode(); } + + public void addOtherQueryParams(URIBuilder uriBuilder) { + uriBuilder.addParameter("limit", String.valueOf(limit < 1 ? DEFAULT_LIMIT : limit)); + uriBuilder.addParameter("offset", String.valueOf(offset < 0 ? 0 : offset)); + } } diff --git a/commcare/src/main/java/org/motechproject/commcare/service/CommcareReportService.java b/commcare/src/main/java/org/motechproject/commcare/service/CommcareReportService.java new file mode 100644 index 000000000..a54ee38d6 --- /dev/null +++ b/commcare/src/main/java/org/motechproject/commcare/service/CommcareReportService.java @@ -0,0 +1,49 @@ +package org.motechproject.commcare.service; + +import org.motechproject.commcare.domain.report.ReportDataInfo; +import org.motechproject.commcare.domain.report.ReportsMetadataInfo; + +/** + * Responsible for interacting with CommCareHQ's Report Metadata and Data API. + */ +public interface CommcareReportService { + + /** + * Query CommCareHQ for all reports. + * + * @param configName the name of the configuration used for connecting to CommcareHQ, null means default configuration + * @return {@link ReportsMetadataInfo} object that contains reports metadata and the list of {@link ReportsMetadataInfo} objects representing reports + * found on the given CommcareHQ configuration + */ + ReportsMetadataInfo getReportsList(String configName); + + /** + * Same as {@link #getReportsList(String)} but uses default Commcare configuration. + */ + ReportsMetadataInfo getReportsList(); + + /** + * Query CommCareHQ for a report by its report id. + * + * @param reportId the id of the report on CommCareHQ + * @param configName the name of the configuration used for connecting to CommcareHQ, null means default configuration + * @return the ReportDataInfo object representing the state of the report data or null if that report does not exist. + */ + ReportDataInfo getReportById(String reportId, String configName); + + /** + * Query CommCareHQ for a report by its report id and using filters declared in report. + * + * @param reportId the id of the report on CommCareHQ + * @param configName the name of the configuration used for connecting to CommcareHQ, null means default configuration + * @param filters the list of filters able to use in request + * @return the ReportDataInfo object representing the state of the report data or null if that report does not exist. + */ + ReportDataInfo getReportByIdWithFilters(String reportId, String configName, String filters); + + /** + * Same as {@link #getReportById(String, String) getReportById} but uses default Commcare configuration. + */ + ReportDataInfo getReportById(String reportId); + +} diff --git a/commcare/src/main/java/org/motechproject/commcare/service/CommcareSchemaService.java b/commcare/src/main/java/org/motechproject/commcare/service/CommcareSchemaService.java index 0eef006c8..02640d8eb 100644 --- a/commcare/src/main/java/org/motechproject/commcare/service/CommcareSchemaService.java +++ b/commcare/src/main/java/org/motechproject/commcare/service/CommcareSchemaService.java @@ -2,6 +2,7 @@ import org.motechproject.commcare.domain.CommcareApplicationJson; import org.motechproject.commcare.domain.FormSchemaJson; +import org.motechproject.commcare.domain.report.ReportsMetadataInfo; import org.motechproject.commcare.tasks.builder.model.CaseTypeWithApplicationName; import org.motechproject.commcare.tasks.builder.model.FormWithApplicationName; @@ -57,4 +58,11 @@ public interface CommcareSchemaService { * @return Set of Forms with display names. */ Set getFormsWithApplicationName(String configName); + + /** + * Retrieves reports metadata by configuration name + * + * @return the list of matching reports metadata + */ + List getReportsMetadata(String configName); } diff --git a/commcare/src/main/java/org/motechproject/commcare/service/ReportActionService.java b/commcare/src/main/java/org/motechproject/commcare/service/ReportActionService.java new file mode 100644 index 000000000..59fad0393 --- /dev/null +++ b/commcare/src/main/java/org/motechproject/commcare/service/ReportActionService.java @@ -0,0 +1,19 @@ +package org.motechproject.commcare.service; + +/** + * This service is responsible for handling Commcare report actions in tasks. + */ +public interface ReportActionService { + + /** + * This task action allows to query the Commcare Report UCR API. + * When the report is queried, is parsed by MOTECH and a Received Report + * event is raised. + * + * @param configName the name of the configuration used for connecting to CommcareHQ + * @param reportId the Id of report + * @param reportName the name of report + * @param urlParsedFilters parsed filter fields used in requests url + */ + void queryReport (String configName, String reportId, String reportName, String urlParsedFilters); +} diff --git a/commcare/src/main/java/org/motechproject/commcare/service/ReportsMetadataDataService.java b/commcare/src/main/java/org/motechproject/commcare/service/ReportsMetadataDataService.java new file mode 100644 index 000000000..2a5125d29 --- /dev/null +++ b/commcare/src/main/java/org/motechproject/commcare/service/ReportsMetadataDataService.java @@ -0,0 +1,25 @@ +package org.motechproject.commcare.service; + +import org.motechproject.commcare.domain.report.ReportsMetadataInfo; +import org.motechproject.mds.annotations.Lookup; +import org.motechproject.mds.annotations.LookupField; +import org.motechproject.mds.service.MotechDataService; + +import java.util.List; + +/** + * Data service for the {@link ReportsMetadataInfo} class. Provides methods for managing instances of said class. + */ +public interface ReportsMetadataDataService extends MotechDataService { + + /** + * Returns a list of the reports that originate from the configuration with the given + * {@code configurationName}. + * + * @param configurationName the name of the configuration + * @return the list of matching reports + */ + @Lookup(name = "By Source configuration") + List bySourceConfiguration(@LookupField(name = "configName") String configurationName); + +} diff --git a/commcare/src/main/java/org/motechproject/commcare/service/impl/CommcareAppStructureServiceImpl.java b/commcare/src/main/java/org/motechproject/commcare/service/impl/CommcareAppStructureServiceImpl.java index 8e6e6a56b..4bbe61db3 100644 --- a/commcare/src/main/java/org/motechproject/commcare/service/impl/CommcareAppStructureServiceImpl.java +++ b/commcare/src/main/java/org/motechproject/commcare/service/impl/CommcareAppStructureServiceImpl.java @@ -83,8 +83,10 @@ private AppStructureResponseJson parseApplicationsFromResponse(String response, } private void setDomain(AppStructureResponseJson responseJson, String configName) { - for (CommcareApplicationJson application : responseJson.getApplications()) { - application.setConfigName(configName); + if (responseJson != null) { + for (CommcareApplicationJson application : responseJson.getApplications()) { + application.setConfigName(configName); + } } } } diff --git a/commcare/src/main/java/org/motechproject/commcare/service/impl/CommcareCaseServiceImpl.java b/commcare/src/main/java/org/motechproject/commcare/service/impl/CommcareCaseServiceImpl.java index a12f8e660..553c6a084 100644 --- a/commcare/src/main/java/org/motechproject/commcare/service/impl/CommcareCaseServiceImpl.java +++ b/commcare/src/main/java/org/motechproject/commcare/service/impl/CommcareCaseServiceImpl.java @@ -305,6 +305,7 @@ private CaseInfo populateCaseInfo(CaseJson caseResponse, String configName) { caseInfo.setCaseId(caseResponse.getCaseId()); caseInfo.setUserId(caseResponse.getUserId()); caseInfo.setConfigName(configName); + caseInfo.setDateModified(caseResponse.getDateModified()); return caseInfo; } diff --git a/commcare/src/main/java/org/motechproject/commcare/service/impl/CommcareFormServiceImpl.java b/commcare/src/main/java/org/motechproject/commcare/service/impl/CommcareFormServiceImpl.java index ca2849b03..172d44e9f 100644 --- a/commcare/src/main/java/org/motechproject/commcare/service/impl/CommcareFormServiceImpl.java +++ b/commcare/src/main/java/org/motechproject/commcare/service/impl/CommcareFormServiceImpl.java @@ -36,11 +36,14 @@ public CommcareFormServiceImpl(CommCareAPIHttpClient commcareHttpClient, Commcar @Override public CommcareForm retrieveForm(String id, String configName) { String returnJson = commcareHttpClient.formRequest(configService.getByName(configName).getAccountConfig(), id); - - CommcareForm form = FormAdapter.readJson(returnJson); - form.setConfigName(configName); - - return form; + CommcareForm form = new CommcareForm(); + if (("").equals(returnJson)){ + return form; + } else { + form = FormAdapter.readJson(returnJson); + form.setConfigName(configName); + return form; + } } @Override diff --git a/commcare/src/main/java/org/motechproject/commcare/service/impl/CommcareFormsEventParser.java b/commcare/src/main/java/org/motechproject/commcare/service/impl/CommcareFormsEventParser.java index 0489a9396..cb64bcad4 100644 --- a/commcare/src/main/java/org/motechproject/commcare/service/impl/CommcareFormsEventParser.java +++ b/commcare/src/main/java/org/motechproject/commcare/service/impl/CommcareFormsEventParser.java @@ -4,6 +4,8 @@ import com.google.common.collect.Multimap; import org.apache.commons.lang.StringUtils; import org.motechproject.commons.api.TasksEventParser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import java.util.HashMap; @@ -32,6 +34,7 @@ public class CommcareFormsEventParser implements TasksEventParser { public static final String PARSER_NAME = "CommcareForms"; private static final String ID = "id"; + private static final Logger LOGGER = LoggerFactory.getLogger(CommcareFormsEventParser.class); @Override public Map parseEventParameters(String subject, Map entryParameters) { @@ -54,7 +57,13 @@ public String parseEventSubject(String eventSubject, Map eventPa if (eventSubject.equals(FORMS_EVENT)) { String xmlns = (String) ((Map) eventParameters.get(ATTRIBUTES)).get("xmlns"); - return eventSubject.concat(".").concat(configName).concat(".").concat(xmlns); + String appId = (String) ((Map) eventParameters.get(ATTRIBUTES)).get("app_id"); + + if (appId == null) { + appId = ""; + LOGGER.warn("AppId for form is null. XMLNS: {}", xmlns); + } + return eventSubject.concat(".").concat(configName).concat(".").concat(xmlns).concat(appId); } return eventSubject.concat(".").concat(configName); diff --git a/commcare/src/main/java/org/motechproject/commcare/service/impl/CommcareReportServiceImpl.java b/commcare/src/main/java/org/motechproject/commcare/service/impl/CommcareReportServiceImpl.java new file mode 100644 index 000000000..e5002303a --- /dev/null +++ b/commcare/src/main/java/org/motechproject/commcare/service/impl/CommcareReportServiceImpl.java @@ -0,0 +1,141 @@ +package org.motechproject.commcare.service.impl; + +import com.google.common.reflect.TypeToken; +import org.motechproject.commcare.client.CommCareAPIHttpClient; +import org.motechproject.commcare.domain.CommcareMetadataInfo; +import org.motechproject.commcare.domain.CommcareMetadataJson; +import org.motechproject.commcare.domain.report.ReportMetadataInfo; +import org.motechproject.commcare.domain.report.ReportDataInfo; +import org.motechproject.commcare.domain.report.ReportDataResponseJson; +import org.motechproject.commcare.domain.report.ReportsMetadataInfo; +import org.motechproject.commcare.domain.report.ReportMetadataJson; +import org.motechproject.commcare.domain.report.ReportsMetadataResponseJson; +import org.motechproject.commcare.domain.report.constants.ColumnType; +import org.motechproject.commcare.domain.report.constants.FilterDataType; +import org.motechproject.commcare.domain.report.constants.FilterType; +import org.motechproject.commcare.service.CommcareConfigService; +import org.motechproject.commcare.service.CommcareReportService; +import org.motechproject.commons.api.json.MotechJsonReader; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.HashMap; + +/** + * A {@link CommcareReportService} that is responsible for interacting with CommCareHQ's Report Metadata and Data API. + */ +@Service +public class CommcareReportServiceImpl implements CommcareReportService { + + private CommCareAPIHttpClient commcareHttpClient; + private CommcareConfigService configService; + private MotechJsonReader motechJsonReader; + + @Autowired + public CommcareReportServiceImpl(CommCareAPIHttpClient commcareHttpClient, CommcareConfigService configService) { + this.commcareHttpClient = commcareHttpClient; + this.configService = configService; + this.motechJsonReader = new MotechJsonReader(); + } + + @Override + public ReportsMetadataInfo getReportsList(String configName) { + String response = commcareHttpClient.reportsListMetadataRequest(configService.getByName(configName).getAccountConfig()); + + ReportsMetadataResponseJson reportsMetadataResponseJson = parseReportsFromResponse(response); + + return new ReportsMetadataInfo(generateReportsFromReportsResponse(reportsMetadataResponseJson.getReports()), populateReportsMetadata(reportsMetadataResponseJson.getMetadata())); + } + + @Override + public ReportsMetadataInfo getReportsList() { + return getReportsList(null); + } + + @Override + public ReportDataInfo getReportById(String reportId, String configName) { + String response = commcareHttpClient.singleReportDataRequest(configService.getByName(configName).getAccountConfig(), + reportId); + + ReportDataResponseJson reportResponse = parseSingleReportFromResponse(response); + + return generateReportFromReportResponse(reportResponse); + } + + @Override + public ReportDataInfo getReportByIdWithFilters (String reportId, String configName, String filters) { + String response = commcareHttpClient.singleReportDataRequestWithFilters(configService.getByName(configName).getAccountConfig(), + reportId, filters); + ReportDataResponseJson reportResponse = parseSingleReportFromResponse(response); + + return generateReportFromReportResponse(reportResponse); + } + + @Override + public ReportDataInfo getReportById(String reportId) { + return getReportById(reportId, null); + } + + private ReportsMetadataResponseJson parseReportsFromResponse(String response) { + Type reportsResponseType = new TypeToken() { } .getType(); + Map adapters = new HashMap<>(); + adapters.put(ColumnType.class, new ColumnType.ColumnTypeDeserializer()); + adapters.put(FilterType.class, new FilterType.FilterTypeDeserializer()); + adapters.put(FilterDataType.class, new FilterDataType.FilterDataTypeDeserializer()); + return (ReportsMetadataResponseJson) motechJsonReader.readFromString(response, reportsResponseType, adapters); + } + + private ReportDataResponseJson parseSingleReportFromResponse(String response) { + Type reportResponseType = new TypeToken() { } .getType(); + return (ReportDataResponseJson) motechJsonReader.readFromString(response, reportResponseType); + } + + private ReportDataInfo generateReportFromReportResponse(ReportDataResponseJson reportResponse) { + return populateReportDataInfo(reportResponse); + } + + private List generateReportsFromReportsResponse(List reportResponses) { + List reportsInfoList = Collections.emptyList(); + + if (reportResponses != null) { + reportsInfoList = new ArrayList<>(); + for (ReportMetadataJson reportResponse : reportResponses) { + reportsInfoList.add(populateReportMetadataInfo(reportResponse)); + } + } + + return reportsInfoList; + } + + private ReportMetadataInfo populateReportMetadataInfo(ReportMetadataJson reportResponse) { + ReportMetadataInfo reportMetadataInfo = null; + + if (reportResponse != null) { + reportMetadataInfo = new ReportMetadataInfo(reportResponse.getId(), reportResponse.getTitle(), + reportResponse.getColumns(), reportResponse.getFilters()); + } + + return reportMetadataInfo; + } + + private ReportDataInfo populateReportDataInfo(ReportDataResponseJson reportResponse) { + ReportDataInfo reportDataInfo = null; + + if (reportResponse != null) { + reportDataInfo = new ReportDataInfo(reportResponse.getColumns(), reportResponse.getData(), + reportResponse.getNextPage(), reportResponse.getTotalRecords()); + } + + return reportDataInfo; + } + + private CommcareMetadataInfo populateReportsMetadata(CommcareMetadataJson metadataJson) { + return new CommcareMetadataInfo(metadataJson.getLimit(), metadataJson.getNextPageQueryString(), + metadataJson.getOffset(), metadataJson.getPreviousPageQueryString(), metadataJson.getTotalCount()); + } +} diff --git a/commcare/src/main/java/org/motechproject/commcare/service/impl/CommcareSchemaServiceImpl.java b/commcare/src/main/java/org/motechproject/commcare/service/impl/CommcareSchemaServiceImpl.java index 0bd592b81..416077b5e 100644 --- a/commcare/src/main/java/org/motechproject/commcare/service/impl/CommcareSchemaServiceImpl.java +++ b/commcare/src/main/java/org/motechproject/commcare/service/impl/CommcareSchemaServiceImpl.java @@ -3,8 +3,10 @@ import org.motechproject.commcare.domain.CommcareApplicationJson; import org.motechproject.commcare.domain.CommcareModuleJson; import org.motechproject.commcare.domain.FormSchemaJson; +import org.motechproject.commcare.domain.report.ReportsMetadataInfo; import org.motechproject.commcare.service.CommcareApplicationDataService; import org.motechproject.commcare.service.CommcareSchemaService; +import org.motechproject.commcare.service.ReportsMetadataDataService; import org.motechproject.commcare.tasks.builder.model.CaseTypeWithApplicationName; import org.motechproject.commcare.tasks.builder.model.FormWithApplicationName; import org.springframework.beans.factory.annotation.Autowired; @@ -27,6 +29,8 @@ public class CommcareSchemaServiceImpl implements CommcareSchemaService { private CommcareApplicationDataService commcareApplicationDataService; + private ReportsMetadataDataService reportsMetadataDataService; + @Override @Transactional public List getAllFormSchemas(String configName) { @@ -106,8 +110,19 @@ public List retrieveApplications(String configName) { return commcareApplicationDataService.bySourceConfiguration(configName); } + @Override + @Transactional + public List getReportsMetadata (String configName) { + return reportsMetadataDataService.bySourceConfiguration(configName); + } + @Autowired public void setCommcareApplicationDataService(CommcareApplicationDataService commcareApplicationDataService) { this.commcareApplicationDataService = commcareApplicationDataService; } + + @Autowired + public void setReportsMetadataDataService (ReportsMetadataDataService reportsMetadataDataService) { + this.reportsMetadataDataService = reportsMetadataDataService; + } } diff --git a/commcare/src/main/java/org/motechproject/commcare/service/impl/ReportActionServiceImpl.java b/commcare/src/main/java/org/motechproject/commcare/service/impl/ReportActionServiceImpl.java new file mode 100644 index 000000000..6379d4cdf --- /dev/null +++ b/commcare/src/main/java/org/motechproject/commcare/service/impl/ReportActionServiceImpl.java @@ -0,0 +1,44 @@ +package org.motechproject.commcare.service.impl; + +import org.motechproject.commcare.events.constants.EventDataKeys; +import org.motechproject.commcare.events.constants.EventSubjects; +import org.motechproject.commcare.service.CommcareReportService; +import org.motechproject.commcare.service.ReportActionService; +import org.motechproject.event.MotechEvent; +import org.motechproject.event.listener.EventRelay; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.Map; + +@Service("reportActionService") +public class ReportActionServiceImpl implements ReportActionService { + + private CommcareReportService commcareReportService; + private EventRelay eventRelay; + + @Autowired + public ReportActionServiceImpl (CommcareReportService commcareReportService, EventRelay eventRelay) { + this.commcareReportService = commcareReportService; + this.eventRelay = eventRelay; + } + + @Override + public void queryReport (String configName, String reportId, String reportName, String urlParsedFilters) { + + commcareReportService.getReportByIdWithFilters(reportId, configName, urlParsedFilters); + + eventRelay.sendEventMessage(new MotechEvent(EventSubjects.RECEIVED_REPORT + "." + configName + "." + reportId, + prepareReceiveReportParameters(reportId, reportName))); + } + + private Map prepareReceiveReportParameters (String reportId, String reportName) { + Map parameters = new HashMap<>(); + + parameters.put(EventDataKeys.REPORT_ID, reportId); + parameters.put(EventDataKeys.REPORT_NAME, reportName); + + return parameters; + } +} diff --git a/commcare/src/main/java/org/motechproject/commcare/tasks/CommcareActionProxyService.java b/commcare/src/main/java/org/motechproject/commcare/tasks/CommcareActionProxyService.java index a56f8397d..41758ae6c 100644 --- a/commcare/src/main/java/org/motechproject/commcare/tasks/CommcareActionProxyService.java +++ b/commcare/src/main/java/org/motechproject/commcare/tasks/CommcareActionProxyService.java @@ -30,4 +30,9 @@ public interface CommcareActionProxyService { * See {@link org.motechproject.commcare.service.QueryStockLedgerActionService#queryStockLedger(String, String, String, DateTime, DateTime, Map) queryStockLedger} */ void queryStockLedger(String configName, String caseId, String sectionId, DateTime startDate, DateTime endDate, Map extraData); + + /** + * See {@link org.motechproject.commcare.service.ReportActionService#queryReport(String, String, String, String)} + */ + void queryReport (String configName, String reportId, String reportName, String urlParsedFilters); } diff --git a/commcare/src/main/java/org/motechproject/commcare/tasks/builder/CaseActionBuilder.java b/commcare/src/main/java/org/motechproject/commcare/tasks/builder/CaseActionBuilder.java index 41f6c04c4..810506614 100644 --- a/commcare/src/main/java/org/motechproject/commcare/tasks/builder/CaseActionBuilder.java +++ b/commcare/src/main/java/org/motechproject/commcare/tasks/builder/CaseActionBuilder.java @@ -123,7 +123,7 @@ public List buildActions() { .setDisplayName(DisplayNames.OWNER_ID) .setKey(EventDataKeys.OWNER_ID) .setType(UNICODE.getValue()) - .setRequired(true) + .setRequired(false) .setOrder(order++); parameters.add(builder.createActionParameterRequest()); diff --git a/commcare/src/main/java/org/motechproject/commcare/tasks/builder/CaseTriggerBuilder.java b/commcare/src/main/java/org/motechproject/commcare/tasks/builder/CaseTriggerBuilder.java index fa664530b..8de696ada 100644 --- a/commcare/src/main/java/org/motechproject/commcare/tasks/builder/CaseTriggerBuilder.java +++ b/commcare/src/main/java/org/motechproject/commcare/tasks/builder/CaseTriggerBuilder.java @@ -7,6 +7,7 @@ import org.motechproject.commcare.service.CommcareSchemaService; import org.motechproject.tasks.contract.EventParameterRequest; import org.motechproject.tasks.contract.TriggerEventRequest; +import org.motechproject.tasks.domain.enums.ParameterType; import java.util.ArrayList; import java.util.HashSet; @@ -73,7 +74,7 @@ public List buildTriggers() { if (config.isEventStrategyFull()) { for (String caseProperty : module.getCaseProperties()) { - parameterRequests.add(new EventParameterRequest(caseProperty, caseProperty)); + parameterRequests.add(new EventParameterRequest(caseProperty, caseProperty, ParameterType.DATE.getValue())); } } @@ -99,7 +100,7 @@ private void addPartialCaseFields(List parameters) { parameters.add(new EventParameterRequest("commcare.userId", USER_ID)); parameters.add(new EventParameterRequest("commcare.apiKey", API_KEY)); parameters.add(new EventParameterRequest("commcare.caseAction", CASE_ACTION)); - parameters.add(new EventParameterRequest("commcare.dateModified", DATE_MODIFIED)); + parameters.add(new EventParameterRequest("commcare.dateModified", DATE_MODIFIED, ParameterType.DATE.getValue())); parameters.add(new EventParameterRequest("commcare.caseName", CASE_NAME)); parameters.add(new EventParameterRequest("commcare.ownerId", OWNER_ID)); } diff --git a/commcare/src/main/java/org/motechproject/commcare/tasks/builder/ChannelRequestBuilder.java b/commcare/src/main/java/org/motechproject/commcare/tasks/builder/ChannelRequestBuilder.java index c63226424..5386fd4e3 100644 --- a/commcare/src/main/java/org/motechproject/commcare/tasks/builder/ChannelRequestBuilder.java +++ b/commcare/src/main/java/org/motechproject/commcare/tasks/builder/ChannelRequestBuilder.java @@ -49,6 +49,7 @@ public ChannelRequest buildChannelRequest() { FormTriggerBuilder formTriggerBuilder = new FormTriggerBuilder(schemaService, configService); CaseTriggerBuilder caseTriggerBuilder = new CaseTriggerBuilder(schemaService, configService); + ReportTriggerBuilder reportTriggerBuilder = new ReportTriggerBuilder(schemaService, configService); CommonTriggerBuilder commonTriggerBuilder = new CommonTriggerBuilder(configService); // Actions @@ -56,9 +57,11 @@ public ChannelRequest buildChannelRequest() { QueryStockLedgerActionBuilder queryStockLedgerActionBuilder = new QueryStockLedgerActionBuilder(configService); CaseActionBuilder caseActionBuilder = new CaseActionBuilder(configService); FormActionBuilder formActionBuilder = new FormActionBuilder(schemaService, configService); + ReportActionBuilder reportActionBuilder = new ReportActionBuilder(schemaService, configService); List triggers = formTriggerBuilder.buildTriggers(); triggers.addAll(caseTriggerBuilder.buildTriggers()); + triggers.addAll(reportTriggerBuilder.buildTriggers()); triggers.addAll(commonTriggerBuilder.buildTriggers()); List actions = new ArrayList<>(); @@ -66,6 +69,7 @@ public ChannelRequest buildChannelRequest() { actions.addAll(queryStockLedgerActionBuilder.buildActions()); actions.addAll(caseActionBuilder.buildActions()); actions.addAll(formActionBuilder.buildActions()); + actions.addAll(reportActionBuilder.buildActions()); return new ChannelRequest(DISPLAY_NAME, bundleContext.getBundle().getSymbolicName(), bundleContext.getBundle().getVersion().toString(), null, triggers, actions); diff --git a/commcare/src/main/java/org/motechproject/commcare/tasks/builder/CommonTriggerBuilder.java b/commcare/src/main/java/org/motechproject/commcare/tasks/builder/CommonTriggerBuilder.java index 5ae7ff120..83f448911 100644 --- a/commcare/src/main/java/org/motechproject/commcare/tasks/builder/CommonTriggerBuilder.java +++ b/commcare/src/main/java/org/motechproject/commcare/tasks/builder/CommonTriggerBuilder.java @@ -5,6 +5,7 @@ import org.motechproject.commcare.service.CommcareConfigService; import org.motechproject.tasks.contract.EventParameterRequest; import org.motechproject.tasks.contract.TriggerEventRequest; +import org.motechproject.tasks.domain.enums.ParameterType; import java.util.ArrayList; import java.util.List; @@ -73,7 +74,7 @@ private List buildStockTransactionTriggers() { parameterRequests.add(new EventParameterRequest(DisplayNames.QUANTITY, QUANTITY)); parameterRequests.add(new EventParameterRequest(DisplayNames.SECTION_ID, SECTION_ID)); parameterRequests.add(new EventParameterRequest(DisplayNames.STOCK_ON_HAND, STOCK_ON_HAND)); - parameterRequests.add(new EventParameterRequest(DisplayNames.TRANSACTION_DATE, TRANSACTION_DATE)); + parameterRequests.add(new EventParameterRequest(DisplayNames.TRANSACTION_DATE, TRANSACTION_DATE, ParameterType.DATE.getValue())); parameterRequests.add(new EventParameterRequest(DisplayNames.TYPE, TYPE)); String displayName = DisplayNameHelper.buildDisplayName(DisplayNames.RETRIEVED_STOCK_TRANSACTION, @@ -133,7 +134,7 @@ private List buildFormStubTrigger() { for (Config config : configService.getConfigs().getConfigs()) { List parameterRequests = new ArrayList<>(); - parameterRequests.add(new EventParameterRequest("commcare.receivedOn", RECEIVED_ON)); + parameterRequests.add(new EventParameterRequest("commcare.receivedOn", RECEIVED_ON, ParameterType.DATE.getValue())); parameterRequests.add(new EventParameterRequest("commcare.formId", FORM_ID)); parameterRequests.add(new EventParameterRequest("commcare.caseIds", CASE_IDS, "LIST")); parameterRequests.add(new EventParameterRequest(CONFIG_NAME_KEY, CONFIG_NAME)); diff --git a/commcare/src/main/java/org/motechproject/commcare/tasks/builder/FormActionBuilder.java b/commcare/src/main/java/org/motechproject/commcare/tasks/builder/FormActionBuilder.java index d64c17245..6bef8e020 100644 --- a/commcare/src/main/java/org/motechproject/commcare/tasks/builder/FormActionBuilder.java +++ b/commcare/src/main/java/org/motechproject/commcare/tasks/builder/FormActionBuilder.java @@ -61,9 +61,10 @@ private List buildActionsForConfig(Config config) { String displayName = DisplayNameHelper.buildDisplayName(DisplayNames.SUBMIT_FORM, form.getFormName(), application.getApplicationName(), config.getName()); + String appId = (application.getCommcareAppId() == null) ? "" : application.getCommcareAppId(); ActionEventRequestBuilder actionBuilder = new ActionEventRequestBuilder() .setDisplayName(displayName) - .setSubject(EventSubjects.SUBMIT_FORM + "." + form.getXmlns() + "." + config.getName()) + .setSubject(EventSubjects.SUBMIT_FORM + "." + form.getXmlns() + appId + "." + config.getName()) .setActionParameters(parameters); actions.add(actionBuilder.createActionEventRequest()); } @@ -76,9 +77,10 @@ private List buildActionsForConfig(Config config) { private SortedSet buildActionParameters(FormSchemaJson form) { SortedSet parameters = new TreeSet<>(); int order = 0; + ActionParameterRequestBuilder builder; for (FormSchemaQuestionJson question : form.getQuestions()) { - ActionParameterRequestBuilder builder = new ActionParameterRequestBuilder(); + builder = new ActionParameterRequestBuilder(); String displayName = StringUtils.isBlank(question.getQuestionLabel()) ? question.getQuestionValue() : question.getQuestionLabel(); @@ -100,9 +102,21 @@ private SortedSet buildActionParameters(FormSchemaJson f parameters.add(builder.createActionParameterRequest()); } + parameters.add(addXmlnsParameter(form, order)); return parameters; } + private ActionParameterRequest addXmlnsParameter(FormSchemaJson form, int order) { + ActionParameterRequestBuilder builder = new ActionParameterRequestBuilder(); + builder.setValue(form.getXmlns()) + .setDisplayName("xmlns") + .setKey("xmlns") + .setOrder(order) + .setType(ParameterType.UNICODE.getValue()) + .setHidden(true); + return builder.createActionParameterRequest(); + } + private SortedSet convertCommcareoptionsToTaskActionOptions(List options) { SortedSet availableOptions = new TreeSet<>(); diff --git a/commcare/src/main/java/org/motechproject/commcare/tasks/builder/FormTriggerBuilder.java b/commcare/src/main/java/org/motechproject/commcare/tasks/builder/FormTriggerBuilder.java index f042f791a..06b140f98 100644 --- a/commcare/src/main/java/org/motechproject/commcare/tasks/builder/FormTriggerBuilder.java +++ b/commcare/src/main/java/org/motechproject/commcare/tasks/builder/FormTriggerBuilder.java @@ -9,6 +9,7 @@ import org.motechproject.commcare.service.CommcareSchemaService; import org.motechproject.tasks.contract.EventParameterRequest; import org.motechproject.tasks.contract.TriggerEventRequest; +import org.motechproject.tasks.domain.enums.ParameterType; import java.util.ArrayList; import java.util.List; @@ -99,8 +100,8 @@ private TriggerEventRequest buildTriggerForForm(Config config, CommcareApplicati String formName = form.getFormName(); String displayName = DisplayNameHelper.buildDisplayName(RECEIVED_FORM, formName, applicationName, config.getName()); - - return new TriggerEventRequest(displayName, FORMS_EVENT + "." + config.getName() + "." + form.getXmlns(), + String appId = (application.getCommcareAppId() != null) ? application.getCommcareAppId() : ""; + return new TriggerEventRequest(displayName, FORMS_EVENT + "." + config.getName() + "." + form.getXmlns() + appId, null, buildTriggerParameters(form), FORMS_EVENT); } @@ -110,7 +111,7 @@ private List buildTriggerParameters(FormSchemaJson form) addCaseFields(parameters); for (FormSchemaQuestionJson question : form.getQuestions()) { - parameters.add(new EventParameterRequest(question.getQuestionValue(), question.getQuestionValue())); + parameters.add(new EventParameterRequest(question.getQuestionValue(), question.getQuestionValue(), ParameterType.DATE.getValue())); } return parameters; @@ -128,8 +129,8 @@ private void addMetadataFields(List parameters) { parameters.add(new EventParameterRequest("commcare.form.field.deviceId", META_DEVICE_ID)); parameters.add(new EventParameterRequest("commcare.form.field.username", META_USERNAME)); parameters.add(new EventParameterRequest("commcare.form.field.appVersion", META_APP_VERSION)); - parameters.add(new EventParameterRequest("commcare.form.field.timeStart", META_TIME_START)); - parameters.add(new EventParameterRequest("commcare.form.field.timeEnd", META_TIME_END)); + parameters.add(new EventParameterRequest("commcare.form.field.timeStart", META_TIME_START, ParameterType.DATE.getValue())); + parameters.add(new EventParameterRequest("commcare.form.field.timeEnd", META_TIME_END, ParameterType.DATE.getValue())); parameters.add(new EventParameterRequest("commcare.field.configName", CONFIG_NAME)); } } diff --git a/commcare/src/main/java/org/motechproject/commcare/tasks/builder/ReportActionBuilder.java b/commcare/src/main/java/org/motechproject/commcare/tasks/builder/ReportActionBuilder.java new file mode 100644 index 000000000..12edd3af9 --- /dev/null +++ b/commcare/src/main/java/org/motechproject/commcare/tasks/builder/ReportActionBuilder.java @@ -0,0 +1,210 @@ +package org.motechproject.commcare.tasks.builder; + +import org.motechproject.commcare.config.Config; +import org.motechproject.commcare.domain.report.ReportMetadataFilter; +import org.motechproject.commcare.domain.report.ReportMetadataInfo; +import org.motechproject.commcare.domain.report.ReportsMetadataInfo; +import org.motechproject.commcare.domain.report.constants.FilterDataType; +import org.motechproject.commcare.domain.report.constants.FilterType; +import org.motechproject.commcare.events.constants.DisplayNames; +import org.motechproject.commcare.events.constants.EventDataKeys; +import org.motechproject.commcare.events.constants.EventSubjects; +import org.motechproject.commcare.service.CommcareConfigService; +import org.motechproject.commcare.service.CommcareSchemaService; +import org.motechproject.tasks.contract.ActionEventRequest; +import org.motechproject.tasks.contract.ActionParameterRequest; +import org.motechproject.tasks.contract.builder.ActionEventRequestBuilder; +import org.motechproject.tasks.contract.builder.ActionParameterRequestBuilder; +import org.motechproject.tasks.domain.enums.ParameterType; + +import java.util.ArrayList; +import java.util.List; +import java.util.SortedSet; +import java.util.TreeSet; + +/** + * Class responsible for building the Task module actions, related to sending the + * forms via submission API. The built list of {@link ActionEventRequest}s instances can + * be passed to the Task module to register channel actions. + */ +public class ReportActionBuilder implements ActionBuilder { + + private CommcareSchemaService schemaService; + private CommcareConfigService configService; + + public ReportActionBuilder (CommcareSchemaService schemaService, CommcareConfigService configService) { + this.schemaService = schemaService; + this.configService = configService; + } + + @Override + public List buildActions () { + List actions = new ArrayList<>(); + + for (Config config : configService.getConfigs().getConfigs()) { + List builtActions = buildActionsForConfigs(config); + actions.addAll(builtActions); + } + + return actions; + } + + private List buildActionsForConfigs (Config config) { + List actions = new ArrayList<>(); + + for (ReportsMetadataInfo reportsMetadataInfo : schemaService.getReportsMetadata(config.getName())) { + for (ReportMetadataInfo reportMetadata : reportsMetadataInfo.getReportMetadataInfoList()) { + SortedSet parameters = buildActionParameters(reportMetadata); + + parameters.addAll(prepareCommonParameters(parameters.size(), reportMetadata.getId(), reportMetadata.getTitle())); + + String displayName = DisplayNameHelper.buildDisplayName(DisplayNames.CREATE_REPORT, reportMetadata.getTitle(), + config.getName()); + ActionEventRequestBuilder actionBuilder = new ActionEventRequestBuilder() + .setDisplayName(displayName) + .setSubject(EventSubjects.REPORT_EVENT + "." + reportMetadata.getTitle() + "." + config.getName()) + .setActionParameters(parameters); + + actions.add(actionBuilder.createActionEventRequest()); + } + } + + return actions; + } + + private SortedSet buildActionParameters (ReportMetadataInfo reportMetadata) { + SortedSet parameters = new TreeSet<>(); + int order = 0; + + for (ReportMetadataFilter filter : reportMetadata.getFilters()) { + if (filter.getDatatype().equals(FilterDataType.DECIMAL) || filter.getDatatype().equals(FilterDataType.INTEGER)) { + parameters.addAll(prepareNumericParameters(filter, order)); + order = parameters.size(); + } else if (filter.getType().equals(FilterType.DATE)) { + parameters.addAll(prepareDateParameters(filter, order)); + order = parameters.size(); + } else { + parameters.addAll(prepareUnicodeParameters(filter, order)); + order = parameters.size(); + } + } + + return parameters; + } + + private SortedSet prepareNumericParameters (ReportMetadataFilter filter, int startOrder) { + SortedSet parameters = new TreeSet<>(); + int order = startOrder; + + ActionParameterRequestBuilder builder = new ActionParameterRequestBuilder() + .setDisplayName(filter.getSlug()) + .setKey(filter.getSlug()) + .setType(ParameterType.SELECT.getValue()) + .setOptions(createNumericFilterOperators()) + .setOrder(order++); + + parameters.add(builder.createActionParameterRequest()); + + builder = new ActionParameterRequestBuilder() + .setDisplayName(filter.getSlug() + " " + EventDataKeys.VALUE) + .setKey(filter.getSlug() + "." + EventDataKeys.VALUE) + .setType(ParameterType.UNICODE.getValue()) + .setOrder(order++); + + parameters.add(builder.createActionParameterRequest()); + + parameters.add(prepareFilterTypeParameter(filter, FilterDataType.DECIMAL.toString(), order++)); + + return parameters; + } + + private SortedSet prepareDateParameters (ReportMetadataFilter filter, int startOrder) { + SortedSet parameters = new TreeSet<>(); + int order = startOrder; + + ActionParameterRequestBuilder builder = new ActionParameterRequestBuilder() + .setDisplayName(filter.getSlug()) + .setKey(filter.getSlug()) + .setType(ParameterType.DATE.getValue()) + .setOrder(order++); + + parameters.add(builder.createActionParameterRequest()); + + builder = new ActionParameterRequestBuilder() + .setDisplayName(DisplayNames.END_DATE) + .setKey(filter.getSlug() + "." + DisplayNames.END_DATE) + .setType(ParameterType.DATE.getValue()) + .setOrder(order++); + + parameters.add(builder.createActionParameterRequest()); + + parameters.add(prepareFilterTypeParameter(filter, FilterType.DATE.toString(), order++)); + + return parameters; + } + + private SortedSet prepareUnicodeParameters (ReportMetadataFilter filter, int startOrder) { + SortedSet parameters = new TreeSet<>(); + int order = startOrder; + + ActionParameterRequestBuilder builder = new ActionParameterRequestBuilder() + .setDisplayName(filter.getSlug()) + .setKey(filter.getSlug()) + .setType(ParameterType.UNICODE.getValue()) + .setOrder(order++); + + parameters.add(builder.createActionParameterRequest()); + + parameters.add(prepareFilterTypeParameter(filter, FilterType.CHOICE_LIST.toString(), order++)); + + return parameters; + } + + private ActionParameterRequest prepareFilterTypeParameter (ReportMetadataFilter filter, String type, int order) { + return new ActionParameterRequestBuilder() + .setDisplayName(filter.getSlug() + "." + EventDataKeys.TYPE) + .setKey(filter.getSlug() + "." + EventDataKeys.TYPE) + .setValue(type) + .setHidden(true) + .setOrder(order) + .createActionParameterRequest(); + } + + private SortedSet prepareCommonParameters (int startOrder, String reportId, String reportName) { + SortedSet parameters = new TreeSet<>(); + int order = startOrder; + + ActionParameterRequestBuilder builder = new ActionParameterRequestBuilder() + .setDisplayName(EventDataKeys.REPORT_ID) + .setKey(EventDataKeys.REPORT_ID) + .setValue(reportId) + .setHidden(true) + .setOrder(order++); + + parameters.add(builder.createActionParameterRequest()); + + builder = new ActionParameterRequestBuilder() + .setDisplayName(EventDataKeys.REPORT_NAME) + .setKey(EventDataKeys.REPORT_NAME) + .setValue(reportName) + .setHidden(true) + .setOrder(order); + + parameters.add(builder.createActionParameterRequest()); + + return parameters; + } + + private SortedSet createNumericFilterOperators () { + SortedSet availableOperators = new TreeSet<>(); + + availableOperators.add("="); + availableOperators.add("!="); + availableOperators.add("<"); + availableOperators.add("<="); + availableOperators.add(">"); + availableOperators.add(">="); + + return availableOperators; + } +} diff --git a/commcare/src/main/java/org/motechproject/commcare/tasks/builder/ReportTriggerBuilder.java b/commcare/src/main/java/org/motechproject/commcare/tasks/builder/ReportTriggerBuilder.java new file mode 100644 index 000000000..0823d7285 --- /dev/null +++ b/commcare/src/main/java/org/motechproject/commcare/tasks/builder/ReportTriggerBuilder.java @@ -0,0 +1,80 @@ +package org.motechproject.commcare.tasks.builder; + +import org.motechproject.commcare.config.Config; +import org.motechproject.commcare.domain.report.ReportMetadataColumn; +import org.motechproject.commcare.domain.report.ReportMetadataInfo; +import org.motechproject.commcare.domain.report.ReportsMetadataInfo; +import org.motechproject.commcare.events.constants.EventSubjects; +import org.motechproject.commcare.service.CommcareConfigService; +import org.motechproject.commcare.service.CommcareSchemaService; +import org.motechproject.tasks.contract.EventParameterRequest; +import org.motechproject.tasks.contract.TriggerEventRequest; + +import java.util.ArrayList; +import java.util.List; + +import static org.motechproject.commcare.events.constants.EventDataKeys.CONFIG_NAME; + +/** + * The ReportTriggerBuilder class builds report triggers for each report + * present in MOTECH database. Each trigger has got its own attributes, depending on + * the case properties of each type. Some fields are common for all reports. + */ +public class ReportTriggerBuilder implements TriggerBuilder { + + private CommcareSchemaService schemaService; + private CommcareConfigService configService; + + private static final String RECEIVED_CASE = "Received Report"; + private static final String COLUMN_NAME_SUFFIX = " Column Name"; + + /** + * Creates an instance of the {@link ReportTriggerBuilder} class. It will use the given {@code schemaService} and + * {@code configService} for creating report triggers. + * + * @param schemaService the schema service + * @param configService the configuration service + */ + public ReportTriggerBuilder (CommcareSchemaService schemaService, CommcareConfigService configService) { + this.schemaService = schemaService; + this.configService = configService; + } + + @Override + public List buildTriggers () { + List triggers = new ArrayList<>(); + + for (Config config : configService.getConfigs().getConfigs()) { + triggers.addAll(buildTriggersForConfig(config)); + } + + return triggers; + } + + private List buildTriggersForConfig (Config config) { + List triggers = new ArrayList<>(); + + for (ReportsMetadataInfo reportMetadataInfo : schemaService.getReportsMetadata(config.getName())) { + for (ReportMetadataInfo reportMetadata : reportMetadataInfo.getReportMetadataInfoList()) { + String displayName = DisplayNameHelper.buildDisplayName(RECEIVED_CASE, reportMetadata.getTitle(), + config.getName()); + triggers.add(new TriggerEventRequest(displayName, EventSubjects.RECEIVED_REPORT + "." + config.getName() + "." + reportMetadata.getId(), + null, createTriggerParameters(reportMetadata))); + } + } + + return triggers; + } + + private List createTriggerParameters (ReportMetadataInfo reportMetadata) { + List parameters = new ArrayList<>(); + + parameters.add(new EventParameterRequest("commcare.field.configName", CONFIG_NAME)); + + for (ReportMetadataColumn column : reportMetadata.getColumns()) { + parameters.add(new EventParameterRequest(column.getDisplay() + COLUMN_NAME_SUFFIX, column.getDisplay())); + } + + return parameters; + } +} diff --git a/commcare/src/main/java/org/motechproject/commcare/tasks/impl/CommcareActionProxyServiceImpl.java b/commcare/src/main/java/org/motechproject/commcare/tasks/impl/CommcareActionProxyServiceImpl.java index ec57fbbf2..523cd8f9e 100644 --- a/commcare/src/main/java/org/motechproject/commcare/tasks/impl/CommcareActionProxyServiceImpl.java +++ b/commcare/src/main/java/org/motechproject/commcare/tasks/impl/CommcareActionProxyServiceImpl.java @@ -4,6 +4,7 @@ import org.motechproject.commcare.domain.CaseTask; import org.motechproject.commcare.service.CaseActionService; import org.motechproject.commcare.service.QueryStockLedgerActionService; +import org.motechproject.commcare.service.ReportActionService; import org.motechproject.commcare.service.imports.ImportFormActionService; import org.motechproject.commcare.tasks.CommcareActionProxyService; import org.springframework.beans.factory.annotation.Autowired; @@ -17,14 +18,17 @@ public class CommcareActionProxyServiceImpl implements CommcareActionProxyServic private CaseActionService caseActionService; private ImportFormActionService importFormActionService; private QueryStockLedgerActionService queryStockLedgerActionService; + private ReportActionService reportActionService; @Autowired public CommcareActionProxyServiceImpl(CaseActionService caseActionService, ImportFormActionService importFormActionService, - QueryStockLedgerActionService queryStockLedgerActionService) { + QueryStockLedgerActionService queryStockLedgerActionService, + ReportActionService reportActionService) { this.caseActionService = caseActionService; this.importFormActionService = importFormActionService; this.queryStockLedgerActionService = queryStockLedgerActionService; + this.reportActionService = reportActionService; } @@ -47,4 +51,9 @@ public void importForms(String configName, DateTime startDate, DateTime endDate) public void queryStockLedger(String configName, String caseId, String sectionId, DateTime startDate, DateTime endDate, Map extraData) { queryStockLedgerActionService.queryStockLedger(configName, caseId, sectionId, startDate, endDate, extraData); } + + @Override + public void queryReport (String configName, String reportId, String reportName, String urlParsedFilters) { + reportActionService.queryReport(configName, reportId, reportName, urlParsedFilters); + } } diff --git a/commcare/src/main/java/org/motechproject/commcare/web/CaseImportController.java b/commcare/src/main/java/org/motechproject/commcare/web/CaseImportController.java new file mode 100644 index 000000000..1c00615a7 --- /dev/null +++ b/commcare/src/main/java/org/motechproject/commcare/web/CaseImportController.java @@ -0,0 +1,93 @@ +package org.motechproject.commcare.web; + +import org.motechproject.commcare.pull.CommcareCaseImporter; +import org.motechproject.commcare.pull.CommcareCaseImporterFactory; +import org.motechproject.commcare.pull.CaseImportStatus; +import org.motechproject.commcare.web.domain.CaseImportRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.ResponseStatus; + +import javax.servlet.http.HttpSession; + +import static org.motechproject.commcare.util.Constants.HAS_MANAGE_COMMCARE_PERMISSION; +/** + * This controller is responsible for importing cases. + * Can initialize and start importing the cases and check the status of an ongoing import. + */ +@Controller +@RequestMapping("/case-import") +@PreAuthorize(HAS_MANAGE_COMMCARE_PERMISSION) +public class CaseImportController extends CommcareController{ + private static final Logger LOGGER = LoggerFactory.getLogger(CaseImportController.class); + + @Autowired + private CommcareCaseImporterFactory caseImporterFactory; + + /** + * Checks the status of the ongoing case import. If there is no import in progress currently, + * that will be reflected in the returned object. + * @return the status representation for case import + */ + @RequestMapping(value = "/status", method = RequestMethod.GET) + @ResponseBody + public CaseImportStatus checkImportStatus(HttpSession session) { + LOGGER.debug("Received import STATUS request"); + CommcareCaseImporter importer = caseImporterFactory.getImporter(session); + return importer.importStatus(); + } + + /** + * Initializes case import, on the UI this will trigger a modal with the number of cases. + * Under the covers this just does a case count for the provided request. + * @param importRequest the request for + * @return the number of cases that match this request + */ + @RequestMapping(value = "/init", method = RequestMethod.POST) + @ResponseBody + public int initImport(@RequestBody CaseImportRequest importRequest, HttpSession session) { + LOGGER.debug("Received import INIT request: {}", importRequest); + CommcareCaseImporter importer = caseImporterFactory.getImporter(session); + if (importRequest.getCaseId() == null) { + return importer.countForImport(importRequest.getDateRange(), importRequest.getConfig()); + } else { + return importer.checkCaseIdForImport(importRequest.getCaseId(), importRequest.getConfig()) ? 1 : 0; + } + } + + /** + * Starts a case import using the provided request. + * @param importRequest the request for the case import + * @return returns whether case imported by id is failed or not. + */ + @RequestMapping(value = "/start", method = RequestMethod.POST) + @ResponseStatus(HttpStatus.OK) + public void startImport(@RequestBody CaseImportRequest importRequest, HttpSession session) { + LOGGER.debug("Received import START request: {}", importRequest); + CommcareCaseImporter importer = caseImporterFactory.getImporter(session); + importer.startImport(importRequest.getDateRange(), importRequest.getConfig()); + } + + /** + * Starts a case import by the given case id. + * @param importRequest the request for the case import + * @return returns 0 if there is an error and 1 if there is no error. + */ + @RequestMapping(value = "/import-by-id", method = RequestMethod.POST) + @ResponseStatus(HttpStatus.OK) + public void startImportById(@RequestBody CaseImportRequest importRequest, HttpSession session) { + LOGGER.debug("Received import START request: {}", importRequest); + CommcareCaseImporter importer = caseImporterFactory.getImporter(session); + if (importRequest.getCaseId() != null) { + importer.importSingleCase(importRequest.getCaseId(), importRequest.getConfig()); + } + } +} diff --git a/commcare/src/main/java/org/motechproject/commcare/web/FormImportController.java b/commcare/src/main/java/org/motechproject/commcare/web/FormImportController.java index 3cf1cf1e9..82cce41ea 100644 --- a/commcare/src/main/java/org/motechproject/commcare/web/FormImportController.java +++ b/commcare/src/main/java/org/motechproject/commcare/web/FormImportController.java @@ -37,6 +37,7 @@ public class FormImportController extends CommcareController { /** * Checks the status of the ongoing form import. If there is no import in progress currently, * that will be reflected in the returned object. + * * @return the status representation for form import */ @RequestMapping(value = "/status", method = RequestMethod.GET) @@ -50,6 +51,7 @@ public FormImportStatus checkImportStatus(HttpSession session) { /** * Initializes form import, on the UI this will trigger a modal with the number of forms. * Under the covers this just does a form count for the provided request. + * * @param importRequest the request for * @return the number of forms that match this request */ @@ -58,11 +60,16 @@ public FormImportStatus checkImportStatus(HttpSession session) { public int initImport(@RequestBody FormImportRequest importRequest, HttpSession session) { LOGGER.debug("Received import INIT request: {}", importRequest); CommcareFormImporter importer = importerFactory.getImporter(session); - return importer.countForImport(importRequest.getDateRange(), importRequest.getConfig()); + if (importRequest.getFormId() == null) { + return importer.countForImport(importRequest.getDateRange(), importRequest.getConfig()); + } else { + return importer.checkFormIdForImport(importRequest.getFormId(), importRequest.getConfig()) ? 1 : 0; + } } /** * Starts a form import using the provided request. + * * @param importRequest the request for the form import */ @RequestMapping(value = "/start", method = RequestMethod.POST) @@ -70,6 +77,10 @@ public int initImport(@RequestBody FormImportRequest importRequest, HttpSession public void startImport(@RequestBody FormImportRequest importRequest, HttpSession session) { LOGGER.debug("Received import START request: {}", importRequest); CommcareFormImporter importer = importerFactory.getImporter(session); - importer.startImport(importRequest.getDateRange(), importRequest.getConfig()); + if (importRequest.getFormId() == null) { + importer.startImport(importRequest.getDateRange(), importRequest.getConfig()); + } else { + importer.startImportById(importRequest.getFormId(), importRequest.getConfig()); + } } } diff --git a/commcare/src/main/java/org/motechproject/commcare/web/FullFormController.java b/commcare/src/main/java/org/motechproject/commcare/web/FullFormController.java index b5ca34963..648eba1f4 100644 --- a/commcare/src/main/java/org/motechproject/commcare/web/FullFormController.java +++ b/commcare/src/main/java/org/motechproject/commcare/web/FullFormController.java @@ -4,7 +4,7 @@ import org.motechproject.commcare.domain.FormValueElement; import org.motechproject.commcare.events.FullFormEvent; import org.motechproject.commcare.events.FullFormFailureEvent; -import org.motechproject.commcare.events.MalformedFormStatusMessageEvent; +import org.motechproject.commcare.events.FailedImportStatusMessageEvent; import org.motechproject.commcare.exception.EndpointNotSupported; import org.motechproject.commcare.exception.FullFormParserException; import org.motechproject.commcare.parser.FullFormParser; @@ -17,6 +17,7 @@ import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseStatus; import javax.servlet.http.HttpServletRequest; @@ -42,17 +43,17 @@ public FullFormController(EventRelay eventRelay, CommcareConfigService configSer @RequestMapping @ResponseStatus(HttpStatus.OK) - public void receiveFormForDefaultConfig(@RequestBody String body, HttpServletRequest request) throws EndpointNotSupported { - doReceiveForm(body, request, configService.getDefault()); + public void receiveFormForDefaultConfig(@RequestBody String body, HttpServletRequest request, @RequestParam(value = "app_id", required = false) String appId) throws EndpointNotSupported { + doReceiveForm(body, request, configService.getDefault(), appId); } @RequestMapping(value = "/{configName}") @ResponseStatus(HttpStatus.OK) - public void receiveForm(@RequestBody String body, HttpServletRequest request) throws EndpointNotSupported { - doReceiveForm(body, request, configService.getByName(getConfigName(request))); + public void receiveForm(@RequestBody String body, HttpServletRequest request, @RequestParam(value = "app_id", required = false) String appId) throws EndpointNotSupported { + doReceiveForm(body, request, configService.getByName(getConfigName(request)), appId); } - private void doReceiveForm(String body, HttpServletRequest request, Config config) throws EndpointNotSupported { + private void doReceiveForm(String body, HttpServletRequest request, Config config, String appId) throws EndpointNotSupported { LOGGER.trace("Received request for mapping /forms: {}", body); @@ -64,6 +65,7 @@ private void doReceiveForm(String body, HttpServletRequest request, Config confi try { FormValueElement formValueElement = parser.parse(); + formValueElement.addAttribute("app_id", appId); FullFormEvent fullFormEvent = new FullFormEvent(formValueElement, request.getHeader("received-on"), config.getName()); @@ -80,7 +82,7 @@ private void handleError(Exception e, Config config) { eventRelay.sendEventMessage(failureEvent.toMotechEvent()); // publish a status message in the Admin module String msg = "Error while receiving a form from Commcare: " + e.getMessage(); - MalformedFormStatusMessageEvent statusMessageEvent = new MalformedFormStatusMessageEvent(msg); + FailedImportStatusMessageEvent statusMessageEvent = new FailedImportStatusMessageEvent(msg); eventRelay.sendEventMessage(statusMessageEvent.toMotechEvent()); } diff --git a/commcare/src/main/java/org/motechproject/commcare/web/SchemaController.java b/commcare/src/main/java/org/motechproject/commcare/web/SchemaController.java index 894cf513b..10ec1b75f 100644 --- a/commcare/src/main/java/org/motechproject/commcare/web/SchemaController.java +++ b/commcare/src/main/java/org/motechproject/commcare/web/SchemaController.java @@ -3,10 +3,12 @@ import org.motechproject.commcare.domain.CaseInfo; import org.motechproject.commcare.domain.CasesInfo; import org.motechproject.commcare.domain.CommcareApplicationJson; +import org.motechproject.commcare.domain.report.ReportsMetadataInfo; import org.motechproject.commcare.exception.ConfigurationNotFoundException; import org.motechproject.commcare.service.CommcareApplicationDataService; import org.motechproject.commcare.service.CommcareCaseService; import org.motechproject.commcare.service.CommcareConfigService; +import org.motechproject.commcare.service.ReportsMetadataDataService; import org.motechproject.commcare.web.domain.CasesRecords; import org.motechproject.commcare.web.domain.GridSettings; import org.springframework.beans.factory.annotation.Autowired; @@ -37,6 +39,9 @@ public class SchemaController extends CommcareController { @Autowired private CommcareApplicationDataService commcareApplicationDataService; + @Autowired + private ReportsMetadataDataService reportsMetadataDataService; + @Autowired private CommcareCaseService caseService; @@ -50,6 +55,13 @@ public List schema(@PathVariable String configName) { return commcareApplicationDataService.bySourceConfiguration(configName); } + @RequestMapping(value = "/reports/{configName}") + @ResponseBody + public List reports(@PathVariable String configName) { + validateConfig(configName); + return reportsMetadataDataService.bySourceConfiguration(configName); + } + @RequestMapping(value = "/caseList/{configName}") @ResponseBody public CasesRecords caseList(GridSettings settings, @PathVariable String configName) { diff --git a/commcare/src/main/java/org/motechproject/commcare/web/domain/CaseImportRequest.java b/commcare/src/main/java/org/motechproject/commcare/web/domain/CaseImportRequest.java new file mode 100644 index 000000000..2b90794fa --- /dev/null +++ b/commcare/src/main/java/org/motechproject/commcare/web/domain/CaseImportRequest.java @@ -0,0 +1,90 @@ +package org.motechproject.commcare.web.domain; + +import org.codehaus.jackson.annotate.JsonIgnore; +import org.joda.time.DateTime; +import org.motechproject.commons.api.Range; +import org.motechproject.commons.date.util.DateUtil; + +import java.io.Serializable; + +/** + * Represents a request for importing cases + */ +public class CaseImportRequest implements Serializable{ + private static final long serialVersionUID = 4392180915357501643L; + + /** + * To get cases modified after this date. + */ + private DateTime modifiedDateStart; + + /** + * To get cases modified before this date. + */ + private DateTime modifiedDateEnd; + + /** + * The name of the configuration for which to initiate the import. + */ + private String config; + + /** + * The uuid of the case to be imported. + */ + private String caseId; + /** + * @return date after which cases are modified. + */ + public DateTime getModifiedDateStart() { + return modifiedDateStart; + } + + /** + * @param modifiedDateStart date after which cases are modified. + */ + public void setModifiedDateStart(DateTime modifiedDateStart) { + this.modifiedDateStart = modifiedDateStart; + } + + /** + * @return date before which cases are modified. + */ + public DateTime getModifiedDateEnd() { + return modifiedDateEnd; + } + + /** + * @param modifiedDateEnd date before which cases are modified. + */ + public void setModifiedDateEnd(DateTime modifiedDateEnd) { + this.modifiedDateEnd = modifiedDateEnd; + } + + /** + * @return The name of the configuration for which to initiate the import. + */ + public String getConfig() { + return config; + } + + /** + * @param config The name of the configuration for which to initiate the import. + */ + public void setConfig(String config) { + this.config = config; + } + + public String getCaseId() { + return caseId; + } + + public void setCaseId(String caseId) { + this.caseId = caseId; + } + + @JsonIgnore + public Range getDateRange() { + return new Range<>(DateUtil.setTimeZoneUTC(modifiedDateStart), + DateUtil.setTimeZoneUTC(modifiedDateEnd)); + } +} diff --git a/commcare/src/main/java/org/motechproject/commcare/web/domain/FormImportRequest.java b/commcare/src/main/java/org/motechproject/commcare/web/domain/FormImportRequest.java index 5d3a51aeb..c03e5ea2b 100644 --- a/commcare/src/main/java/org/motechproject/commcare/web/domain/FormImportRequest.java +++ b/commcare/src/main/java/org/motechproject/commcare/web/domain/FormImportRequest.java @@ -24,12 +24,16 @@ public class FormImportRequest implements Serializable { */ private DateTime receivedOnEnd; + /** + * The id for form import. + */ + private String formId; + /** * The name of the configuration for which to initiate the import. */ private String config; - /** * @return the starting date for form import */ @@ -58,6 +62,20 @@ public void setReceivedOnEnd(DateTime receivedOnEnd) { this.receivedOnEnd = receivedOnEnd; } + /** + * @return the form id for form import + */ + public String getFormId() { + return formId; + } + + /** + * @param formId the id for form import + */ + public void setFormId(String formId) { + this.formId = formId; + } + /** * @return the name of the configuration for which to initiate the import */ @@ -72,6 +90,7 @@ public void setConfig(String config) { this.config = config; } + @JsonIgnore public Range getDateRange() { return new Range<>(DateUtil.setTimeZoneUTC(receivedOnStart), diff --git a/commcare/src/main/resources/META-INF/spring/blueprint.xml b/commcare/src/main/resources/META-INF/spring/blueprint.xml index 933701423..01fbbb5be 100644 --- a/commcare/src/main/resources/META-INF/spring/blueprint.xml +++ b/commcare/src/main/resources/META-INF/spring/blueprint.xml @@ -65,6 +65,9 @@ + + @@ -76,6 +79,9 @@ + + diff --git a/commcare/src/main/resources/velocity.templates/task-data-provider.vm b/commcare/src/main/resources/velocity.templates/task-data-provider.vm index 14c1d7669..e9553037e 100644 --- a/commcare/src/main/resources/velocity.templates/task-data-provider.vm +++ b/commcare/src/main/resources/velocity.templates/task-data-provider.vm @@ -71,7 +71,8 @@ }, { "displayName" : "commcare.form.field.receivedOn", - "fieldKey" : "receivedOn" + "fieldKey" : "receivedOn", + "type" : "DATE" }, { "displayName" : "commcare.form.field.metadata", @@ -128,7 +129,8 @@ }, { "displayName" : "commcare.case.field.dateClosed", - "fieldKey" : "dateClosed" + "fieldKey" : "dateClosed", + "type" : "DATE" }, { "displayName" : "commcare.case.field.domain", @@ -145,7 +147,8 @@ }, { "displayName" : "commcare.case.field.serverDateOpened", - "fieldKey" : "serverDateOpened" + "fieldKey" : "serverDateOpened", + "type" : "DATE" }, { "displayName" : "commcare.case.field.caseType", @@ -153,7 +156,8 @@ }, { "displayName" : "commcare.case.field.dateOpened", - "fieldKey" : "dateOpened" + "fieldKey" : "dateOpened", + "type" : "DATE" }, { "displayName" : "commcare.case.field.ownerId", @@ -171,7 +175,8 @@ #end { "displayName" : "commcare.case.field.serverDateModified", - "fieldKey" : "serverDateModified" + "fieldKey" : "serverDateModified", + "type" : "DATE" }, { "displayName" : "commcare.case.field.closed", @@ -268,7 +273,8 @@ }, { "displayName": "commcare.location.field.lastModified", - "fieldKey": "lastModified" + "fieldKey": "lastModified", + "type" : "DATE" }, { "displayName": "commcare.location.field.locationData", diff --git a/commcare/src/main/resources/webapp/css/commcare.css b/commcare/src/main/resources/webapp/css/commcare.css index df5b0c410..406eab35e 100644 --- a/commcare/src/main/resources/webapp/css/commcare.css +++ b/commcare/src/main/resources/webapp/css/commcare.css @@ -226,6 +226,46 @@ td.questionData .table { color: #224477; } +.report-schema .report-schema-group { + padding: 1px 7px 1px 15px; + line-height: 32px; + color: #005580; + background-color: #E4F1F9; + border: 1px solid #D1E6F9; + border-top: none; +} + +.report-schema .report-columns{ + padding-bottom: 12px; +} + +.report-schema .report-column{ + padding: 6px 20px; +} + +.report-schema .report-column button { + float: right; +} + +.report-schema .report-filters{ + padding-bottom: 12px; +} + +.report-schema .report-filters button{ + float: right; +} + +.report .modal-dialog .modal-content .modal-body p{ + font-weight: bold; + margin-left: 3px; +} + +.report .modal-dialog .modal-content .modal-body{ + padding: 15px; +} + + + .case-schema .case-schema-group { padding: 1px 7px 1px 15px; height: 37px; diff --git a/commcare/src/main/resources/webapp/index.html b/commcare/src/main/resources/webapp/index.html index 8ffdc019f..6b33119ad 100644 --- a/commcare/src/main/resources/webapp/index.html +++ b/commcare/src/main/resources/webapp/index.html @@ -5,17 +5,19 @@
  • {{msg('server.home')}}
  • {{msg('commcare')}}
  • {{msg('commcare.tab.settings')}}
  • -
  • {{msg('commcare.tab.import')}}
  • +
  • {{msg('commcare.tab.importForms')}}
  • +
  • {{msg('commcare.tab.importCases')}}
  • {{msg('commcare.tab.forms')}}
  • -
  • {{msg('commcare.tab.cases')}}
  • +
  • {{msg('commcare.tab.reports')}}
  • @@ -26,38 +28,4 @@
    - -
    - - -
    {{msg('commcare.filters')}}
    -
    -
    -
    - {{msg('commcare.caseName')}} -
    -
    - - - - -
    -
    -
    -
    - {{msg('commcare.dateModifiedRange')}} -
    - -
    -
    - -
    -
    -
    -
    -
    \ No newline at end of file diff --git a/commcare/src/main/resources/webapp/js/app.js b/commcare/src/main/resources/webapp/js/app.js index feaa7f7c8..fc7bdc254 100644 --- a/commcare/src/main/resources/webapp/js/app.js +++ b/commcare/src/main/resources/webapp/js/app.js @@ -27,8 +27,8 @@ } } }) - .state('commcare.import', { - url: '/import', + .state('commcare.importForms', { + url: '/importForms', parent: 'commcare', views: { 'commcareView': { @@ -37,6 +37,16 @@ } } }) + .state('commcare.importCases', { + url: '/importCases', + parent: 'commcare', + views: { + 'commcareView': { + templateUrl: '../commcare/resources/partials/importCases.html', + controller: 'CommcareImportCasesCtrl' + } + } + }) .state('commcare.forms', { url: '/forms', parent: 'commcare', @@ -56,6 +66,16 @@ controller: 'CommcareCaseSchemasCtrl' } } + }) + .state('commcare.reports', { + url: '/reports', + parent: 'commcare', + views: { + 'commcareView': { + templateUrl: '../commcare/resources/partials/reports.html', + controller: 'CommcareReportsCtrl' + } + } }); }]); }()); diff --git a/commcare/src/main/resources/webapp/js/controllers.js b/commcare/src/main/resources/webapp/js/controllers.js index a206890b0..d39ee05c7 100644 --- a/commcare/src/main/resources/webapp/js/controllers.js +++ b/commcare/src/main/resources/webapp/js/controllers.js @@ -47,11 +47,13 @@ $scope.receivedOnStart = null; $scope.receivedOnEnd = null; $scope.lastFormId = null; + $scope.formId = null; $scope.lastReceivedOn = null; $('#importCompleteAlert').fadeOut("slow"); $scope.byDateRange = false; - $scope.importOptions = ['all', 'byDateRange']; + $scope.byFormId = false; + $scope.importOptions = ['all', 'byDateRange', 'byFormId']; $scope.selectedImportOption = $scope.importOptions[0]; $scope.setImportOption = function (index) { $scope.resetImportRequest(); @@ -59,8 +61,13 @@ $scope.selectedImportOption = $scope.importOptions[index]; if ('byDateRange' === $scope.importOptions[index]) { $scope.byDateRange = true; + $scope.byFormId = false; + } else if ('byFormId' === $scope.importOptions[index]) { + $scope.byFormId = true; + $scope.byDateRange = false; } else { $scope.byDateRange = false; + $scope.byFormId = false; } }; @@ -109,7 +116,8 @@ $scope.importRequest = { "config": $scope.selectedConfig !== undefined? $scope.selectedConfig.name : '', "receivedOnStart": $scope.receivedOnStart, - "receivedOnEnd": $scope.receivedOnEnd + "receivedOnEnd": $scope.receivedOnEnd, + "formId": $scope.formId }; $scope.updateImportRequest = function (nameParameter, valueParameter) { @@ -129,6 +137,9 @@ case 'receivedOnEnd': $scope.importRequest.receivedOnEnd = normalizeDate(valueParameter); break; + case 'formId': + $scope.importRequest.formId = valueParameter; + break; case 'config': $scope.importRequest.config = valueParameter; break; @@ -142,7 +153,8 @@ $scope.importRequest = { "config": $scope.selectedConfig.name = $scope.selectedConfig !== undefined? $scope.selectedConfig.name : '', "receivedOnStart": null, - "receivedOnEnd": null + "receivedOnEnd": null, + "formId": null }; }; @@ -165,6 +177,7 @@ $scope.importFormsProgressShow = false; $('#importCommcareForms').modal('hide'); $scope.importFormsComplete = true; + $('#importCompleteAlert').fadeIn("slow"); clearInterval($scope.importStatusInterval); } @@ -218,6 +231,176 @@ }); }); + controllers.controller('CommcareImportCasesCtrl', function ($scope, $http, LoadingModal, ModalFactory) { + $scope.importOptions = ['all', 'byDateRange', 'byId']; + $scope.importCasesProgressShow = false; + $scope.importCasesComplete = false; + $scope.totalCases = 0; + $scope.casesImported = 0; + $scope.statusError = false; + $scope.errorMsg = 'Unknown error'; + $scope.importInProgress = false; + $scope.modifiedDateStart = null; + $scope.modifiedDateEnd = null; + $scope.caseId = null; + $scope.lastCaseId = null; + $scope.lastReceivedOn = null; + $('#importCompleteAlert').fadeOut("slow"); + + $scope.byDateRange = false; + $scope.byId = false; + $scope.importRequest = { + "config": $scope.selectedConfig !== undefined? $scope.selectedConfig.name : '', + "caseId": $scope.caseId, + "modifiedDateStart": $scope.modifiedDateStart, + "modifiedDateEnd": $scope.modifiedDateEnd + }; + $scope.selectedImportOption = $scope.importOptions[0]; + $scope.updateProgress = function () { + var percentage = Math.round(($scope.casesImported / $scope.totalCases) * 100); + percentage = !isNaN(percentage) && percentage !== Infinity && percentage >= 0 ? percentage : 0; + $('#commcareImportPercentage').text(percentage + '%').css({width: percentage + '%'}).attr('aria-valuenow', percentage); + }; + $scope.setImportOption = function (index) { + $scope.resetImportRequest(); + $scope.resetImportValues(); + $scope.selectedImportOption = $scope.importOptions[index]; + if ('byDateRange' === $scope.importOptions[index]) { + $scope.byDateRange = true; + } else { + $scope.byDateRange = false; + } + if ('byId' === $scope.importOptions[index]) { + $scope.byId = true; + } else { + $scope.byId = false; + } + }; + $scope.resetImportRequest = function () { + $scope.importRequest = { + "config": $scope.selectedConfig.name = $scope.selectedConfig !== undefined? $scope.selectedConfig.name : '', + "caseId": null, + "modifiedDateStart": null, + "modifiedDateEnd": null + }; + }; + $scope.resetImportValues = function () { + $scope.initImportComplete = false; + $scope.importCasesProgressShow = false; + $scope.importCasesComplete = false; + $scope.totalCases = 0; + $scope.casesImported = 0; + $scope.statusError = false; + $scope.importInProgress = false; + $('#importCompleteAlert').fadeOut("slow"); + }; + $scope.updateImportRequest = function (nameParameter, valueParameter) { + $scope.resetImportValues(); + var dValue, + normalizeDate = function (dateValue) { + var indexTValue = dateValue.indexOf('T'); + dValue = dateValue.replace(' ', ''); + dValue = dValue.replace(' ', ''); + return dValue; + }; + + switch (nameParameter) { + case 'modifiedDateStart': + $scope.importRequest.modifiedDateStart = normalizeDate(valueParameter); + break; + case 'modifiedDateEnd': + $scope.importRequest.modifiedDateEnd = normalizeDate(valueParameter); + break; + case 'config': + $scope.importRequest.config = valueParameter; + break; + case 'caseId': + $scope.importRequest.caseId = valueParameter; + break; + default: + break; + } + + }; + $scope.importCasesStart = function () { + $scope.updateImportRequest(); + $('#importCommcareCases').modal('show'); + $scope.initImport(); + }; + $scope.initImport = function () { + $http.post('../commcare/case-import/init', $scope.importRequest).success( function(data) { + $scope.totalCases = data; + $scope.initImportComplete = true; + }).error(function(data) { + $scope.importError(data); + }); + }; + $scope.closeImportCases = function () { + $('#importCommcareCases').modal('hide'); + $scope.importCasesProgressShow = false; + }; + $scope.importCasesContinue = function () { + $scope.updateProgress(); + if (!$scope.importCasesProgressShow) { + $scope.startImport(); + } + $scope.importCasesProgressShow = true; + }; + $scope.startImport = function () { + if (!$scope.importInProgress) { + var url = $scope.byId ? "../commcare/case-import/import-by-id" : "../commcare/case-import/start"; + $http.post(url, $scope.importRequest).success( function(data) { + $scope.importInProgress = true; + $scope.lastCaseId = null; + $scope.lastReceivedOn = null; + $scope.checkStatus(); + }).error(function(data) { + $scope.importError(data); + }); + } + }; + $scope.checkStatus = function () { + var getStatus = function () { + $http.get('../commcare/case-import/status').success(function(data) { + $scope.statusError = data.error; + if (data.casesImported > 0) { + $scope.casesImported = data.casesImported; + } + + $scope.lastCaseId = data.lastImportCaseId; + $scope.lastReceivedOn = data.lastImportDate; + + $scope.updateProgress(); + + if (data.casesImported === data.totalCases) { + $scope.importCasesProgressShow = false; + $('#importCommcareCases').modal('hide'); + $scope.importCasesComplete = true; + $('#importCompleteAlert').fadeIn("slow"); + clearInterval($scope.importStatusInterval); + } + + if (!data.error) { + $scope.importInProgress = data.importInProgress; + } else { + $scope.importError(data.errorMsg); + } + }).error(function(data) { + $scope.importError(data); + }); + }; + $scope.importStatusInterval = setInterval(function () { + getStatus(); + }, 2000); + }; + $scope.importError = function (msg) { + $scope.importCasesProgressShow = false; + $scope.errorMsg = msg; + clearInterval($scope.importStatusInterval); + $scope.statusError = true; + }; + }); + controllers.controller('CommcareSettingsCtrl', function ($scope, Configurations, ModalFactory, LoadingModal, $timeout) { $scope.eventStrategyOptions = [ 'minimal', 'partial', 'full' ]; @@ -625,7 +808,7 @@ $scope.downloadingCases = false; $scope.formatJson = function(jsonResponse) { - return JSON.stringify(jsonResponse, null,4); + return JSON.stringify(jsonResponse, null, 4); }; $scope.$watch('selectedConfig', function() { @@ -692,4 +875,39 @@ }; }); + controllers.controller('CommcareReportsCtrl', function ($scope, $http, Reports, LoadingModal) { + + $scope.reportError = false; + + $scope.formatJson = function(jsonResponse) { + return JSON.stringify(jsonResponse, null, 4); + }; + + $scope.$watch('selectedConfig', function() { + if (!$scope.$parent.selectedConfig) { + return; + } + if ($scope.$parent.selectedConfig === $scope.$parent.newlyCreatedConfig) { + return; + } + LoadingModal.open(); + + $scope.reports = Reports.query({name: $scope.selectedConfig.name}, function() { + $scope.reportError = false; + LoadingModal.close(); + }, function() { + $scope.reportError = true; + LoadingModal.close(); + }); + + LoadingModal.close(); + }); + + innerLayout({ + spacing_closed: 30, + east__minSize: 200, + east__maxSize: 350 + }); + }); + }()); diff --git a/commcare/src/main/resources/webapp/js/directives.js b/commcare/src/main/resources/webapp/js/directives.js index d3779db58..32d2b3d09 100644 --- a/commcare/src/main/resources/webapp/js/directives.js +++ b/commcare/src/main/resources/webapp/js/directives.js @@ -52,6 +52,7 @@ ngModel.$setViewValue(viewValue); }); scope.updateImportRequest('receivedOnStart', viewValue); + scope.updateImportRequest('modifiedDateStart', viewValue); }, onChangeMonthYear: function (year, month, inst) { var curDate = elem.datetimepicker("getDate"); @@ -114,6 +115,7 @@ ngModel.$setViewValue(viewValue); }); scope.updateImportRequest('receivedOnEnd', viewValue); + scope.updateImportRequest('modifiedDateEnd', viewValue); }, onChangeMonthYear: function (year, month, inst) { var curDate = $(this).datetimepicker("getDate"); diff --git a/commcare/src/main/resources/webapp/js/services.js b/commcare/src/main/resources/webapp/js/services.js index ce514a486..a3994f983 100644 --- a/commcare/src/main/resources/webapp/js/services.js +++ b/commcare/src/main/resources/webapp/js/services.js @@ -50,4 +50,13 @@ return $resource('../commcare/caseList/:name'); }); + services.factory('Reports', function($resource) { + return $resource('../commcare/reports/:name', {}, { + get: { + method: 'GET', + isArray: true + } + }); + }); + }()); diff --git a/commcare/src/main/resources/webapp/messages/messages.properties b/commcare/src/main/resources/webapp/messages/messages.properties index b77dbd26d..54e96a298 100644 --- a/commcare/src/main/resources/webapp/messages/messages.properties +++ b/commcare/src/main/resources/webapp/messages/messages.properties @@ -1,10 +1,12 @@ commcare=Commcare commcare.ok=OK commcare.tab.dataMapping=Data Mapping -commcare.tab.import=Import Forms +commcare.tab.importForms=Import Forms +commcare.tab.importCases=Import Cases commcare.tab.settings=Settings -commcare.tab.forms=Forms +commcare.tab.forms=Form Definitions commcare.tab.cases=Cases +commcare.tab.reports=Reports commcare.settings.section.accountSettings=Account Settings commcare.settings.section.eventForwarding=Event Forwarding @@ -45,7 +47,7 @@ commcare.save.failure=Failure while saving configuration. commcare.sync.success=Synchronization initiated successfully. -commcare.settings.tooltip.eventStrategy=There are three different strategies for case event forwarding\: minimal (default), partial and full. The minimal event includes only the case ID. The partial event passes in all of the basic case data that are not case specific. The full implementation will also include field values, which for some implementations could bloat the event. +commcare.settings.tooltip.eventStrategy=There are three different strategies for case event forwarding\: minimal , partial and full (default). The minimal event includes only the case ID. The partial event passes in all of the basic case data that are not case specific. The full implementation will also include field values, which for some implementations could bloat the event. commcare.settings.tooltip.forwardingRules=The column on the right displays the url's that will be sent to CommcareHQ as forwarding endpoint. If the forwarding API does not work in your instance, use these urls in CommcareHQ. commcare.select=Select @@ -60,11 +62,16 @@ commcare.view=View commcare.json=JSON commcare.close=Close commcare.importForms=Import Forms +commcare.importCases=Import Cases +commcare.importSingleCase=Import commcare.startImport=Start Import commcare.cancel=Cancel commcare.all=All +commcare.byId=By Case Id commcare.byDateRange=By Date Range +commcare.byFormId=By Form Id commcare.formsToImport=Forms to Import +commcare.casesToImport=Cases to Import commcare.verifyToSave=Successful verification is required to save configuration commcare.id=ID @@ -90,7 +97,6 @@ commcare.message=Message commcare.closeCase=Close Case commcare.receivedOn=Received On -commcare.formId=Form Id commcare.caseIds=Case Ids commcare.form.questions=Questions @@ -124,6 +130,7 @@ commcare.to=To commcare.startDateTime=Start DateTime commcare.endDateTime=End DateTime commcare.dateModifiedRange= Date Modified Range +commcare.formId = Form Id commcare.user.field.defaultPhoneNumber=Default phone number commcare.user.field.email=E-mail @@ -201,9 +208,26 @@ commcare.case.field.closed=Closed commcare.case.field.indices=Indices commcare.case.field.configName=Configuration Name +commcare.reports.reportDetails=Report Details +commcare.reports.columnsTitle=Columns +commcare.reports.filtersTitle=Filters +commcare.reports.title = Report Title + +commcare.report.columnTitle=Column Details +commcare.report.columnId=Column Id +commcare.report.columnDisplay=Display Name +commcare.report.columnType=Type + +commcare.report.filterTitle=Filters Details +commcare.report.filterDataType=Data Type +commcare.report.filterSlug=Slug +commcare.report.filterType=Type + commcare.error=Error commcare.error.cases=Unable to retrieve the list of cases commcare.error.schema=Unable to retrieve application schema +commcare.error.importCase=Unable to import the form with the given id +commcare.error.reports=Unable to retrieve reports metadata commcare.confirm.deleteConfig=Are you sure you want to delete this configuration? commcare.confirm.discardChanges=All changes will be lost! Continue? @@ -215,14 +239,27 @@ commcare.alert.info.noConfigurationsDefined=No configurations defined. Please cr commcare.ImportFormsReceivedBetweenPart1=Import Forms Received Between commcare.ImportFormsReceivedBetweenPart2= (Leave blank or import all forms) commcare.formsWillImported=forms will be imported. +commcare.formWillImported=form will be imported. commcare.importComplete=Import complete! +commcare.casesWillImported=cases will be imported. +commcare.caseWillImported=case will be imported. +commcare.retrievingCases=Retrieving the number of cases to import. +commcare.noCasesToImport=No cases were returned, nothing to import. +commcare.noCaseToImport=No case was returned, nothing to import. + commcare.import.error=There was an error while importing the forms: {0} commcare.import.lastForm=The following form was the last one successfully imported commcare.import.formId=Form Id: {0} commcare.import.receivedOn=Received on: {0} commcare.import.retry=Please try this import again starting from {0} commcare.formImport.imported=Imported {0} of {1} forms +commcare.import.formById =Checking for existence of form with id {0} + +commcare.import.caseId=Form Id: {0} +commcare.import.checkForCaseId=Checking for existence of case with id {0} +commcare.caseImport.imported=Imported {0} of {1} cases commcare.retrievingForms=Retrieving the number of forms to import. commcare.noFormsToImport=No forms were returned, nothing to import. +commcare.noFormToImport=No form was returned, nothing to import. diff --git a/commcare/src/main/resources/webapp/partials/cases.html b/commcare/src/main/resources/webapp/partials/cases.html deleted file mode 100644 index 1dc9365a4..000000000 --- a/commcare/src/main/resources/webapp/partials/cases.html +++ /dev/null @@ -1,48 +0,0 @@ -
    -
    -

     {{msg('commcare.alert.info.noConfigurationsDefined')}}

    -
    -
    - -
    - -
    - - - -
    - -
    -

    {{msg('commcare.alert.warning')}}

    - {{loadingError}} -
    - -
    -
    -
    -
    - - - -
    diff --git a/commcare/src/main/resources/webapp/partials/importCases.html b/commcare/src/main/resources/webapp/partials/importCases.html new file mode 100644 index 000000000..aa706d19b --- /dev/null +++ b/commcare/src/main/resources/webapp/partials/importCases.html @@ -0,0 +1,122 @@ +
    +
    +

     {{msg('commcare.alert.info.noConfigurationsDefined')}}

    +
    +
    + +
    +
    +
    + +
    + +
    +
    +
    +
    +
    +
    + + +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    +
    + +
    +
    +
    +
    + +
    +
    +

    {{msg('commcare.importComplete')}}

    +
    +
    + + +
    \ No newline at end of file diff --git a/commcare/src/main/resources/webapp/partials/importForms.html b/commcare/src/main/resources/webapp/partials/importForms.html index ef0f691cb..3e9834bc9 100644 --- a/commcare/src/main/resources/webapp/partials/importForms.html +++ b/commcare/src/main/resources/webapp/partials/importForms.html @@ -53,6 +53,12 @@

    {{msg('commcare.alert.warning')}}

    +
    + +
    + +
    +
    @@ -75,15 +81,24 @@

    {{msg('commcare.alert.warning')}}