commit a287fa62af88f84cc56e85f5672da60d94c70a68
parent 23ea52dcb52d912290ece0a93a78c79d6bd7d4d3
Author: Kebigon <git@kebigon.xyz>
Date: Sun, 5 Apr 2020 15:08:28 +0900
Add possibility to configure several searches with different conditions
Diffstat:
7 files changed, 297 insertions(+), 288 deletions(-)
diff --git a/src/main/java/xyz/kebigon/housesearch/HouseSearchApplication.java b/src/main/java/xyz/kebigon/housesearch/HouseSearchApplication.java
@@ -19,66 +19,71 @@ import xyz.kebigon.housesearch.mail.EmailSender;
@Slf4j
public class HouseSearchApplication
{
- public static void main(String[] args) throws IOException, EmailException
- {
- final SearchConditions conditions = SearchConditions.load();
- final SentPostingsCache sentPostings = SentPostingsCache.load();
-
- try
- {
- Collection<Posting> postings;
-
- try (final SuumoBrowser suumo = new SuumoBrowser())
- {
- postings = suumo.search(conditions, sentPostings);
- }
-
- if (postings.isEmpty())
- {
- log.info("No postings found on Suumo, terminating");
- return;
- }
-
- if (!StringUtils.isEmpty(conditions.getExpression()))
- {
- try (final YahooTransitBrowser yahooTransit = new YahooTransitBrowser())
- {
- ApplicationContext.setYahooTransitBrowser(yahooTransit);
-
- postings = postings.stream() // Do not parallel here
- .filter(property -> SearchConditionsValidator.validateExpression(property, conditions)).collect(Collectors.toList());
- }
-
- if (postings.isEmpty())
- {
- log.info("No postings left after applying expression filter, terminating");
- return;
- }
- }
-
- log.info("=======[ RESULTS ]=======");
- log.info("Found {} postings", postings.size());
-
- for (final Posting posting : postings)
- log.info("-> {}", posting);
-
- log.info("Sending email notification");
-
- final EmailSender sender = new EmailSender();
- sender.send(postings);
-
- // Register sent postings
- postings.forEach(sentPostings::add);
-
- log.info("Email notification sent, terminating");
- }
- catch (final Throwable t)
- {
- log.error("Unrecoverable exception", t);
- }
- finally
- {
- sentPostings.save();
- }
- }
+ public static void main(String[] args) throws IOException, EmailException
+ {
+ final SentPostingsCache sentPostings = SentPostingsCache.load();
+
+ try
+ {
+ for (final SearchConditions conditions : SearchConditions.load())
+ processConditions(conditions, sentPostings);
+ }
+ catch (final Throwable t)
+ {
+ log.error("Unrecoverable exception", t);
+ }
+ finally
+ {
+ sentPostings.save();
+ }
+ }
+
+ private static void processConditions(SearchConditions conditions, SentPostingsCache sentPostings) throws IOException, EmailException
+ {
+ Collection<Posting> postings;
+
+ try (final SuumoBrowser suumo = new SuumoBrowser())
+ {
+ postings = suumo.search(conditions, sentPostings);
+ }
+
+ if (postings.isEmpty())
+ {
+ log.info("No postings found on Suumo, terminating");
+ return;
+ }
+
+ if (!StringUtils.isEmpty(conditions.getExpression()))
+ {
+ try (final YahooTransitBrowser yahooTransit = new YahooTransitBrowser())
+ {
+ ApplicationContext.setYahooTransitBrowser(yahooTransit);
+
+ postings = postings.stream() // Do not parallel here
+ .filter(property -> SearchConditionsValidator.validateExpression(property, conditions)).collect(Collectors.toList());
+ }
+
+ if (postings.isEmpty())
+ {
+ log.info("No postings left after applying expression filter, terminating");
+ return;
+ }
+ }
+
+ log.info("=======[ RESULTS ]=======");
+ log.info("Found {} postings", postings.size());
+
+ for (final Posting posting : postings)
+ log.info("-> {}", posting);
+
+ log.info("Sending email notification");
+
+ final EmailSender sender = new EmailSender();
+ sender.send(postings, conditions);
+
+ // Register sent postings
+ postings.forEach(sentPostings::add);
+
+ log.info("Email notification sent, terminating");
+ }
}
diff --git a/src/main/java/xyz/kebigon/housesearch/domain/SearchConditions.java b/src/main/java/xyz/kebigon/housesearch/domain/SearchConditions.java
@@ -3,94 +3,69 @@ package xyz.kebigon.housesearch.domain;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
-import java.util.Properties;
-import org.springframework.util.StringUtils;
+import org.apache.commons.lang3.StringUtils;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Data;
@Data
public class SearchConditions
{
- private Area area;
- private AnounceType type;
- private Long minPrice;
- private Long maxPrice;
- private Integer minAge;
- private Integer maxAge;
- private Integer minLandSurface;
- private Integer maxLandSurface;
- private Integer minHouseSurface;
- private Integer maxHouseSurface;
- private Integer maxWalkTimeToStation;
- private String expression;
-
- public static SearchConditions load() throws IOException
- {
- final Properties properties = new Properties();
- properties.load(SearchConditions.class.getClassLoader().getResourceAsStream("search-conditions.cfg"));
-
- final SearchConditions conditions = new SearchConditions();
- conditions.setArea(areaProp(properties, "area"));
- conditions.setType(anounceTypeProp(properties, "type"));
- conditions.setMinPrice(longProp(properties, "price.min"));
- conditions.setMaxPrice(longProp(properties, "price.max"));
- conditions.setMinAge(intProp(properties, "age.min"));
- conditions.setMaxAge(intProp(properties, "age.max"));
- conditions.setMinLandSurface(intProp(properties, "surface.land.min"));
- conditions.setMaxLandSurface(intProp(properties, "surface.land.max"));
- conditions.setMinHouseSurface(intProp(properties, "surface.house.min"));
- conditions.setMaxHouseSurface(intProp(properties, "surface.house.max"));
- conditions.setMaxWalkTimeToStation(intProp(properties, "station.walktime.max"));
- conditions.setExpression(expressionProp(properties, "expression.file"));
- return conditions;
- }
-
- private static Area areaProp(Properties properties, String key)
- {
- final String property = properties.getProperty(key);
- return StringUtils.isEmpty(property) ? null : Area.valueOf(property);
- }
-
- private static AnounceType anounceTypeProp(Properties properties, String key)
- {
- final String property = properties.getProperty(key);
- return StringUtils.isEmpty(property) ? null : AnounceType.valueOf(property);
- }
-
- private static Integer intProp(Properties properties, String key)
- {
- final String property = properties.getProperty(key);
- return StringUtils.isEmpty(property) ? null : Integer.parseInt(property);
- }
-
- private static Long longProp(Properties properties, String key)
- {
- final String property = properties.getProperty(key);
- return StringUtils.isEmpty(property) ? null : Long.parseLong(property);
- }
+ private String name;
+ private Area area;
+ private AnounceType type;
+ @JsonProperty("price.min")
+ private Long minPrice;
+ @JsonProperty("price.max")
+ private Long maxPrice;
+ @JsonProperty("age.min")
+ private Integer minAge;
+ @JsonProperty("age.max")
+ private Integer maxAge;
+ @JsonProperty("surface.land.min")
+ private Integer minLandSurface;
+ @JsonProperty("surface.land.max")
+ private Integer maxLandSurface;
+ @JsonProperty("surface.house.min")
+ private Integer minHouseSurface;
+ @JsonProperty("surface.house.max")
+ private Integer maxHouseSurface;
+ @JsonProperty("station.walktime.max")
+ private Integer maxWalkTimeToStation;
+ private String expression;
- private static String expressionProp(Properties properties, String key) throws IOException
- {
- final String property = properties.getProperty(key);
- if (StringUtils.isEmpty(property))
- return null;
+ public static SearchConditions[] load() throws IOException
+ {
+ final ObjectMapper mapper = new ObjectMapper();
+ return mapper.readValue(SearchConditions.class.getClassLoader().getResourceAsStream("searches.json"), SearchConditions[].class);
+ }
- String expression = "";
- try (final BufferedReader reader = new BufferedReader(new InputStreamReader(SearchConditions.class.getClassLoader().getResourceAsStream(property))))
- {
- String line;
- while ((line = reader.readLine()) != null)
- {
- line = line.trim();
- if (line.isEmpty())
- continue;
- if (line.startsWith("#"))
- continue;
- expression += line;
- }
- }
+ @JsonProperty("expression.file")
+ private void expressionProp(String expressionFile) throws IOException
+ {
+ if (StringUtils.isEmpty(expressionFile))
+ {
+ expression = null;
+ return;
+ }
- return expression;
- }
+ expression = "";
+ try (final BufferedReader reader = new BufferedReader(
+ new InputStreamReader(SearchConditions.class.getClassLoader().getResourceAsStream(expressionFile))))
+ {
+ String line;
+ while ((line = reader.readLine()) != null)
+ {
+ line = line.trim();
+ if (line.isEmpty())
+ continue;
+ if (line.startsWith("#"))
+ continue;
+ expression += line;
+ }
+ }
+ }
}
diff --git a/src/main/java/xyz/kebigon/housesearch/mail/EmailSender.java b/src/main/java/xyz/kebigon/housesearch/mail/EmailSender.java
@@ -13,131 +13,138 @@ import org.apache.commons.mail.HtmlEmail;
import xyz.kebigon.housesearch.HouseSearchApplication;
import xyz.kebigon.housesearch.domain.Posting;
import xyz.kebigon.housesearch.domain.Route;
+import xyz.kebigon.housesearch.domain.SearchConditions;
public class EmailSender
{
- private final Session mailSession;
-
- public EmailSender() throws IOException
- {
- final Properties mailProperties = new Properties();
- mailProperties.load(HouseSearchApplication.class.getClassLoader().getResourceAsStream("mail.cfg"));
- mailSession = Session.getInstance(mailProperties);
- }
-
- public void send(Collection<Posting> postings) throws EmailException
- {
- // Send email separately
- if (postings.size() <= 3)
- {
- for (final Posting posting : postings)
- {
- final StringBuilder builder = new StringBuilder();
- appendPosting(posting, builder);
- send(posting.getAge() + "yo house found near " + posting.getStation() + " station for " + (posting.getPrice() / 10000) + "万円",
- builder.toString());
- }
- }
- // Send one email with all results
- else
- {
- final StringBuilder builder = new StringBuilder();
- for (final Posting posting : postings)
- appendPosting(posting, builder);
- send(postings.size() + " houses found", builder.toString());
- }
- }
-
- private void send(String title, String content) throws EmailException
- {
- final Email email = new HtmlEmail();
- email.setMailSession(mailSession);
- email.setSubject(title);
- email.setCharset("UTF-8");
- email.setMsg(content);
-
- String from = mailSession.getProperty("housesearch.mail.from");
- String to = mailSession.getProperty("housesearch.mail.to");
- String bcc = mailSession.getProperty("housesearch.mail.bcc");
-
- if (from != null && !(from = from.trim()).isEmpty())
- email.setFrom(from);
-
- if (to != null && !(to = to.trim()).isEmpty())
- for (final String address : mailSession.getProperty(to).split(","))
- email.addTo(address.trim());
-
- if (bcc != null && !(bcc = bcc.trim()).isEmpty())
- for (final String address : bcc.split(","))
- email.addBcc(address.trim());
-
- email.send();
- }
-
- private static void appendPosting(Posting posting, StringBuilder builder)
- {
- builder.append("<p><a href=\"").append(posting.getUrl()).append("\">");
- builder.append(posting.getAge()).append("yo house near ").append(posting.getStation()).append(" station for ").append(posting.getPrice() / 10000)
- .append("万円");
- builder.append("</a>");
- builder.append("<br>House surface: ").append(Math.round(posting.getHouseSurface())).append("m2 / ")
- .append(Math.round(posting.getHouseSurface() * 0.3025)).append("坪");
- builder.append("<br>Land surface: ").append(Math.round(posting.getLandSurface())).append("m2 / ").append(Math.round(posting.getLandSurface() * 0.3025))
- .append("坪");
-
- appendRoutes(posting, builder);
-
- builder.append("</p>");
- }
-
- private static void appendRoutes(Posting posting, StringBuilder builder)
- {
- final Route fastestRoute = posting.getFastestRoute();
- final Route cheapestRoute = posting.getCheapestRoute();
- final Route easiestRoute = posting.getEasiestRoute();
-
- // No route have been analyzed
- if (fastestRoute == null && cheapestRoute == null && easiestRoute == null)
- return;
-
- builder.append("<br>Routes: ");
-
- if (fastestRoute == cheapestRoute)
- if (fastestRoute == easiestRoute)
- appendRoute("Best", posting, fastestRoute, builder);
- else
- {
- appendRoute("Fastest & Cheapest", posting, fastestRoute, builder);
- appendRoute("Easiest", posting, easiestRoute, builder);
- }
- else if (fastestRoute == easiestRoute)
- {
- appendRoute("Fastest & Easiest", posting, fastestRoute, builder);
- appendRoute("Cheapest", posting, cheapestRoute, builder);
- }
- else
- {
- appendRoute("Fastest", posting, fastestRoute, builder);
-
- if (cheapestRoute == easiestRoute)
- appendRoute("Cheapest & Easiest", posting, cheapestRoute, builder);
- else
- {
- appendRoute("Cheapest", posting, cheapestRoute, builder);
- appendRoute("Easiest", posting, easiestRoute, builder);
- }
- }
- }
-
- private static void appendRoute(String label, Posting property, Route route, StringBuilder builder)
- {
- if (route == null)
- return;
-
- builder.append("<br>- ").append(label).append(": ");
- builder.append(route.getTime() + property.getWalkTimeToStation()).append("min to ").append(route.getTo());
- builder.append(" (walk: ").append(property.getWalkTimeToStation()).append("min, train: ").append(route.getTime()).append("min)");
- builder.append(" / ").append(route.getFare()).append("円");
- builder.append(" / ").append(route.getTransfer()).append(" transfer");
- }
+ private static final String ONE_POSTING_TITLE = "[%1$s] %2$syo house found near %3$s station for %4$s万円";
+ private static final String SEVERAL_POSTINGS_TITLE = "[%1$s] %2$s houses found";
+
+ private final Session mailSession;
+
+ public EmailSender() throws IOException
+ {
+ final Properties mailProperties = new Properties();
+ mailProperties.load(HouseSearchApplication.class.getClassLoader().getResourceAsStream("mail.cfg"));
+ mailSession = Session.getInstance(mailProperties);
+ }
+
+ public void send(Collection<Posting> postings, SearchConditions conditions) throws EmailException
+ {
+ // Send email separately
+ if (postings.size() <= 3)
+ {
+ for (final Posting posting : postings)
+ {
+ final String title = String.format(ONE_POSTING_TITLE, conditions.getName(), posting.getAge(), posting.getStation(), posting.getPrice() / 10000);
+
+ final StringBuilder builder = new StringBuilder();
+ appendPosting(posting, builder);
+ send(title, builder.toString());
+ }
+ }
+ // Send one email with all results
+ else
+ {
+ final String title = String.format(SEVERAL_POSTINGS_TITLE, conditions.getName(), postings.size());
+
+ final StringBuilder builder = new StringBuilder();
+ for (final Posting posting : postings)
+ appendPosting(posting, builder);
+ send(title, builder.toString());
+ }
+ }
+
+ private void send(String title, String content) throws EmailException
+ {
+ final Email email = new HtmlEmail();
+ email.setMailSession(mailSession);
+ email.setSubject(title);
+ email.setCharset("UTF-8");
+ email.setMsg(content);
+
+ String from = mailSession.getProperty("housesearch.mail.from");
+ String to = mailSession.getProperty("housesearch.mail.to");
+ String bcc = mailSession.getProperty("housesearch.mail.bcc");
+
+ if (from != null && !(from = from.trim()).isEmpty())
+ email.setFrom(from);
+
+ if (to != null && !(to = to.trim()).isEmpty())
+ for (final String address : mailSession.getProperty(to).split(","))
+ email.addTo(address.trim());
+
+ if (bcc != null && !(bcc = bcc.trim()).isEmpty())
+ for (final String address : bcc.split(","))
+ email.addBcc(address.trim());
+
+ email.send();
+ }
+
+ private static void appendPosting(Posting posting, StringBuilder builder)
+ {
+ builder.append("<p><a href=\"").append(posting.getUrl()).append("\">");
+ builder.append(posting.getAge()).append("yo house near ").append(posting.getStation()).append(" station for ").append(posting.getPrice() / 10000)
+ .append("万円");
+ builder.append("</a>");
+ builder.append("<br>House surface: ").append(Math.round(posting.getHouseSurface())).append("m2 / ")
+ .append(Math.round(posting.getHouseSurface() * 0.3025)).append("坪");
+ builder.append("<br>Land surface: ").append(Math.round(posting.getLandSurface())).append("m2 / ").append(Math.round(posting.getLandSurface() * 0.3025))
+ .append("坪");
+
+ appendRoutes(posting, builder);
+
+ builder.append("</p>");
+ }
+
+ private static void appendRoutes(Posting posting, StringBuilder builder)
+ {
+ final Route fastestRoute = posting.getFastestRoute();
+ final Route cheapestRoute = posting.getCheapestRoute();
+ final Route easiestRoute = posting.getEasiestRoute();
+
+ // No route have been analyzed
+ if (fastestRoute == null && cheapestRoute == null && easiestRoute == null)
+ return;
+
+ builder.append("<br>Routes: ");
+
+ if (fastestRoute == cheapestRoute)
+ if (fastestRoute == easiestRoute)
+ appendRoute("Best", posting, fastestRoute, builder);
+ else
+ {
+ appendRoute("Fastest & Cheapest", posting, fastestRoute, builder);
+ appendRoute("Easiest", posting, easiestRoute, builder);
+ }
+ else if (fastestRoute == easiestRoute)
+ {
+ appendRoute("Fastest & Easiest", posting, fastestRoute, builder);
+ appendRoute("Cheapest", posting, cheapestRoute, builder);
+ }
+ else
+ {
+ appendRoute("Fastest", posting, fastestRoute, builder);
+
+ if (cheapestRoute == easiestRoute)
+ appendRoute("Cheapest & Easiest", posting, cheapestRoute, builder);
+ else
+ {
+ appendRoute("Cheapest", posting, cheapestRoute, builder);
+ appendRoute("Easiest", posting, easiestRoute, builder);
+ }
+ }
+ }
+
+ private static void appendRoute(String label, Posting property, Route route, StringBuilder builder)
+ {
+ if (route == null)
+ return;
+
+ builder.append("<br>- ").append(label).append(": ");
+ builder.append(route.getTime() + property.getWalkTimeToStation()).append("min to ").append(route.getTo());
+ builder.append(" (walk: ").append(property.getWalkTimeToStation()).append("min, train: ").append(route.getTime()).append("min)");
+ builder.append(" / ").append(route.getFare()).append("円");
+ builder.append(" / ").append(route.getTransfer()).append(" transfer");
+ }
}
diff --git a/src/main/packaged-resources/cfg/cheap-house-expression.cfg b/src/main/packaged-resources/cfg/cheap-house-expression.cfg
@@ -0,0 +1,10 @@
+
+# At least one good route
+(
+ (#timeToStation(#property, '東京駅') <= 85 && #fareToStation(#property, '東京駅') <= 650 && #transferToStation(#property, '東京駅') <= 1)
+ || (#timeToStation(#property, '三越前') <= 85 && #fareToStation(#property, '三越前') <= 650 && #transferToStation(#property, '三越前') <= 1)
+ || (#timeToStation(#property, '大手町(東京都)駅') <= 85 && #fareToStation(#property, '大手町(東京都)駅') <= 650 && #transferToStation(#property, '大手町(東京都)駅') <= 1)
+)
+
+# Mistaken with Tokyo's Akasaka
+&& !url.startsWith('https://suumo.jp/chukoikkodate/gumma/sc_maebashi')
+\ No newline at end of file
diff --git a/src/main/packaged-resources/cfg/search-conditions.cfg b/src/main/packaged-resources/cfg/search-conditions.cfg
@@ -1,22 +0,0 @@
-area=KANTO
-type=USED_HOUSE
-
-# Price of the house (in yens)
-price.min=
-price.max=20000000
-
-# Age of the house (in years)
-age.min=
-age.max=20
-
-# Surface of the house/land (in squared meters)
-surface.land.min=100
-surface.land.max=
-surface.house.min=80
-surface.house.max=
-
-# Time to go to the nearest station by foot (in minutes)
-station.walktime.max=
-
-# More complex conditions in form of a spring expression
-expression.file=search-expression.cfg
diff --git a/src/main/packaged-resources/cfg/searches.json b/src/main/packaged-resources/cfg/searches.json
@@ -0,0 +1,32 @@
+[
+ {
+ "name": "Cheap house",
+ "area": "KANTO",
+ "type": "USED_HOUSE",
+ "price.min": null,
+ "price.max": 12000000,
+ "age.min": null,
+ "age.max": 20,
+ "surface.land.min": 100,
+ "surface.land.max": null,
+ "surface.house.min": 80,
+ "surface.house.max": null,
+ "station.walktime.max": null,
+ "expression.file": "cheap-house-expression.cfg"
+ },
+ {
+ "name": "House close to Tokyo",
+ "area": "KANTO",
+ "type": "USED_HOUSE",
+ "price.min": null,
+ "price.max": 20000000,
+ "age.min": null,
+ "age.max": 20,
+ "surface.land.min": 100,
+ "surface.land.max": null,
+ "surface.house.min": 80,
+ "surface.house.max": null,
+ "station.walktime.max": null,
+ "expression.file": "tokyo-house-expression.cfg"
+ }
+]
+\ No newline at end of file
diff --git a/src/main/packaged-resources/cfg/search-expression.cfg b/src/main/packaged-resources/cfg/tokyo-house-expression.cfg