summaryrefslogtreecommitdiff
path: root/src/main/groovy/org
diff options
context:
space:
mode:
authorYves Fischer <yves.fischer@cern.ch>2016-01-25 22:14:26 +0100
committerYves Fischer <yves.fischer@cern.ch>2016-01-25 22:14:26 +0100
commit08b882f4cf284bed3f8f80f6e47d37037753a22b (patch)
treed328f7f98b079e324690f8b3fbbb0d0ee2b1ba61 /src/main/groovy/org
downloadglocals-classifieds-08b882f4cf284bed3f8f80f6e47d37037753a22b.tar.gz
glocals-classifieds-08b882f4cf284bed3f8f80f6e47d37037753a22b.zip
commit
Diffstat (limited to 'src/main/groovy/org')
-rw-r--r--src/main/groovy/org/xapek/yvesf/classifieds/ClassifiedParserMain.groovy87
-rw-r--r--src/main/groovy/org/xapek/yvesf/classifieds/Dumper.groovy25
-rw-r--r--src/main/groovy/org/xapek/yvesf/classifieds/Model.groovy102
-rw-r--r--src/main/groovy/org/xapek/yvesf/classifieds/util/Fail.groovy40
-rw-r--r--src/main/groovy/org/xapek/yvesf/classifieds/util/Failable.groovy14
-rw-r--r--src/main/groovy/org/xapek/yvesf/classifieds/util/Success.groovy36
6 files changed, 304 insertions, 0 deletions
diff --git a/src/main/groovy/org/xapek/yvesf/classifieds/ClassifiedParserMain.groovy b/src/main/groovy/org/xapek/yvesf/classifieds/ClassifiedParserMain.groovy
new file mode 100644
index 0000000..499a37a
--- /dev/null
+++ b/src/main/groovy/org/xapek/yvesf/classifieds/ClassifiedParserMain.groovy
@@ -0,0 +1,87 @@
+package org.xapek.yvesf.classifieds
+
+import groovy.json.JsonSlurper
+import groovyx.net.http.HTTPBuilder
+import org.apache.http.HttpResponse
+import org.xapek.yvesf.classifieds.util.Fail
+import org.xapek.yvesf.classifieds.util.Failable
+import org.xapek.yvesf.classifieds.util.Success
+
+class ClassifiedParserMain {
+ static final url = 'http://www.glocals.com/classifieds/housing-and-real-estate/&get_classified_flats'
+
+ static void main(String[] args) {
+ handleData(args.length == 1 ? readJsonFile(args.first()) : readNetwork()).with {
+ if (it instanceof Fail) {
+ println "Failed: ${it.error}"
+ System.exit(1)
+ } else {
+ Dumper.dump(it.value, new PrintWriter(System.out))
+ System.exit(0)
+ }
+ }
+ }
+
+ static Object readJsonFile(String f) {
+ final jsonParser = new JsonSlurper()
+ return jsonParser.parse(new File(f))
+ }
+
+ static Object readNetwork() {
+ final http = new HTTPBuilder(url)
+ final jsonParser = new JsonSlurper()
+ http.post(
+ body: ['start': '20', 'limit': '20', 'form[bl_city_network]': '-1'],
+ headers: ['User-Agent': 'Mozilla/5.0 Firefox/3.0.4',
+ 'Accept' : 'application/json']) { HttpResponse resp ->
+ assert resp.statusLine.statusCode < 300 && resp.statusLine.statusCode >= 200:
+ "HTTP Request failed with status code ${resp.statusLine.statusCode}"
+ return jsonParser.parse(resp.entity.content)
+ } as Object
+ }
+
+ private static Failable<Model.ClassifiedList> handleData(Object p) {
+ expectMap(p) << { Map m ->
+ getOrFail(m, 'totalCount', Number) << { Number totalCount ->
+ getOrFail(m, 'classifieds', List) << { List rawClassifieds ->
+ all(rawClassifieds.collect { handleClassified(it) }) << { List<Model.Classified> classifieds ->
+ final Model.ClassifiedList classifiedList = new Model.ClassifiedList(totalCount: totalCount)
+ classifiedList.addAll(classifieds)
+ return new Success(classifiedList)
+ }
+ }
+ }
+ }
+ }
+
+ private static Failable<Model.Classified> handleClassified(Object p) {
+ expectMap(p) << { Map map ->
+ return new Success(new Model.Classified(map)) // here I cheat a bit
+ }
+ }
+
+ static <T> Failable<List<T>> all(Collection<Failable<T>> values) {
+ if (values.every { it.success }) {
+ return new Success<List<T>>(values.collect { it.value })
+ } else {
+ return new Fail("Not all values are successful, first error: ${values.find { !it.success }.error}")
+ }
+ }
+
+ static Failable<Map> expectMap(Object data) {
+ if (data instanceof Map) return new Success(data)
+ else return new Fail('object is not instanceof Map')
+ }
+
+ static <T> Failable<T> getOrFail(Map map, String key, Class<T> clazz = Object) {
+ if (!map.containsKey(key)) return new Fail('key not in map')
+ else {
+ final x = map.get(key)
+ if (clazz.isAssignableFrom(x.getClass())) {
+ return new Success<T>(x as T)
+ } else {
+ return new Fail("Key ${key} found but not of type ${clazz}")
+ }
+ }
+ }
+}
diff --git a/src/main/groovy/org/xapek/yvesf/classifieds/Dumper.groovy b/src/main/groovy/org/xapek/yvesf/classifieds/Dumper.groovy
new file mode 100644
index 0000000..884166d
--- /dev/null
+++ b/src/main/groovy/org/xapek/yvesf/classifieds/Dumper.groovy
@@ -0,0 +1,25 @@
+package org.xapek.yvesf.classifieds
+
+import groovy.xml.MarkupBuilder
+import org.xapek.yvesf.classifieds.Model.ClassifiedList
+
+
+class Dumper {
+ static dump(ClassifiedList classifiedList, PrintWriter writer) {
+ new MarkupBuilder(writer).root {
+ rss(version: '2.0') {
+ channel {
+ title('Glocals')
+ }
+ classifiedList.each { Model.Classified classified ->
+ item {
+ title("${classified.type} - ${classified.title} - ${classified.composedLocation}")
+ guid(classified.id)
+ author(classified.memberName)
+ description(classified.description)
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/groovy/org/xapek/yvesf/classifieds/Model.groovy b/src/main/groovy/org/xapek/yvesf/classifieds/Model.groovy
new file mode 100644
index 0000000..ee0b11d
--- /dev/null
+++ b/src/main/groovy/org/xapek/yvesf/classifieds/Model.groovy
@@ -0,0 +1,102 @@
+package org.xapek.yvesf.classifieds
+
+class Model {
+ static class Classified {
+ //0 = available" -> "Flexible"
+ //1 = city" -> "France"
+ //2 = column_headings" -> "0"
+ //3 = contact" -> "0795354458"
+ //4 = currency" -> "CHF"
+ //5 = date" -> "Jan 25, 16"
+ //6 = description" -> "Fully furnished house of 270 sqm in total. <br />\r\n<br />\r\nVery nice and big house of 270sqm (included 90 sqm2 of basement) with 5 bedrooms, 2 showerooms,1 bathroom and a guest toilet. 70 sqm of closed garage. Wine cellar. Lots of storage.<br />\r\n<br />\r\nBig living/dining room with fireplace. Big kitchen all equipped.<br />\r\n<br />\r\nLaundry area and storage room.<br />\r\n<br />\r\n1000 sqm of land.<br />\r\n<br />\r\nLocated in Valleiry 20 minutes from Geneva.<br />\r\n<br />\r\n3'000 €<br />\r\n<br />\r\nCan be rent unfurnished TBD<br />\r\n<br />\r\nNo agencies, thanks!"
+ //7 = expired" -> "0"
+ //8 = id" -> "67199"
+ //9 = location" -> "Valleiry"
+ //10 ="mark" -> "<a href="javascript:mark(67199)" id="classified_mark_67199">Mark / Save Ad</a>"
+ //11 ="mem_first_name" -> "petitemanga"
+ //12 ="mem_id" -> "16088"
+ //13 ="mem_link" -> "javascript:open_login_popup();"
+ //14 ="mem_name" -> "petitemanga"
+ //15 ="mem_name_js" -> "petitemanga"
+ //16 ="mem_photo" -> "http://cdn.glocals.com/sites/glocals/_static_media/public/members/empty54.gif"
+ //17 ="network" -> "Geneva"
+ //18 ="photo1" -> "/_media/board_flat1/67/67199_bl_photo_eedba.jpg"
+ //19 ="photo2" -> "/_media/board_flat1/67/67199_bl_photo2_58cc1.jpg"
+ //20 ="photo3" -> "/_media/board_flat1/67/67199_bl_photo3_a5502.jpg"
+ //21 ="photo4" -> "/_media/board_flat1/67/67199_bl_photo4_63653.jpg"
+ //22 ="photos" -> "1"
+ //23 ="price" -> "3000"
+ //24 ="rooms" -> "7"
+ //25 ="status" -> "null"
+ //26 ="title" -> "Fully furnished house of 270 sqm and huge terrace of 90 sqm"
+ //27 ="title_js" -> "Fully furnished house of 270 sqm and huge terrace of 90 sqm"
+ //28 ="type" -> "Apts / Housing for rent"
+ //29 ="views" -> "8"
+ private Map map
+
+ Classified(Map m) {
+ this.@map = m
+ }
+
+ Map getMap() {
+ return map
+ }
+
+ String getComposedLocation() {
+ return "${map.get('location')}, ${map.get('city')}"
+ }
+
+ String getType() {
+ return map.get('type') as String
+ }
+
+ String getAvailable() {
+ return map.get('available') as String
+ }
+
+ String getCity() {
+ return map.get('city')
+ }
+
+ String getPrice() {
+ return map.get('price') as String
+ }
+
+ String getCurrency() {
+ return map.get('currency') as String
+ }
+
+ String getViews() {
+ return map.get('views') as String
+ }
+
+ String getTitle() {
+ return map.get('title') as String
+ }
+
+ String getId() {
+ return map.get('id') as String
+ }
+
+ String getMemberName() {
+ return map.get('mem_name') as String
+ }
+
+ String getDescription() {
+ return map.get('description') as String
+ }
+ }
+
+ static class ClassifiedList extends ArrayList<Classified> {
+ private long totalCount
+
+ void setTotalCount(long totalCount) {
+ this.@totalCount = totalCount
+ }
+
+ @Override
+ String toString() {
+ return "Classified List: totalCount=${totalCount} actualCount=${size()}"
+ }
+ }
+}
diff --git a/src/main/groovy/org/xapek/yvesf/classifieds/util/Fail.groovy b/src/main/groovy/org/xapek/yvesf/classifieds/util/Fail.groovy
new file mode 100644
index 0000000..c58d6ef
--- /dev/null
+++ b/src/main/groovy/org/xapek/yvesf/classifieds/util/Fail.groovy
@@ -0,0 +1,40 @@
+package org.xapek.yvesf.classifieds.util
+
+import groovy.transform.CompileStatic
+import groovy.transform.ToString
+
+
+@CompileStatic
+@Newify
+@ToString(includeFields = true, includeNames = true)
+class Fail<T> implements Failable<T> {
+ private String error
+
+ Fail() {
+ this.@error = null
+ }
+
+ Fail(String error) {
+ this.@error = error;
+ }
+
+ @Override
+ boolean isSuccess() {
+ return false
+ }
+
+ @Override
+ Object getValue() {
+ return null
+ }
+
+ @Override
+ String getError() {
+ return error
+ }
+
+ @Override
+ def <T2> Failable<T2> leftShift(Closure<Failable<T2>> closure) {
+ return null
+ }
+}
diff --git a/src/main/groovy/org/xapek/yvesf/classifieds/util/Failable.groovy b/src/main/groovy/org/xapek/yvesf/classifieds/util/Failable.groovy
new file mode 100644
index 0000000..4ad5e90
--- /dev/null
+++ b/src/main/groovy/org/xapek/yvesf/classifieds/util/Failable.groovy
@@ -0,0 +1,14 @@
+package org.xapek.yvesf.classifieds.util
+
+import groovy.transform.CompileStatic
+
+@CompileStatic
+interface Failable<T> {
+ boolean isSuccess()
+
+ T getValue()
+
+ String getError()
+
+ public <T2> Failable<T2> leftShift(Closure<Failable<T2>> closure);
+} \ No newline at end of file
diff --git a/src/main/groovy/org/xapek/yvesf/classifieds/util/Success.groovy b/src/main/groovy/org/xapek/yvesf/classifieds/util/Success.groovy
new file mode 100644
index 0000000..224909d
--- /dev/null
+++ b/src/main/groovy/org/xapek/yvesf/classifieds/util/Success.groovy
@@ -0,0 +1,36 @@
+package org.xapek.yvesf.classifieds.util
+
+import groovy.transform.CompileStatic
+import groovy.transform.ToString
+
+
+@CompileStatic
+@Newify
+@ToString(includeFields = true, includeNames = true)
+class Success<T> implements Failable<T> {
+ private T value
+
+ public Success(T value) {
+ this.value = value
+ }
+
+ @Override
+ boolean isSuccess() {
+ return true
+ }
+
+ @Override
+ T getValue() {
+ return value
+ }
+
+ @Override
+ String getError() {
+ return null
+ }
+
+ @Override
+ def <T2> Failable<T2> leftShift(Closure<Failable<T2>> closure) {
+ return closure.call(value)
+ }
+} \ No newline at end of file