Skip to content

Commit

Permalink
Merge pull request #9 from hmrc/fset-1067-location-scheme-spike
Browse files Browse the repository at this point in the history
FSET-1067 Add address lookup demo
  • Loading branch information
benjstephenson authored Dec 30, 2016
2 parents 83123eb + f08b6fe commit ef411d3
Show file tree
Hide file tree
Showing 8 changed files with 259 additions and 1 deletion.
3 changes: 3 additions & 0 deletions app/config/frontendAppConfig.scala
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ case class FasttrackConfig(url: FasttrackUrl)
case class FasttrackUrl(host: String, base: String)

case class FasttrackFrontendConfig(blockNewAccountsDate: Option[LocalDateTime], blockApplicationsDate: Option[LocalDateTime])
case class AddressLookupConfig(url: String)

object FasttrackFrontendConfig {
def read(blockNewAccountsDate: Option[String], blockApplicationsDate: Option[String]): FasttrackFrontendConfig = {
Expand All @@ -57,6 +58,7 @@ trait AppConfig {
val userManagementConfig: UserManagementConfig
val fasttrackConfig: FasttrackConfig
val fasttrackFrontendConfig: FasttrackFrontendConfig
val addressLookupConfig: AddressLookupConfig
}

object FrontendAppConfig extends AppConfig with ServicesConfig {
Expand All @@ -81,4 +83,5 @@ object FrontendAppConfig extends AppConfig with ServicesConfig {
blockNewAccountsDate = configuration.getString("application.blockNewAccountsDate"),
blockApplicationsDate = configuration.getString("application.blockApplicationsDate")
)
override lazy val addressLookupConfig = configuration.underlying.as[AddressLookupConfig]("microservice.services.address-lookup")
}
71 changes: 71 additions & 0 deletions app/connectors/addresslookup/AddressLookupClient.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Copyright 2016 HM Revenue & Customs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package connectors.addresslookup

import java.net.URLEncoder

import config.CSRHttp
import uk.gov.hmrc.play.http.{ HeaderCarrier, _ }

import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future

/**
* The following client has been take from taken from https://github.com/hmrc/address-reputation-store. The project has
* not been added as a dependency, as it brings in many transitive dependencies that are not needed,
* as well as data cleansing/ingestion and backward compatibility logic that is not needed for this project.
* If the version 2 api gets deprecated, then these DTOs will have to change.
* There have been some minor changes made to the code to ensure that it compiles and passes scalastyle,
* but there is some copied code that is not idiomatic Scala and should be changed at some point in the future
*/

trait AddressLookupClient {

def addressLookupEndpoint: String
val http: CSRHttp

private def url = s"$addressLookupEndpoint/v2/uk/addresses"

def findById(id: String)(implicit hc: HeaderCarrier): Future[Option[AddressRecord]] = {
assert(id.length <= 100, "Postcodes cannot be larger than 100 characters")
val uq = "/" + enc(id)
http.GET[Option[AddressRecord]](url + uq).recover {
case _: NotFoundException => None
}
}

def findByUprn(uprn: Long)(implicit hc: HeaderCarrier): Future[List[AddressRecord]] = {
val uq = "?uprn=" + uprn.toString
http.GET[List[AddressRecord]](url + uq)
}

def findByPostcode(postcode: String, filter: Option[String])(implicit hc: HeaderCarrier): Future[List[AddressRecord]] = {
val safePostcode = postcode.replaceAll("[^A-Za-z0-9]", "")
assert(safePostcode.length <= 10, "Postcodes cannot be larger than 10 characters")
val pq = "?postcode=" + enc(safePostcode)
val fq = filter.map(fi => "&filter=" + enc(fi)).getOrElse("")
http.GET[List[AddressRecord]](url + pq + fq)
}

def findByOutcode(outcode: Outcode, filter: String)(implicit hc: HeaderCarrier): Future[List[AddressRecord]] = {
val pq = "?outcode=" + outcode.toString
val fq = "&filter=" + enc(filter)
http.GET[List[AddressRecord]](url + pq + fq)
}

private def enc(s: String) = URLEncoder.encode(s, "UTF-8")
}
140 changes: 140 additions & 0 deletions app/connectors/addresslookup/AddressRecord.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/*
* Copyright 2016 HM Revenue & Customs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package connectors.addresslookup

import play.api.libs.json.Json


/**
* The following DTOs are taken from https://github.com/hmrc/address-reputation-store. The project has
* not been added as a dependency, as it brings in many transitive dependencies that are not needed,
* as well as data cleansing/ingestion and backward compatibility logic that is not needed for this project.
* If the version 2 api gets deprecated, then these DTOs will have to change.
* There have been some minor changes made to the code to ensure that it compiles and passes scalastyle,
* but there is some copied code that is not idiomatic Scala and should be changed at some point in the future
*/

case class LocalCustodian(code: Int, name: String)

object LocalCustodian {
implicit val reads = Json.reads[LocalCustodian]
}

/** Represents a country as per ISO3166. */
case class Country(
// ISO3166-1 or ISO3166-2 code, e.g. "GB" or "GB-ENG" (note that "GB" is the official
// code for UK although "UK" is a reserved synonym and may be used instead)
// See https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2
// and https://en.wikipedia.org/wiki/ISO_3166-2:GB
code: String,
// The printable name for the country, e.g. "United Kingdom"
name: String)

object Country {
implicit val countryReads = Json.reads[Country]
}

case class Outcode(area: String, district: String) {
override lazy val toString: String = area + district
}

object Outcode {
implicit val outcodeReads = Json.reads[Outcode]
}

object Countries {
// note that "GB" is the official ISO code for UK, although "UK" is a reserved synonym and is less confusing
val UK = Country("UK", "United Kingdom")
val GB = Country("GB", "United Kingdom") // special case provided for in ISO-3166
val GG = Country("GG", "Guernsey")
val IM = Country("IM", "Isle of Man")
val JE = Country("JE", "Jersey")

val England = Country("GB-ENG", "England")
val Scotland = Country("GB-SCT", "Scotland")
val Wales = Country("GB-WLS", "Wales")
val Cymru = Country("GB-CYM", "Cymru")
val NorthernIreland = Country("GB-NIR", "Northern Ireland")

private val all = List(UK, GB, GG, IM, JE, England, Scotland, Wales, Cymru, NorthernIreland)

def find(code: String): Option[Country] = all.find(_.code == code)

def findByName(name: String): Option[Country] = all.find(_.name == name)

// TODO this is possibly not good enough - should consult a reference HMG-approved list of countries
}

/**
* Address typically represents a postal address.
* For UK addresses, 'town' will always be present.
* For non-UK addresses, 'town' may be absent and there may be an extra line instead.
*/
case class Address(lines: List[String],
town: Option[String],
county: Option[String],
postcode: String,
subdivision: Option[Country],
country: Country) {

def nonEmptyFields: List[String] = lines ::: town.toList ::: county.toList ::: List(postcode)

/** Gets a conjoined representation, excluding the country. */
def printable(separator: String): String = nonEmptyFields.mkString(separator)

/** Gets a single-line representation, excluding the country. */
def printable: String = printable(", ")

def line1: String = lines.lift(0).getOrElse("")

def line2: String = lines.lift(1).getOrElse("")

def line3: String = lines.lift(2).getOrElse("")

def line4: String = lines.lift(3).getOrElse("")
}

object Address {
implicit val addressFormat = Json.reads[Address]
}

case class LatLong(lat: Double, long: Double) {
def toLocation: String = lat.toString + "," + long.toString
}

case class AddressRecord(
id: String,
uprn: Option[Long],
address: Address,
// ISO639-1 code, e.g. 'en' for English
// see https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
language: String,
localCustodian: Option[LocalCustodian],
blpuState: Option[String],
logicalState: Option[String],
streetClassification: Option[String]) {

def withoutMetadata: AddressRecord = copy(blpuState = None, logicalState = None, streetClassification = None)

// TODO put in the real lat/lng once the address lookup service supports it
// Hardcoded to Coventry for the time being
val geoLocation: LatLong = LatLong(52.40656, -1.51217)
}

object AddressRecord {
implicit val addressRecordFormat = Json.reads[AddressRecord]
}
35 changes: 35 additions & 0 deletions app/controllers/test/AddressLookupController.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright 2016 HM Revenue & Customs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package controllers.test

import config.CSRHttp
import connectors.addresslookup.AddressLookupClient
import controllers.BaseController
import play.api.mvc.Action

trait AddressLookupController extends BaseController with AddressLookupClient {
// TODO Add permissions into this once the feature is ready to be moved from test routes
def addressLookup(postcode: String) = Action.async { implicit request =>
val decoded = java.net.URLDecoder.decode(postcode, "UTF8")
findByPostcode(decoded, None).map(r => Ok(r.toString))
}
}

object AddressLookupController extends AddressLookupController {
val http = CSRHttp
val addressLookupEndpoint = config.FrontendAppConfig.addressLookupConfig.url
}
4 changes: 4 additions & 0 deletions conf/application.conf
Original file line number Diff line number Diff line change
Expand Up @@ -107,5 +107,9 @@ microservice {
host = "http://localhost:8094"
}
}

address-lookup {
url = "http://localhost:9022"
}
}
}
2 changes: 1 addition & 1 deletion conf/prod.routes
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Add all the application routes to the app.routes file

-> /fset-fast-track app.Routes
-> /fset-fast-track app.Routes
-> / health.Routes

GET /admin/metrics com.kenshoo.play.metrics.MetricsController.metrics
Expand Down
2 changes: 2 additions & 0 deletions conf/testOnlyDoNotUseInAppConf.routes
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@

# Add all the application routes to the prod.routes file
-> / prod.Routes

GET /fset-fast-track/address-search/:postcode controllers.test.AddressLookupController.addressLookup(postcode: String)
3 changes: 3 additions & 0 deletions sbtTestRoutes.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/bash

sbt -J-Dapplication.router=testOnlyDoNotUseInAppConf.Routes -Dhttp.port=9283 -Dplay.filters.headers.contentSecurityPolicy='www.google-analytics.com'

0 comments on commit ef411d3

Please sign in to comment.