diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..e99e37b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+build
+.idea
+.gradle
+*.iml
+*.ipr
+*.iws
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..8aebb9b
--- /dev/null
+++ b/README.md
@@ -0,0 +1,2 @@
+ctrl-pkw
+========
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..bf4569c
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,67 @@
+buildscript {
+ ext {
+ springBootVersion = '1.2.1.RELEASE'
+ }
+ repositories {
+ mavenCentral()
+ }
+ dependencies {
+ classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
+ }
+ configurations {
+ compile.exclude module: "spring-boot-starter-tomcat"
+ }
+}
+
+apply plugin: 'java'
+apply plugin: 'spring-boot'
+
+jar {
+ baseName = 'ctrl-pkw'
+ version = '0.0.1-SNAPSHOT'
+}
+
+sourceCompatibility = 1.8
+targetCompatibility = 1.8
+
+repositories {
+ mavenCentral()
+ mavenLocal()
+ maven { url "http://download.osgeo.org/webdav/geotools" }
+ maven { url "http://www.hibernatespatial.org/repository" }
+ //maven { url "http://repo.opengeo.org" }
+ maven { url "http://dev.mapfish.org/maven/repository/" }
+}
+
+dependencies {
+ compile("org.projectlombok:lombok:1.14.8")
+
+ compile("org.springframework.boot:spring-boot-starter-web")
+ compile("org.springframework.boot:spring-boot-starter-undertow")
+ compile("org.springframework.boot:spring-boot-starter-data-jpa")
+ compile("org.springframework.boot:spring-boot-starter-jersey")
+ compile("org.springframework.boot:spring-boot-starter-batch")
+ compile("org.springframework:spring-context-support")
+ compile("org.springframework.shell:spring-shell:1.1.0.RELEASE")
+ compile("org.springframework.plugin:spring-plugin-core:1.1.0.RELEASE")
+ compile("org.apache.commons:commons-lang3:3.3.2")
+
+ //compile('org.glassfish.jersey.ext:jersey-declarative-linking:2.16-SNAPSHOT')
+ compile('org.glassfish.jersey.ext:jersey-declarative-linking:2.15')
+ compile('org.glassfish.jersey.ext:jersey-bean-validation:2.15')
+ compile("com.fasterxml.jackson.datatype:jackson-datatype-joda:2.3.2")
+
+ compile("org.opengeo:geodb:0.7")
+ compile("com.h2database:h2:1.4.185")
+ compile("org.hibernate:hibernate-spatial:4.3")
+ compile("org.jadira.usertype:usertype.core:3.2.0.GA")
+
+ compile("com.datastax.cassandra:cassandra-driver-core:2.1.3")
+ compile("com.datastax.cassandra:cassandra-driver-mapping:2.1.3")
+
+ testCompile("junit:junit:4.10")
+ //testCompile("org.glassfish.jersey.test-framework.providers:jersey-test-framework-provider-bundle:2.13")
+ testCompile("org.springframework.boot:spring-boot-starter-test")
+ testCompile("org.assertj:assertj-core:1.7.0")
+ testCompile("org.springframework.batch:spring-batch-test")
+}
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..b1d2dba
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1 @@
+rootProject.name = 'ctrl-pkw'
diff --git a/src/main/java/pl/ctrlpkw/Application.java b/src/main/java/pl/ctrlpkw/Application.java
new file mode 100644
index 0000000..3d98998
--- /dev/null
+++ b/src/main/java/pl/ctrlpkw/Application.java
@@ -0,0 +1,37 @@
+package pl.ctrlpkw;
+
+import com.google.common.cache.CacheBuilder;
+import com.vividsolutions.jts.geom.GeometryFactory;
+import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.cache.CacheManager;
+import org.springframework.cache.annotation.EnableCaching;
+import org.springframework.cache.guava.GuavaCacheManager;
+import org.springframework.context.annotation.Bean;
+
+import java.util.concurrent.TimeUnit;
+
+@SpringBootApplication
+@EnableCaching
+@EnableBatchProcessing
+public class Application {
+
+ @Bean
+ public GeometryFactory geometryFactory() {
+ return new GeometryFactory();
+ }
+
+ @Bean
+ public CacheManager cacheManager() {
+ GuavaCacheManager cacheManager = new GuavaCacheManager();
+ cacheManager.setCacheBuilder(CacheBuilder.newBuilder()
+ .expireAfterWrite(60, TimeUnit.SECONDS).maximumSize(100));
+ return cacheManager;
+ }
+
+ public static void main(String[] args) {
+ SpringApplication.run(Application.class, args);
+ }
+
+}
diff --git a/src/main/java/pl/ctrlpkw/CassandraContext.java b/src/main/java/pl/ctrlpkw/CassandraContext.java
new file mode 100644
index 0000000..231a6f5
--- /dev/null
+++ b/src/main/java/pl/ctrlpkw/CassandraContext.java
@@ -0,0 +1,39 @@
+package pl.ctrlpkw;
+
+import com.datastax.driver.core.Cluster;
+import com.datastax.driver.core.Session;
+import com.datastax.driver.mapping.MappingManager;
+import lombok.Getter;
+import org.springframework.beans.factory.DisposableBean;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+@Component
+public class CassandraContext implements InitializingBean, DisposableBean {
+
+ @Getter
+ private Cluster cluster;
+
+ @Getter
+ private Session session;
+
+ @Getter
+ private MappingManager mappingManager;
+
+ @Value("${cassandra.contactPoint}")
+ private String contactPoint;
+
+ @Override
+ public void afterPropertiesSet() throws Exception {
+ cluster = Cluster.builder().addContactPoint(contactPoint).build();
+ session = cluster.connect();
+ mappingManager = new MappingManager(session);
+ }
+
+ @Override
+ public void destroy() throws Exception {
+ session.close();
+ cluster.close();
+ }
+}
diff --git a/src/main/java/pl/ctrlpkw/JerseyConfig.java b/src/main/java/pl/ctrlpkw/JerseyConfig.java
new file mode 100644
index 0000000..2274f28
--- /dev/null
+++ b/src/main/java/pl/ctrlpkw/JerseyConfig.java
@@ -0,0 +1,23 @@
+package pl.ctrlpkw;
+
+import pl.ctrlpkw.api.ObjectMapperProvider;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.server.ServerProperties;
+import org.springframework.stereotype.Component;
+
+import javax.ws.rs.ApplicationPath;
+
+@Component
+@ApplicationPath("/api")
+public class JerseyConfig extends ResourceConfig {
+
+ public JerseyConfig() {
+ packages("pl.ctrlpkw.api.resource");
+ register(ObjectMapperProvider.class);
+
+ //Declarative linking need the patched version of Jersey
+ //register(DeclarativeLinkingFeature.class);
+
+ property(ServerProperties.BV_SEND_ERROR_IN_RESPONSE, true);
+ }
+}
diff --git a/src/main/java/pl/ctrlpkw/api/LinkSerializer.java b/src/main/java/pl/ctrlpkw/api/LinkSerializer.java
new file mode 100644
index 0000000..7c6c20c
--- /dev/null
+++ b/src/main/java/pl/ctrlpkw/api/LinkSerializer.java
@@ -0,0 +1,21 @@
+package pl.ctrlpkw.api;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonSerializer;
+import com.fasterxml.jackson.databind.SerializerProvider;
+
+import javax.ws.rs.core.Link;
+import java.io.IOException;
+
+public class LinkSerializer extends JsonSerializer {
+
+ @Override
+ public void serialize(Link link, JsonGenerator jg, SerializerProvider sp)
+ throws IOException, JsonProcessingException {
+ jg.writeStartObject();
+ jg.writeStringField("rel", link.getRel());
+ jg.writeStringField("href", link.getUri().toString());
+ jg.writeEndObject();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/pl/ctrlpkw/api/ObjectMapperProvider.java b/src/main/java/pl/ctrlpkw/api/ObjectMapperProvider.java
new file mode 100644
index 0000000..5b4bbe0
--- /dev/null
+++ b/src/main/java/pl/ctrlpkw/api/ObjectMapperProvider.java
@@ -0,0 +1,38 @@
+package pl.ctrlpkw.api;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+import com.fasterxml.jackson.datatype.joda.JodaModule;
+
+import javax.ws.rs.core.Link;
+import javax.ws.rs.ext.ContextResolver;
+import javax.ws.rs.ext.Provider;
+
+@Provider
+public class ObjectMapperProvider implements ContextResolver {
+
+ final ObjectMapper defaultObjectMapper;
+
+ public ObjectMapperProvider() {
+ defaultObjectMapper = createDefaultMapper();
+ }
+
+ @Override
+ public ObjectMapper getContext(Class> type) {
+ return defaultObjectMapper;
+ }
+
+ private static ObjectMapper createDefaultMapper() {
+ final ObjectMapper mapper = new ObjectMapper();
+ SimpleModule simpleModule = new SimpleModule();
+ simpleModule.addSerializer(Link.class, new LinkSerializer());
+
+ mapper
+ .registerModule(simpleModule)
+ .registerModule(new JodaModule())
+ .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
+ return mapper;
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/pl/ctrlpkw/api/dto/Ballot.java b/src/main/java/pl/ctrlpkw/api/dto/Ballot.java
new file mode 100644
index 0000000..e2ea73f
--- /dev/null
+++ b/src/main/java/pl/ctrlpkw/api/dto/Ballot.java
@@ -0,0 +1,38 @@
+package pl.ctrlpkw.api.dto;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.experimental.Builder;
+import pl.ctrlpkw.api.resource.BallotsResource;
+import org.glassfish.jersey.linking.Binding;
+import org.glassfish.jersey.linking.InjectLink;
+import org.glassfish.jersey.linking.InjectLinks;
+import org.joda.time.LocalDate;
+
+import javax.ws.rs.core.Link;
+import java.util.*;
+
+@Getter
+@Setter
+@Builder
+public class Ballot {
+
+ @JsonIgnore
+ private LocalDate votingDate;
+
+ private Integer no;
+
+ private String question;
+
+ private List options;
+
+ @InjectLinks({
+ @InjectLink(resource = BallotsResource.class, rel = "self", method = "readOne", style = InjectLink.Style.ABSOLUTE,
+ bindings = { @Binding(name = "date", value = "${instance.votingDate}") }
+ )
+ })
+ private List links;
+
+
+}
diff --git a/src/main/java/pl/ctrlpkw/api/dto/BallotResult.java b/src/main/java/pl/ctrlpkw/api/dto/BallotResult.java
new file mode 100644
index 0000000..9aac2ae
--- /dev/null
+++ b/src/main/java/pl/ctrlpkw/api/dto/BallotResult.java
@@ -0,0 +1,43 @@
+package pl.ctrlpkw.api.dto;
+
+import com.google.common.collect.Sets;
+//import com.wordnik.swagger.annotations.ApiModel;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import lombok.experimental.Builder;
+
+import javax.validation.Valid;
+import javax.validation.constraints.NotNull;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+//@ApiModel
+@Getter
+@Setter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class BallotResult {
+
+ @NotNull
+ private Integer ballotNo;
+
+ @NotNull
+ private Long votersEntitledCount;
+
+ @NotNull
+ private Long ballotsGivenCount;
+
+ @NotNull
+ private Long votesCastCount;
+
+ @NotNull
+ private Long votesValidCount;
+
+ private List votesCountPerOption;
+
+}
diff --git a/src/main/java/pl/ctrlpkw/api/dto/Location.java b/src/main/java/pl/ctrlpkw/api/dto/Location.java
new file mode 100644
index 0000000..4c10f3b
--- /dev/null
+++ b/src/main/java/pl/ctrlpkw/api/dto/Location.java
@@ -0,0 +1,20 @@
+package pl.ctrlpkw.api.dto;
+
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import lombok.experimental.Builder;
+
+@Getter
+@Setter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class Location {
+
+ private Double latitude;
+ private Double longitude;
+
+}
diff --git a/src/main/java/pl/ctrlpkw/api/dto/Protocol.java b/src/main/java/pl/ctrlpkw/api/dto/Protocol.java
new file mode 100644
index 0000000..470b9f5
--- /dev/null
+++ b/src/main/java/pl/ctrlpkw/api/dto/Protocol.java
@@ -0,0 +1,33 @@
+package pl.ctrlpkw.api.dto;
+
+//import com.wordnik.swagger.annotations.ApiModel;
+//import com.wordnik.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import lombok.experimental.Builder;
+
+import javax.validation.Valid;
+import javax.validation.constraints.NotNull;
+import java.util.Collection;
+
+@Getter
+@Setter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+//@ApiModel
+public class Protocol {
+
+ @NotNull
+ private String communityCode;
+
+ @NotNull
+ private Integer wardNo;
+
+ @Valid
+ private Collection ballotResults;
+
+
+}
diff --git a/src/main/java/pl/ctrlpkw/api/dto/Voting.java b/src/main/java/pl/ctrlpkw/api/dto/Voting.java
new file mode 100644
index 0000000..cead3f8
--- /dev/null
+++ b/src/main/java/pl/ctrlpkw/api/dto/Voting.java
@@ -0,0 +1,33 @@
+package pl.ctrlpkw.api.dto;
+
+//import com.wordnik.swagger.annotations.ApiModel;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.experimental.Builder;
+import pl.ctrlpkw.api.resource.BallotsResource;
+import pl.ctrlpkw.api.resource.VotingResource;
+import pl.ctrlpkw.api.resource.WardsResource;
+import org.glassfish.jersey.linking.InjectLink;
+import org.glassfish.jersey.linking.InjectLinks;
+import org.joda.time.LocalDate;
+
+import javax.ws.rs.core.Link;
+import java.util.List;
+
+@Getter
+@Setter
+@Builder
+public class Voting {
+
+ private String description;
+
+ private LocalDate date;
+
+ @InjectLinks({
+ @InjectLink(resource = VotingResource.class, rel = "self", method = "readOne", style = InjectLink.Style.ABSOLUTE),
+ @InjectLink(resource = BallotsResource.class, rel = "ballots", method = "readAll", style = InjectLink.Style.ABSOLUTE),
+ @InjectLink(resource = WardsResource.class, rel = "wards", method = "readAll", style = InjectLink.Style.ABSOLUTE)
+ })
+ private List links;
+
+}
diff --git a/src/main/java/pl/ctrlpkw/api/dto/Ward.java b/src/main/java/pl/ctrlpkw/api/dto/Ward.java
new file mode 100644
index 0000000..c6a9cf5
--- /dev/null
+++ b/src/main/java/pl/ctrlpkw/api/dto/Ward.java
@@ -0,0 +1,24 @@
+package pl.ctrlpkw.api.dto;
+
+//import com.wordnik.swagger.annotations.ApiModel;
+//import com.wordnik.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import lombok.experimental.Builder;
+
+@Getter
+@Setter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+//@ApiModel
+public class Ward {
+
+ private String communityCode;
+ private Integer no;
+ private String address;
+ private Location location;
+
+}
diff --git a/src/main/java/pl/ctrlpkw/api/resource/BallotsResource.java b/src/main/java/pl/ctrlpkw/api/resource/BallotsResource.java
new file mode 100644
index 0000000..fc9d802
--- /dev/null
+++ b/src/main/java/pl/ctrlpkw/api/resource/BallotsResource.java
@@ -0,0 +1,52 @@
+package pl.ctrlpkw.api.resource;
+
+//import com.wordnik.swagger.annotations.Api;
+//import com.wordnik.swagger.annotations.ApiOperation;
+import pl.ctrlpkw.model.read.BallotsRepository;
+import org.joda.time.LocalDate;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+import javax.transaction.Transactional;
+import javax.ws.rs.*;
+import javax.ws.rs.core.MediaType;
+import java.util.Collection;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
+
+@Path("/votings/{date}/ballots")
+@Produces(MediaType.APPLICATION_JSON)
+@Component
+public class BallotsResource {
+
+ @Resource
+ private BallotsRepository ballotsRepository;
+
+ @GET
+ @Transactional
+ public Collection readAll(@PathParam("date") String votingDate) {
+ return StreamSupport.stream(ballotsRepository.findByDate(LocalDate.parse(votingDate)).spliterator(), false)
+ .map(entityToDto)
+ .collect(Collectors.toList());
+ }
+
+ @GET
+ @Path("/{no}")
+ @Transactional
+ public pl.ctrlpkw.api.dto.Ballot readOne(@PathParam("date") String votingDate, @PathParam("no") Integer ballotNo) {
+ return entityToDto.apply(ballotsRepository.findByDateAndNo(LocalDate.parse(votingDate), ballotNo));
+ }
+
+ private static Function entityToDto = entity -> pl.ctrlpkw.api.dto.Ballot.builder()
+ .votingDate(entity.getVoting().getDate())
+ .no(entity.getNo())
+ .question(entity.getQuestion())
+ .options(
+ entity.getOptions()
+ .stream()
+ .map(option -> option.getDescription())
+ .collect(Collectors.toList())
+ ).build();
+
+}
diff --git a/src/main/java/pl/ctrlpkw/api/resource/ProtocolsResource.java b/src/main/java/pl/ctrlpkw/api/resource/ProtocolsResource.java
new file mode 100644
index 0000000..c297b8b
--- /dev/null
+++ b/src/main/java/pl/ctrlpkw/api/resource/ProtocolsResource.java
@@ -0,0 +1,54 @@
+package pl.ctrlpkw.api.resource;
+
+import com.datastax.driver.mapping.Mapper;
+import com.google.common.base.Optional;
+import com.google.common.collect.Sets;
+//import com.wordnik.swagger.annotations.Api;
+//import com.wordnik.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import pl.ctrlpkw.CassandraContext;
+import pl.ctrlpkw.api.dto.BallotResult;
+import pl.ctrlpkw.model.write.Ballot;
+import pl.ctrlpkw.model.write.Protocol;
+import pl.ctrlpkw.model.write.Ward;
+import org.joda.time.LocalDate;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+import javax.validation.Valid;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import java.util.UUID;
+
+@Path("/votings/{date}/protocols")
+@Produces(MediaType.APPLICATION_JSON)
+@Component
+@Slf4j
+public class ProtocolsResource {
+
+ @Resource
+ CassandraContext cassandraContext;
+
+ @POST
+ public void create(@Valid pl.ctrlpkw.api.dto.Protocol protocol) {
+ for (BallotResult ballotResult : Optional.fromNullable(protocol.getBallotResults()).or(Sets.newHashSet())) {
+ Protocol localBallotResult = Protocol.builder()
+ .id(UUID.randomUUID())
+ .ballot(Ballot.builder().votingDate(LocalDate.parse("2010-06-20").toDate()).no(ballotResult.getBallotNo()).build())
+ .ward(Ward.builder().communityCode(protocol.getCommunityCode()).no(protocol.getWardNo()).build())
+ .votersEntitledCount(ballotResult.getVotersEntitledCount())
+ .ballotsGivenCount(ballotResult.getBallotsGivenCount())
+ .votesCastCount(ballotResult.getVotesCastCount())
+ .votesValidCount(ballotResult.getVotesValidCount())
+ .votesCountPerOption(
+ ballotResult.getVotesCountPerOption()
+ )
+ .build();
+ Mapper mapper = cassandraContext.getMappingManager().mapper(Protocol.class);
+ mapper.save(localBallotResult);
+ }
+ }
+
+}
diff --git a/src/main/java/pl/ctrlpkw/api/resource/ResultsResource.java b/src/main/java/pl/ctrlpkw/api/resource/ResultsResource.java
new file mode 100644
index 0000000..0e92b00
--- /dev/null
+++ b/src/main/java/pl/ctrlpkw/api/resource/ResultsResource.java
@@ -0,0 +1,108 @@
+package pl.ctrlpkw.api.resource;
+
+import com.google.common.collect.AbstractIterator;
+import com.google.common.collect.Lists;
+import pl.ctrlpkw.CassandraContext;
+import pl.ctrlpkw.api.dto.BallotResult;
+import pl.ctrlpkw.model.read.Ballot;
+import pl.ctrlpkw.model.read.BallotsRepository;
+import pl.ctrlpkw.model.write.Protocol;
+import pl.ctrlpkw.model.write.ProtocolAccessor;
+import org.joda.time.LocalDate;
+import org.springframework.cache.annotation.Cacheable;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+import javax.transaction.Transactional;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import java.util.Iterator;
+import java.util.function.BiFunction;
+import java.util.function.BinaryOperator;
+import java.util.stream.Collectors;
+import java.util.stream.LongStream;
+import java.util.stream.StreamSupport;
+
+@Path("/votings/{date}/ballots/{ballotNo}/result")
+@Produces(MediaType.APPLICATION_JSON)
+@Component
+public class ResultsResource {
+
+ @Resource
+ private BallotsRepository ballotsRepository;
+
+ @Resource
+ CassandraContext cassandraContext;
+
+ @GET
+ @Transactional
+ @Cacheable("results")
+ public BallotResult count(@PathParam("date") String votingDate, @PathParam("ballotNo") Integer ballotNo) {
+
+ Ballot ballot = ballotsRepository.findByDateAndNo(LocalDate.parse(votingDate), ballotNo);
+
+ ProtocolAccessor accessor = cassandraContext.getMappingManager().createAccessor(ProtocolAccessor.class);
+
+ BallotResult results = StreamSupport.stream(accessor.findByVotingDateAndBallotNo().spliterator(), false)
+ .reduce(identity, accumulator, combiner);
+ results.setBallotNo(ballot.getNo());
+ return results;
+ }
+
+ private BallotResult identity = BallotResult.builder()
+ .votersEntitledCount(0l).ballotsGivenCount(0l).votesCastCount(0l).votesValidCount(0l)
+ .votesCountPerOption(LongStream.rangeClosed(1, 10).boxed().map(i -> 0l).collect(Collectors.toList()))
+ .build();
+
+ private BiFunction accumulator = (BallotResult r1, Protocol r2) -> BallotResult.builder()
+ .votersEntitledCount(r1.getVotersEntitledCount() + r2.getVotersEntitledCount())
+ .ballotsGivenCount(r1.getBallotsGivenCount() + r2.getBallotsGivenCount())
+ .votesCastCount(r1.getVotesCastCount() + r2.getVotesCastCount())
+ .votesValidCount(r1.getVotesValidCount() + r2.getVotesValidCount())
+ .votesCountPerOption(
+ Lists.newArrayList(
+ summingIterator(
+ r1.getVotesCountPerOption().iterator(),
+ r2.getVotesCountPerOption().iterator(),
+ Long.valueOf(0l),
+ (a, b) -> a + b
+ )
+ )
+ )
+ .build();
+
+ private BinaryOperator combiner = (BallotResult r1, BallotResult r2) -> BallotResult.builder()
+ .votersEntitledCount(r1.getVotersEntitledCount() + r2.getVotersEntitledCount())
+ .ballotsGivenCount(r1.getBallotsGivenCount() + r2.getBallotsGivenCount())
+ .votesCastCount(r1.getVotesCastCount() + r2.getVotesCastCount())
+ .votesValidCount(r1.getVotesValidCount() + r2.getVotesValidCount())
+ .votesCountPerOption(
+ Lists.newArrayList(
+ summingIterator(
+ r1.getVotesCountPerOption().iterator(),
+ r2.getVotesCountPerOption().iterator(),
+ Long.valueOf(0l),
+ (a, b) -> a + b
+ )
+ )
+ )
+ .build();
+
+ private Iterator summingIterator(Iterator i1, Iterator i2, T zero, BinaryOperator addition) {
+ return new AbstractIterator() {
+ @Override
+ protected T computeNext() {
+ if (!i1.hasNext() && !i2.hasNext())
+ return endOfData();
+ T sum = i1.hasNext() ? i1.next() : zero;
+ if (i2.hasNext()) sum = addition.apply(sum, i2.next());
+ return sum;
+ }
+ };
+ }
+
+
+}
diff --git a/src/main/java/pl/ctrlpkw/api/resource/VotingResource.java b/src/main/java/pl/ctrlpkw/api/resource/VotingResource.java
new file mode 100644
index 0000000..b7f6727
--- /dev/null
+++ b/src/main/java/pl/ctrlpkw/api/resource/VotingResource.java
@@ -0,0 +1,39 @@
+package pl.ctrlpkw.api.resource;
+
+import pl.ctrlpkw.model.read.Voting;
+import pl.ctrlpkw.model.read.VotingRepository;
+import org.joda.time.LocalDate;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+import javax.ws.rs.*;
+import javax.ws.rs.core.MediaType;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
+
+@Path("/votings")
+@Produces(MediaType.APPLICATION_JSON)
+@Component
+public class VotingResource {
+
+ @Resource
+ private VotingRepository votingRepository;
+
+ @GET
+ public Iterable readAll() {
+ return StreamSupport
+ .stream(votingRepository.findAll().spliterator(), false)
+ .map(entityToDto)
+ .collect(Collectors.toList());
+ }
+
+ @GET
+ @Path("/{date}")
+ public pl.ctrlpkw.api.dto.Voting readOne(@PathParam("date") String dateString) {
+ return entityToDto.apply(votingRepository.findByDate(LocalDate.parse(dateString)));
+ }
+
+ private static Function entityToDto = entity -> pl.ctrlpkw.api.dto.Voting.builder().date(entity.getDate()).description(entity.getDescription()).build();
+
+}
diff --git a/src/main/java/pl/ctrlpkw/api/resource/WardsResource.java b/src/main/java/pl/ctrlpkw/api/resource/WardsResource.java
new file mode 100644
index 0000000..cd4d89c
--- /dev/null
+++ b/src/main/java/pl/ctrlpkw/api/resource/WardsResource.java
@@ -0,0 +1,67 @@
+package pl.ctrlpkw.api.resource;
+
+import com.vividsolutions.jts.geom.Coordinate;
+import com.vividsolutions.jts.geom.GeometryFactory;
+import pl.ctrlpkw.api.dto.Location;
+import pl.ctrlpkw.model.read.Voting;
+import pl.ctrlpkw.model.read.VotingRepository;
+import pl.ctrlpkw.model.read.Ward;
+import pl.ctrlpkw.model.read.WardRepository;
+import org.joda.time.LocalDate;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+import javax.validation.constraints.NotNull;
+import javax.ws.rs.*;
+import javax.ws.rs.core.MediaType;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
+
+@Path("/votings/{date}/wards")
+@Produces(MediaType.APPLICATION_JSON)
+@Component
+public class WardsResource {
+
+ @Resource
+ private GeometryFactory geometryFactory;
+
+ @Resource
+ private WardRepository wardRepository;
+
+ @Resource
+ private VotingRepository votingRepository;
+
+ @GET
+ public Iterable readByClosestLocation(
+ @PathParam("date") String votingDate,
+ @QueryParam("latitude") @NotNull Double latidude,
+ @QueryParam("longitude") @NotNull Double longitude,
+ @QueryParam("count") @DefaultValue("3") short count)
+ {
+ Voting voting = votingRepository.findByDate(LocalDate.parse(votingDate));
+ return StreamSupport
+ .stream(
+ wardRepository.findOrderedByDistance(
+ voting,
+ geometryFactory.createPoint(new Coordinate(latidude, longitude)),
+ new PageRequest(0, count)
+ ).spliterator(),
+ false)
+ .map(entityToDto)
+ .collect(Collectors.toList());
+ }
+
+ private static Function entityToDto = entity ->
+ pl.ctrlpkw.api.dto.Ward.builder()
+ .communityCode(entity.getCommunityCode())
+ .no(entity.getWardNo())
+ .address(entity.getWardAddress())
+ .location(entity.getLocation() != null ?
+ Location.builder().latitude(entity.getLocation().getX()).longitude(entity.getLocation().getY()).build()
+ : null
+ )
+ .build();
+
+}
diff --git a/src/main/java/pl/ctrlpkw/batch/WardsLoadingBatchConfig.java b/src/main/java/pl/ctrlpkw/batch/WardsLoadingBatchConfig.java
new file mode 100644
index 0000000..1d33d6a
--- /dev/null
+++ b/src/main/java/pl/ctrlpkw/batch/WardsLoadingBatchConfig.java
@@ -0,0 +1,95 @@
+package pl.ctrlpkw.batch;
+
+import com.vividsolutions.jts.geom.Coordinate;
+import com.vividsolutions.jts.geom.GeometryFactory;
+import pl.ctrlpkw.model.read.VotingRepository;
+import pl.ctrlpkw.model.read.Ward;
+import org.joda.time.LocalDate;
+import org.springframework.batch.core.Job;
+import org.springframework.batch.core.Step;
+import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
+import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
+import org.springframework.batch.core.launch.support.RunIdIncrementer;
+import org.springframework.batch.item.ItemProcessor;
+import org.springframework.batch.item.ItemReader;
+import org.springframework.batch.item.ItemWriter;
+import org.springframework.batch.item.database.JpaItemWriter;
+import org.springframework.batch.item.file.FlatFileItemReader;
+import org.springframework.batch.item.file.mapping.DefaultLineMapper;
+import org.springframework.batch.item.file.mapping.PassThroughFieldSetMapper;
+import org.springframework.batch.item.file.transform.DelimitedLineTokenizer;
+import org.springframework.batch.item.file.transform.FieldSet;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.io.ClassPathResource;
+
+import javax.annotation.Resource;
+import javax.persistence.EntityManagerFactory;
+
+@Configuration
+public class WardsLoadingBatchConfig {
+
+ @Resource
+ private GeometryFactory geometryFactory;
+
+ @Resource
+ private VotingRepository votingRepository;
+
+ @Bean
+ public ItemReader