From a9650805d204dba01a81100f2b9aa5d5f5f1a86e Mon Sep 17 00:00:00 2001 From: gotty Date: Thu, 7 Nov 2019 00:43:23 +0000 Subject: [PATCH] [LIB-8] URL and HTML tag processing, new entities fields git-svn-id: https://svn.hedgecode.org/lib/snooker-score-api/trunk@174 fb0bcced-7025-49ed-a12f-f98bce993226 --- .../org/hedgecode/snooker/SnookerScoreApp.java | 20 +-- .../org/hedgecode/snooker/SnookerURLUtils.java | 150 ++++++++++++++++++ .../org/hedgecode/snooker/annotation/IsURL.java | 32 ++++ .../hedgecode/snooker/annotation/WithHTMLTags.java | 32 ++++ src/main/java/org/hedgecode/snooker/api/Event.java | 12 +- .../java/org/hedgecode/snooker/api/IdEntity.java | 2 +- src/main/java/org/hedgecode/snooker/api/Match.java | 2 +- .../java/org/hedgecode/snooker/api/Player.java | 12 ++ .../org/hedgecode/snooker/api/PlayerImage.java | 66 ++++++++ .../org/hedgecode/snooker/api/RankingType.java | 26 ++++ .../java/org/hedgecode/snooker/api/SnookerURL.java | 45 ++++++ .../java/org/hedgecode/snooker/api/URLEntity.java | 36 +++++ .../snooker/cache/assign/EventAssigner.java | 17 ++- .../java/org/hedgecode/snooker/json/JsonEvent.java | 60 +++++++- .../org/hedgecode/snooker/json/JsonIdEntity.java | 8 +- .../java/org/hedgecode/snooker/json/JsonMatch.java | 19 ++- .../org/hedgecode/snooker/json/JsonPlayer.java | 65 ++++++++ .../java/org/hedgecode/snooker/json/JsonRound.java | 2 + .../org/hedgecode/snooker/json/JsonURLEntity.java | 168 +++++++++++++++++++++ .../org/hedgecode/snooker/json/JsonEventTest.ser | Bin 1241 -> 1327 bytes .../org/hedgecode/snooker/json/JsonEventsTest.ser | Bin 25676 -> 25914 bytes .../org/hedgecode/snooker/json/JsonMatchTest.ser | Bin 949 -> 949 bytes .../org/hedgecode/snooker/json/JsonMatchesTest.ser | Bin 4028 -> 4028 bytes .../snooker/json/JsonOngoingMatchTest.ser | Bin 909 -> 909 bytes .../snooker/json/JsonOngoingMatchesTest.ser | Bin 1435 -> 1435 bytes .../org/hedgecode/snooker/json/JsonPlayerTest.json | 2 +- .../org/hedgecode/snooker/json/JsonPlayerTest.ser | Bin 651 -> 786 bytes .../hedgecode/snooker/json/JsonPlayersTest.json | 2 +- .../org/hedgecode/snooker/json/JsonPlayersTest.ser | Bin 21312 -> 24582 bytes .../org/hedgecode/snooker/json/JsonRankingTest.ser | Bin 385 -> 385 bytes .../hedgecode/snooker/json/JsonRankingsTest.ser | Bin 8292 -> 8292 bytes 31 files changed, 745 insertions(+), 33 deletions(-) create mode 100644 src/main/java/org/hedgecode/snooker/SnookerURLUtils.java create mode 100644 src/main/java/org/hedgecode/snooker/annotation/IsURL.java create mode 100644 src/main/java/org/hedgecode/snooker/annotation/WithHTMLTags.java create mode 100644 src/main/java/org/hedgecode/snooker/api/PlayerImage.java create mode 100644 src/main/java/org/hedgecode/snooker/api/SnookerURL.java create mode 100644 src/main/java/org/hedgecode/snooker/api/URLEntity.java create mode 100644 src/main/java/org/hedgecode/snooker/json/JsonURLEntity.java diff --git a/src/main/java/org/hedgecode/snooker/SnookerScoreApp.java b/src/main/java/org/hedgecode/snooker/SnookerScoreApp.java index dbd2b9a..c597807 100644 --- a/src/main/java/org/hedgecode/snooker/SnookerScoreApp.java +++ b/src/main/java/org/hedgecode/snooker/SnookerScoreApp.java @@ -199,13 +199,12 @@ public final class SnookerScoreApp { ); } - private static void printPlayer(Player player) { + private static void printPlayer(Player player) throws APIException { SnookerScoreConsole.console( String.format( - "%s%s %s [%s] (%s)", + "%s%s [%s] (%s)", INDENT, - player.surnameFirst() ? player.lastName() : player.firstName(), - player.surnameFirst() ? player.firstName() : player.lastName(), + player.fullName(), SnookerDateUtils.formatDate(player.born()), player.nationality() ) @@ -218,17 +217,15 @@ public final class SnookerScoreApp { Player player2 = api.getPlayer(match.player2Id()); SnookerScoreConsole.console( String.format( - "%s[%s %s - %s] %s %s %s-%s %s %s", + "%s[%s %s - %s] %s %s-%s %s", INDENT, SnookerDateUtils.formatDate(match.startDate()), SnookerDateUtils.formatTime(match.startDate()), SnookerDateUtils.formatTime(match.endDate()), - player1.surnameFirst() ? player1.lastName() : player1.firstName(), - player1.surnameFirst() ? player1.firstName() : player1.lastName(), + player1.fullName(), match.score1(), match.score2(), - player2.surnameFirst() ? player2.lastName() : player2.firstName(), - player2.surnameFirst() ? player2.firstName() : player2.lastName() + player2.fullName() ) ); } @@ -302,11 +299,10 @@ public final class SnookerScoreApp { Player player = api.getPlayer(seeding.playerId()); SnookerScoreConsole.console( String.format( - "%s%s. %s %s", + "%s%s. %s", indent, seeding.seeding(), - player.surnameFirst() ? player.lastName() : player.firstName(), - player.surnameFirst() ? player.firstName() : player.lastName() + player.fullName() ) ); } diff --git a/src/main/java/org/hedgecode/snooker/SnookerURLUtils.java b/src/main/java/org/hedgecode/snooker/SnookerURLUtils.java new file mode 100644 index 0000000..c345d78 --- /dev/null +++ b/src/main/java/org/hedgecode/snooker/SnookerURLUtils.java @@ -0,0 +1,150 @@ +/* + * Copyright (c) 2017-2019. Developed by Hedgecode. + * + * 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 org.hedgecode.snooker; + +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.imageio.ImageIO; + +import org.hedgecode.snooker.api.APIException; +import org.hedgecode.snooker.api.SnookerURL; + +/** + * Utils for working with URLs. + * + * @author Dmitry Samoshin aka gotty + */ +public final class SnookerURLUtils { + + private static final String CRLF = System.lineSeparator(); + + private static final String HTTP_REGEX = "(http[s]?://.+)"; + private static final String ANCHOR_REGEX = "]*>([^<]+)"; + + private static final Pattern HTTP_PATTERN = Pattern.compile(HTTP_REGEX); + private static final Pattern ANCHOR_PATTERN = Pattern.compile(ANCHOR_REGEX); + + private static final String BR_REGEX = "<[Bb][Rr][ /]*>"; + private static final String TAG_REGEX = "<[^>]+>"; + + private static final String TWITTER_URL = "https://twitter.com/"; + private static final String TWITTER_HASHTAG = "hashtag/"; + + public static List parseUrls(Map htmlStrings) throws APIException { + List result = new ArrayList<>(); + htmlStrings.forEach( (name, htmlString) -> { + Matcher matcher = ANCHOR_PATTERN.matcher(htmlString); + while (matcher.find()) { + try { + URL url = new URL(matcher.group(1)); + String text = matcher.group(2); + result.add( + new SnookerURL(text, url) + ); + } catch (IOException ignored) { + } + } + }); + return result; + } + + public static List assignUrls(Map urlStrings) throws APIException { + List result = new ArrayList<>(); + urlStrings.forEach( (name, urlString) -> { + Matcher matcher = HTTP_PATTERN.matcher(urlString); + if (matcher.find()) { + try { + URL url = new URL(matcher.group(1)); + result.add( + new SnookerURL(name, url) + ); + } catch (IOException ignored) { + } + } + }); + return result; + } + + public static SnookerURL assignUrl(String name, String urlString) throws APIException { + SnookerURL result = null; + URL url = assignUrl(urlString); + if (url != null) { + result = new SnookerURL(name, url); + } + return result; + } + + public static URL assignUrl(String urlString) throws APIException { + URL result = null; + if (urlString != null && !urlString.isEmpty()) { + Matcher matcher = HTTP_PATTERN.matcher(urlString); + if (matcher.find()) { + try { + result = new URL(matcher.group(1)); + } catch (IOException e) { + throw new APIException( + APIException.Type.INFO, "Failed to recognize URL: " + e.getMessage() + ); + } + } + } + return result; + } + + public static String cutTags(String htmlString) { + if (htmlString != null && !htmlString.isEmpty()) { + return htmlString.replaceAll( + BR_REGEX, CRLF + ).replaceAll( + TAG_REGEX, "" + ); + } + return htmlString; + } + + public static BufferedImage loadImage(URL imageUrl) throws APIException { + BufferedImage result; + try { + result = ImageIO.read(imageUrl); + } catch (IOException e) { + throw new APIException( + APIException.Type.INFO, "Failed to load image at the address: " + imageUrl + ); + } + return result; + } + + public static String twitterUrl(String twitterName) { + return twitterName != null && !twitterName.isEmpty() + ? TWITTER_URL.concat(twitterName) + : null; + } + + public static String hashtagUrl(String twitterHashtag) { + return twitterHashtag != null && !twitterHashtag.isEmpty() + ? TWITTER_URL.concat(TWITTER_HASHTAG).concat(twitterHashtag) + : null; + } + +} diff --git a/src/main/java/org/hedgecode/snooker/annotation/IsURL.java b/src/main/java/org/hedgecode/snooker/annotation/IsURL.java new file mode 100644 index 0000000..47566fc --- /dev/null +++ b/src/main/java/org/hedgecode/snooker/annotation/IsURL.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2017. Developed by Hedgecode. + * + * 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 org.hedgecode.snooker.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation Type indicates the fields and methods that are html urls. + * + * @author Dmitry Samoshin aka gotty + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD, ElementType.METHOD}) +public @interface IsURL { +} diff --git a/src/main/java/org/hedgecode/snooker/annotation/WithHTMLTags.java b/src/main/java/org/hedgecode/snooker/annotation/WithHTMLTags.java new file mode 100644 index 0000000..5a4b0cf --- /dev/null +++ b/src/main/java/org/hedgecode/snooker/annotation/WithHTMLTags.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2017. Developed by Hedgecode. + * + * 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 org.hedgecode.snooker.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation Type indicates the fields that may contain html anchor links. + * + * @author Dmitry Samoshin aka gotty + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD}) +public @interface WithHTMLTags { +} diff --git a/src/main/java/org/hedgecode/snooker/api/Event.java b/src/main/java/org/hedgecode/snooker/api/Event.java index b3fdb68..2410aae 100644 --- a/src/main/java/org/hedgecode/snooker/api/Event.java +++ b/src/main/java/org/hedgecode/snooker/api/Event.java @@ -81,8 +81,12 @@ public interface Event extends IdEntity { String twitter(); + String twitterUrl(); + String hashTag(); + String hashTagUrl(); + float conversionRate(); boolean allRoundsAdded(); @@ -101,8 +105,12 @@ public interface Event extends IdEntity { String commonNote(); - int defendingChampion(); + int defendingChampionId(); + + Player defendingChampion(); + + int previousEditionId(); - int previousEdition(); + Event previousEdition(); } diff --git a/src/main/java/org/hedgecode/snooker/api/IdEntity.java b/src/main/java/org/hedgecode/snooker/api/IdEntity.java index 699ea8f..c1ed21d 100644 --- a/src/main/java/org/hedgecode/snooker/api/IdEntity.java +++ b/src/main/java/org/hedgecode/snooker/api/IdEntity.java @@ -21,7 +21,7 @@ package org.hedgecode.snooker.api; * * @author Dmitry Samoshin aka gotty */ -public interface IdEntity { +public interface IdEntity extends URLEntity { int getId(); diff --git a/src/main/java/org/hedgecode/snooker/api/Match.java b/src/main/java/org/hedgecode/snooker/api/Match.java index b006c32..3a6d947 100644 --- a/src/main/java/org/hedgecode/snooker/api/Match.java +++ b/src/main/java/org/hedgecode/snooker/api/Match.java @@ -75,7 +75,7 @@ public interface Match extends IdEntity { int tableNo(); - String videoURL(); + String videoUrl(); Date initDate(); diff --git a/src/main/java/org/hedgecode/snooker/api/Player.java b/src/main/java/org/hedgecode/snooker/api/Player.java index 9c6c7ea..49a3e7a 100644 --- a/src/main/java/org/hedgecode/snooker/api/Player.java +++ b/src/main/java/org/hedgecode/snooker/api/Player.java @@ -43,6 +43,8 @@ public interface Player extends IdEntity { String shortName(); + String fullName(); + String nationality(); String sex(); @@ -53,6 +55,8 @@ public interface Player extends IdEntity { String twitter(); + String twitterUrl(); + boolean surnameFirst(); String license(); @@ -63,6 +67,14 @@ public interface Player extends IdEntity { String photo(); + PlayerImage image() throws APIException; + + String photoSource(); + + int firstSeasonAsPro(); + + int lastSeasonAsPro(); + String info(); } diff --git a/src/main/java/org/hedgecode/snooker/api/PlayerImage.java b/src/main/java/org/hedgecode/snooker/api/PlayerImage.java new file mode 100644 index 0000000..963d2d2 --- /dev/null +++ b/src/main/java/org/hedgecode/snooker/api/PlayerImage.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2017. Developed by Hedgecode. + * + * 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 org.hedgecode.snooker.api; + +import java.awt.image.BufferedImage; +import java.io.Serializable; +import java.net.URL; + +import org.hedgecode.snooker.SnookerURLUtils; + +/** + * Player Image Storage Entity. + * + * @author Dmitry Samoshin aka gotty + */ +public class PlayerImage implements Serializable { + + private URL url; + private BufferedImage image; + + public PlayerImage(URL imageUrl) { + url = imageUrl; + } + + public PlayerImage(URL imageUrl, boolean autoload) throws APIException { + url = imageUrl; + if (autoload) loadImage(); + } + + public URL url() { + return url; + } + + public String urlString() { + return url.toString(); + } + + public BufferedImage image() { + return image; + } + + public BufferedImage loadImage() throws APIException { + image = SnookerURLUtils.loadImage(url); + return image; + } + + @Override + public String toString() { + return urlString(); + } + +} diff --git a/src/main/java/org/hedgecode/snooker/api/RankingType.java b/src/main/java/org/hedgecode/snooker/api/RankingType.java index 9e308b6..5d7e7aa 100644 --- a/src/main/java/org/hedgecode/snooker/api/RankingType.java +++ b/src/main/java/org/hedgecode/snooker/api/RankingType.java @@ -32,6 +32,8 @@ public enum RankingType { ProvOneYearMoneyRankings (2013, Season.CURRENT_YEAR), ProjectedEndOfSeasonMoneySeedings (2014, Season.CURRENT_YEAR), ProjectedGrandPrixMoneyRankings (2014, Season.CURRENT_YEAR), + ProjectedPCMoneyRankings (2016, Season.CURRENT_YEAR), + ProjectedWCMoneySeedings (2017, Season.CURRENT_YEAR), PrevMoneyRankings (Season.CURRENT_YEAR - 1, Season.CURRENT_YEAR), PrevMoneySeedings (Season.CURRENT_YEAR - 1, Season.CURRENT_YEAR), @@ -40,6 +42,11 @@ public enum RankingType { WorldGrandPrix2015Rankings (2014, 2014), WorldGrandPrix2016Rankings (2015, 2015), + WorldGrandPrix2017Rankings (2017, 2018), + WorldGrandPrix2018Rankings (2018, 2018), + + PC2017Rankings (2017, 2017), + PC2018Rankings (2018, 2018), CombinedOrderofMerit2013 (2013, 2013), CombinedOrderOfMerit2014 (2014, 2014), @@ -82,6 +89,25 @@ public enum RankingType { SeedingsAfterGerman2016 (2015, 2015), SeedingsAfterChina2016 (2015, 2015), SeedingsAfterWorld2016 (2015, 2015), + SeedingsAfterWorldOpen2016 (2016, 2016), + SeedingsAfterPHC2016 (2016, 2016), + SeedingsAfterShanghai2016 (2016, 2016), + SeedingsAfterInternational2016 (2016, 2016), + SeedingsAfterUK2016 (2016, 2016), + SeedingsAfterScottish2016 (2016, 2016), + SeedingsAfterGerman2017 (2016, 2016), + SeedingsAfterChina2017 (2016, 2016), + SeedingsAfterWorld2017 (2017, 2017), + SeedingsAfterRiga2017 (2017, 2017), + SeedingsAfterIndian2017 (2017, 2017), + SeedingsAfterWorldOpen2017 (2017, 2017), + SeedingsAfterEuropean2017 (2017, 2017), + SeedingsAfterInternational2017 (2017, 2017), + SeedingsAfterNIO2017 (2017, 2017), + SeedingsAfterUK2017 (2017, 2017), + SeedingsAfterScottish2017 (2017, 2017), + SeedingsAfterGerman2018 (2017, 2017), + SeedingsAfterShootOut2018 (2017, 2017), OrderOfMeritAfterPTC4 (2011, 2011), OrderOfMeritAfterPTC8 (2011, 2011), diff --git a/src/main/java/org/hedgecode/snooker/api/SnookerURL.java b/src/main/java/org/hedgecode/snooker/api/SnookerURL.java new file mode 100644 index 0000000..6a976aa --- /dev/null +++ b/src/main/java/org/hedgecode/snooker/api/SnookerURL.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2017. Developed by Hedgecode. + * + * 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 org.hedgecode.snooker.api; + +import java.io.Serializable; +import java.net.URL; + +/** + * Snooker URL Entity. + * + * @author Dmitry Samoshin aka gotty + */ +public class SnookerURL implements Serializable { + + private String name; + private URL url; + + public SnookerURL(String name, URL url) { + this.name = name; + this.url = url; + } + + public String name() { + return name; + } + + public URL url() { + return url; + } + +} diff --git a/src/main/java/org/hedgecode/snooker/api/URLEntity.java b/src/main/java/org/hedgecode/snooker/api/URLEntity.java new file mode 100644 index 0000000..0b9f914 --- /dev/null +++ b/src/main/java/org/hedgecode/snooker/api/URLEntity.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2017. Developed by Hedgecode. + * + * 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 org.hedgecode.snooker.api; + +import java.util.List; + +/** + * Abstract URL Entity API Interface. + * + * @author Dmitry Samoshin aka gotty + */ +public interface URLEntity { + + List getLinks() throws APIException; + + List getURLs() throws APIException; + + SnookerURL getURL(String name) throws APIException; + + String withoutTags(String name) throws APIException; + +} diff --git a/src/main/java/org/hedgecode/snooker/cache/assign/EventAssigner.java b/src/main/java/org/hedgecode/snooker/cache/assign/EventAssigner.java index 8c2cc8b..8ea144b 100644 --- a/src/main/java/org/hedgecode/snooker/cache/assign/EventAssigner.java +++ b/src/main/java/org/hedgecode/snooker/cache/assign/EventAssigner.java @@ -18,6 +18,7 @@ package org.hedgecode.snooker.cache.assign; import org.hedgecode.snooker.api.APIException; import org.hedgecode.snooker.api.Event; +import org.hedgecode.snooker.api.Player; import org.hedgecode.snooker.json.JsonEvent; /** @@ -40,14 +41,28 @@ public class EventAssigner extends CacheIdAssigner { @Override public void assign(Event event) throws APIException { + JsonEvent jsonEvent = (JsonEvent) event; if (event.mainEventId() != event.eventId() && event.mainEventId() > 0) { - JsonEvent jsonEvent = (JsonEvent) event; jsonEvent.setMainEvent( cacheScore.getCachedEvent( event.mainEventId() ) ); } + if (event.previousEditionId() > 0) { + jsonEvent.setPreviousEdition( + cacheScore.getCachedEvent( + event.previousEditionId() + ) + ); + } + if (event.defendingChampionId() > 0) { + jsonEvent.setDefendingChampion( + cacheScore.getCachedPlayer( + event.defendingChampionId() + ) + ); + } } } diff --git a/src/main/java/org/hedgecode/snooker/json/JsonEvent.java b/src/main/java/org/hedgecode/snooker/json/JsonEvent.java index 0ba62ab..e7a256d 100644 --- a/src/main/java/org/hedgecode/snooker/json/JsonEvent.java +++ b/src/main/java/org/hedgecode/snooker/json/JsonEvent.java @@ -21,8 +21,12 @@ import java.util.Date; import com.google.gson.annotations.Expose; import com.google.gson.annotations.SerializedName; +import org.hedgecode.snooker.SnookerURLUtils; +import org.hedgecode.snooker.annotation.IsURL; +import org.hedgecode.snooker.annotation.WithHTMLTags; import org.hedgecode.snooker.api.Event; import org.hedgecode.snooker.api.EventFormat; +import org.hedgecode.snooker.api.Player; import org.hedgecode.snooker.api.Season; /** @@ -66,6 +70,7 @@ public class JsonEvent extends JsonIdEntity implements Event { private String sex; @SerializedName("AgeGroup") private String ageGroup; + @IsURL @SerializedName("Url") private String url; @SerializedName("Related") @@ -96,6 +101,7 @@ public class JsonEvent extends JsonIdEntity implements Event { private float conversionRate; @SerializedName("AllRoundsAdded") private boolean allRoundsAdded; + @IsURL @SerializedName("PhotoURLs") private String photoUrls; @SerializedName("NumCompetitors") @@ -106,14 +112,20 @@ public class JsonEvent extends JsonIdEntity implements Event { private int numActive; @SerializedName("NumResults") private int numResults; + @WithHTMLTags @SerializedName("Note") private String note; + @WithHTMLTags @SerializedName("CommonNote") private String commonNote; @SerializedName("DefendingChampion") - private int defendingChampion; + private int defendingChampionId; + @Expose + private Player defendingChampion; @SerializedName("PreviousEdition") - private int previousEdition; + private int previousEditionId; + @Expose + private Event previousEdition; protected JsonEvent() { } @@ -191,14 +203,16 @@ public class JsonEvent extends JsonIdEntity implements Event { @Override public Event mainEvent() { - if (mainEvent == null && mainEventId == eventId) + if (mainEvent == null && mainEventId == eventId) { mainEvent = this; + } return mainEvent; } public void setMainEvent(Event event) { - if (event != null && eventId == event.eventId()) + if (event != null && eventId == event.eventId()) { mainEvent = event; + } } @Override @@ -273,11 +287,23 @@ public class JsonEvent extends JsonIdEntity implements Event { return twitter; } + @IsURL + @Override + public String twitterUrl() { + return SnookerURLUtils.twitterUrl(twitter); + } + @Override public String hashTag() { return hashTag; } + @IsURL + @Override + public String hashTagUrl() { + return SnookerURLUtils.hashtagUrl(hashTag); + } + @Override public float conversionRate() { return conversionRate; @@ -324,15 +350,37 @@ public class JsonEvent extends JsonIdEntity implements Event { } @Override - public int defendingChampion() { + public int defendingChampionId() { + return defendingChampionId; + } + + @Override + public Player defendingChampion() { return defendingChampion; } + public void setDefendingChampion(Player champion) { + if (champion != null && defendingChampionId == champion.playerId()) { + defendingChampion = champion; + } + } + @Override - public int previousEdition() { + public int previousEditionId() { + return previousEditionId; + } + + @Override + public Event previousEdition() { return previousEdition; } + public void setPreviousEdition(Event event) { + if (event != null && previousEditionId == event.eventId()) { + previousEdition = event; + } + } + @Override public int getId() { return eventId; diff --git a/src/main/java/org/hedgecode/snooker/json/JsonIdEntity.java b/src/main/java/org/hedgecode/snooker/json/JsonIdEntity.java index 25207b4..eb6491e 100644 --- a/src/main/java/org/hedgecode/snooker/json/JsonIdEntity.java +++ b/src/main/java/org/hedgecode/snooker/json/JsonIdEntity.java @@ -21,12 +21,14 @@ import java.io.Serializable; import org.hedgecode.snooker.api.IdEntity; /** - * Abstract Entity to JSON deserialize. + * Abstract ID Entity to JSON deserialize. * * @author Dmitry Samoshin aka gotty */ -public abstract class JsonIdEntity implements IdEntity, Serializable { - +public abstract class JsonIdEntity + extends JsonURLEntity + implements IdEntity, Serializable +{ @Override public boolean equals(Object obj) { if (this == obj) diff --git a/src/main/java/org/hedgecode/snooker/json/JsonMatch.java b/src/main/java/org/hedgecode/snooker/json/JsonMatch.java index 22248dd..cda1b86 100644 --- a/src/main/java/org/hedgecode/snooker/json/JsonMatch.java +++ b/src/main/java/org/hedgecode/snooker/json/JsonMatch.java @@ -21,6 +21,8 @@ import java.util.Date; import com.google.gson.annotations.Expose; import com.google.gson.annotations.SerializedName; +import org.hedgecode.snooker.annotation.IsURL; +import org.hedgecode.snooker.annotation.WithHTMLTags; import org.hedgecode.snooker.api.Event; import org.hedgecode.snooker.api.Match; import org.hedgecode.snooker.api.Player; @@ -68,8 +70,10 @@ public class JsonMatch extends JsonIdEntity implements Match { private boolean onBreak; @SerializedName("WorldSnookerID") private int worldSnookerId; + @IsURL @SerializedName("LiveUrl") private String liveUrl; + @IsURL @SerializedName("DetailsUrl") private String detailsUrl; @SerializedName("PointsDropped") @@ -82,8 +86,9 @@ public class JsonMatch extends JsonIdEntity implements Match { private int type; @SerializedName("TableNo") private int tableNo; + @IsURL @SerializedName("VideoURL") - private String videoURL; + private String videoUrl; @SerializedName("InitDate") private Date initDate; @SerializedName("ModDate") @@ -94,14 +99,18 @@ public class JsonMatch extends JsonIdEntity implements Match { private Date endDate; @SerializedName("ScheduledDate") private Date scheduledDate; + @WithHTMLTags @SerializedName("FrameScores") private String frameScores; + @WithHTMLTags @SerializedName("Sessions") private String sessions; + @WithHTMLTags @SerializedName("Note") - private String note; + private String note; + @WithHTMLTags @SerializedName("ExtendedNote") - private String extendedNote; + private String extendedNote; protected JsonMatch() { } @@ -252,8 +261,8 @@ public class JsonMatch extends JsonIdEntity implements Match { } @Override - public String videoURL() { - return videoURL; + public String videoUrl() { + return videoUrl; } @Override diff --git a/src/main/java/org/hedgecode/snooker/json/JsonPlayer.java b/src/main/java/org/hedgecode/snooker/json/JsonPlayer.java index 04ac5da..aa481ed 100644 --- a/src/main/java/org/hedgecode/snooker/json/JsonPlayer.java +++ b/src/main/java/org/hedgecode/snooker/json/JsonPlayer.java @@ -16,11 +16,17 @@ package org.hedgecode.snooker.json; +import java.net.URL; import java.util.Date; +import com.google.gson.annotations.Expose; import com.google.gson.annotations.SerializedName; +import org.hedgecode.snooker.SnookerURLUtils; +import org.hedgecode.snooker.annotation.IsURL; +import org.hedgecode.snooker.api.APIException; import org.hedgecode.snooker.api.Player; +import org.hedgecode.snooker.api.PlayerImage; /** * Player Entity to JSON deserialize. @@ -47,10 +53,13 @@ public class JsonPlayer extends JsonIdEntity implements Player { private int teamSeason; @SerializedName("ShortName") private String shortName; + @Expose + private String fullName; @SerializedName("Nationality") private String nationality; @SerializedName("Sex") private String sex; + @IsURL @SerializedName("BioPage") private String bioPage; @SerializedName("Born") @@ -63,10 +72,21 @@ public class JsonPlayer extends JsonIdEntity implements Player { private String license; @SerializedName("Club") private String club; + @IsURL @SerializedName("URL") private String url; + @IsURL @SerializedName("Photo") private String photo; + @Expose + private PlayerImage image; + @IsURL + @SerializedName("PhotoSource") + private String photoSource; + @SerializedName("FirstSeasonAsPro") + private int firstSeasonAsPro; + @SerializedName("LastSeasonAsPro") + private int lastSeasonAsPro; @SerializedName("Info") private String info; @@ -119,6 +139,19 @@ public class JsonPlayer extends JsonIdEntity implements Player { } @Override + public String fullName() { + if (fullName == null) { + fullName = String.format( + "%s %s %s", + surnameFirst ? lastName : firstName, + middleName, + surnameFirst ? firstName : lastName + ).replaceAll("\\s+", " ").trim(); + } + return fullName; + } + + @Override public String nationality() { return nationality; } @@ -145,6 +178,12 @@ public class JsonPlayer extends JsonIdEntity implements Player { return twitter; } + @IsURL + @Override + public String twitterUrl() { + return SnookerURLUtils.twitterUrl(twitter); + } + @Override public boolean surnameFirst() { return surnameFirst; @@ -171,6 +210,32 @@ public class JsonPlayer extends JsonIdEntity implements Player { } @Override + public PlayerImage image() throws APIException { + if (image == null) { + URL imageUrl = SnookerURLUtils.assignUrl(photo); + if (imageUrl != null) { + image = new PlayerImage(imageUrl, false); + } + } + return image; + } + + @Override + public String photoSource() { + return photoSource; + } + + @Override + public int firstSeasonAsPro() { + return firstSeasonAsPro; + } + + @Override + public int lastSeasonAsPro() { + return lastSeasonAsPro; + } + + @Override public String info() { return info; } diff --git a/src/main/java/org/hedgecode/snooker/json/JsonRound.java b/src/main/java/org/hedgecode/snooker/json/JsonRound.java index 7dbb3b1..78995ff 100644 --- a/src/main/java/org/hedgecode/snooker/json/JsonRound.java +++ b/src/main/java/org/hedgecode/snooker/json/JsonRound.java @@ -19,6 +19,7 @@ package org.hedgecode.snooker.json; import com.google.gson.annotations.Expose; import com.google.gson.annotations.SerializedName; +import org.hedgecode.snooker.annotation.WithHTMLTags; import org.hedgecode.snooker.api.Event; import org.hedgecode.snooker.api.Round; @@ -47,6 +48,7 @@ public class JsonRound extends JsonIdEntity implements Round { private int numLeft; @SerializedName("NumMatches") private int numMatches; + @WithHTMLTags @SerializedName("Note") private String note; @SerializedName("ValueType") diff --git a/src/main/java/org/hedgecode/snooker/json/JsonURLEntity.java b/src/main/java/org/hedgecode/snooker/json/JsonURLEntity.java new file mode 100644 index 0000000..b625f52 --- /dev/null +++ b/src/main/java/org/hedgecode/snooker/json/JsonURLEntity.java @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2017. Developed by Hedgecode. + * + * 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 org.hedgecode.snooker.json; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.hedgecode.snooker.SnookerURLUtils; +import org.hedgecode.snooker.annotation.IsURL; +import org.hedgecode.snooker.annotation.WithHTMLTags; +import org.hedgecode.snooker.api.APIException; +import org.hedgecode.snooker.api.SnookerURL; +import org.hedgecode.snooker.api.URLEntity; + +/** + * Abstract URL Entity. + * + * @author Dmitry Samoshin aka gotty + */ +public abstract class JsonURLEntity implements URLEntity { + + @Override + public List getLinks() throws APIException { + List links = null; + Map htmlStrings = getAnnotatedStrings(WithHTMLTags.class); + if (!htmlStrings.isEmpty()) { + links = SnookerURLUtils.parseUrls(htmlStrings); + } + return links; + } + + @Override + public List getURLs() throws APIException { + List urls = null; + Map urlStrings = getAnnotatedStrings(IsURL.class); + urlStrings.putAll(getAnnotatedMethodStrings(IsURL.class)); + if (!urlStrings.isEmpty()) { + urls = SnookerURLUtils.assignUrls(urlStrings); + } + return urls; + } + + @Override + public SnookerURL getURL(String name) throws APIException { + String urlString = getAnnotatedString(IsURL.class, name); + if (urlString == null) { + urlString = getAnnotatedMethodString(IsURL.class, name); + } + if (urlString != null && !urlString.isEmpty()) { + return SnookerURLUtils.assignUrl(name, urlString); + } + return null; + } + + @Override + public String withoutTags(String name) throws APIException { + String tagString = getAnnotatedString(WithHTMLTags.class, name); + if (tagString != null && !tagString.isEmpty()) { + return SnookerURLUtils.cutTags(tagString); + } + return null; + } + + private Map getAnnotatedStrings( + Class annotationClass + ) { + Map result = new HashMap<>(); + Class clazz = getClass(); + for (Field field : clazz.getDeclaredFields()) { + if (field.isAnnotationPresent(annotationClass) + && field.getType().equals(String.class)) + { + field.setAccessible(true); + try { + result.put( + field.getName(), + (String) field.get(this) + ); + } catch (Exception ignored) { + } + } + } + return result; + } + + private String getAnnotatedString( + Class annotationClass, + String fieldName + ) { + String result = null; + Class clazz = getClass(); + for (Field field : clazz.getDeclaredFields()) { + if (field.isAnnotationPresent(annotationClass) + && field.getName().equals(fieldName) + && field.getType().equals(String.class)) + { + field.setAccessible(true); + try { + result = (String) field.get(this); + } catch (Exception ignored) { + } + } + } + return result; + } + + private Map getAnnotatedMethodStrings( + Class annotationClass + ) { + Map result = new HashMap<>(); + Class clazz = getClass(); + for (Method method : clazz.getMethods()) { + if (method.isAnnotationPresent(annotationClass) + && method.getReturnType().equals(String.class) + && method.getParameterCount() == 0) + { + try { + result.put( + method.getName(), + (String) method.invoke(this) + ); + } catch (Exception ignored) { + } + } + } + return result; + } + + private String getAnnotatedMethodString( + Class annotationClass, + String methodName + ) { + String result = null; + Class clazz = getClass(); + for (Method method : clazz.getMethods()) { + if (method.isAnnotationPresent(annotationClass) + && method.getName().equals(methodName) + && method.getReturnType().equals(String.class) + && method.getParameterCount() == 0) + { + try { + result = (String) method.invoke(this); + } catch (Exception ignored) { + } + } + } + return result; + } + +} diff --git a/src/test/resources/org/hedgecode/snooker/json/JsonEventTest.ser b/src/test/resources/org/hedgecode/snooker/json/JsonEventTest.ser index 356fb15588794e7bf56dfb537646890ee12044bf..51976bd57ed2cb6040e222d5e59e40b2338c670d 100644 GIT binary patch delta 151 zcmcb~xt?o+4)>fb765BiG=%^F delta 92 zcmZ3_b(3>~4!4cdgvsgiwjN<((41(lA|#lSnwFZEl9`w8oROGYkeQ!1F~3xRzo00! vEHl5f*fk}y1SIyYWb$IhbCWxmrZF;YmS^^3oSerJKKU4nHY4xkPb{_oK#3v# diff --git a/src/test/resources/org/hedgecode/snooker/json/JsonEventsTest.ser b/src/test/resources/org/hedgecode/snooker/json/JsonEventsTest.ser index 3d5f9b4253adb1d67501b939930bbb8c627b0c9d..46c3604279669f299a2edff4303ee4c341602ee8 100644 GIT binary patch delta 3438 zcmZ{nYiyHc6vx{ETesPDUAMKRyL9Z{T^L*0g~Gaa7uX!^zIE-`eZazOcSD1K@WByu zL@p;NL&0DO9|WAsNCbk$iBY2?EQ&I58u;J`ViZJlnh1EF_p-M0G-;amZGPu}F3)++ z^K|+P>6I^~?~h7@PpY0CF!dcf?I%ft*QE*Js_vdGJML z!Lam!v>%o)v*Gv2l-MB63o?mBTK{Ni5!4v8aBw4B+TJrb#7s3X=AVi9 z=l*e04czm;NDvnSvXF4eqhQA7FV!5C{YpTOWf>f#0c)U2s+nvIR7(kLPZ0BVGDQog zT2TwW3VKLT*MsAcdJw!4h4jyaQ9@QZ?{9^WW#i?XJ8;(=4&(BDgyLOgfy^>2k>IBv z;=+s}{EM_ksKQjp8QKcoQv_fvXae^Xa*TK?q!3nzZlP&enTHRERf-1Nm9#+-Cv6dA zPlwS|0aUBRAVyW9<}H;ET2|r_L{X$v+6|@rtM3DvU-aqrJkRhTT!J5Gz-Mg@O&}_3$+MEl&sV zI7SR2Dpm}lC)QK6Lbgy!76kv|p?S$euLhr4Lhr_jT7SfOX~CUL!C%Pw#le|iO}rN@ z+aRu?J|U4SP(1V|ytd#^k+=b$n-W)w8)sgU1p}p#lV=kXs0F#5WJX?_%$n4Z{DkQH z-V`GeIB<~oFlB!E|7Hv~hMIQ&d;rV^E8u==I^sG*I^r7){}6&TY57RJl9r6bL>dsp zz6_(gz!T-+a3eztSJPSjHf2>pWQLtIGNCL_CkdoAlO~Nq%?0hnb!SeTD}9){S3r!c zD2tEmEE$#2-LRrT-zqVc&sI@hva-cg_GXKzJj|v@tdy{4#8isCTZp9+WAtUpce)tX z10xITiIJGuM{~2e;;NxMHw?#iOKu<4O|yB4h&|5B6S}YIT;ZoUwj-b}{~Mw>xdoMo z>kLW4aWP)-8$n%IlZMpnnp8p57WL|Rm@3mjl4*{>!`4~?u}ASN@ayV=tlAh1_r1Ot&P9COoPt%FHUCZ!-xskm1zWKBC-W^j%4e6AA;+xg zVm9~3w{a!9V?9JD?k#5fIn*94;LVPkk8nS?LS@Mif!`?E#p4*_w$j@^Hf!@pz)euL zcfU0@OM^D?gzI}%EG~8-tKKLz= zX+KV=e`jZe!jUahi!)`{(Kg2zfy-KiU@$Fb%a||!qvuo5zYa{B*?@EJar^#}X>O1I zvbmj5ceJv#aLL8HvHkGZFLP7tlhzReZ)#&x%O%&q8x44Mhs$kL$eryaNb!v|-Tncg z108Iw%ylusu>PTAoS;s0GH)}Ttn>cur2cPw+{Ln;=}d&sZhxVk4rlS-TqCXSR{sZG C4k=Cm delta 3142 zcmZ`*YfPI}81BcdT*s~O70RX1LbS^*E7PuK3VAC8)(Xiq9G-y~L9I{xg z1HFSoCk1B&G(7fFL98$ig|)&D`C%{b47eW<1yg!21TlO6$%mMHln5e*d7n^gR(U)fa;a>V^;vOoZ%L_X~BQ zu6Iy>8tSN?A|j|SiZt*{v|oKR%!PWngL+@Mqxx((K|O`mzyQ5p{STU;-X+e3S+N|o z?XEbJ&^9X40M(HSU-|`afq-g$5a|tDo?^HZIR~9m8r41{)&R;ZAAvB8uR)tx1Qn6#&Wzzv3op&kSht&K4AY& z)wA=kCH-Vq#IW0Uyoj`TPHZIJQLK`!Z<~zU#2uMK9OmWrP27>&`7+p)PqH~?67=v( z0ychNqK;6(Zbd1oU^INHko(xHXe^C@NliGoCq3j_^+{5e)5KRL6DGbs#R%`Dq_JHw z4qjeEU5SA0l<)Y04^#6XB29q`tW5)cSeu@;Tgy0Q0fO1Gs^M*=nGEHoC{C$_(<)pI z4F6U|gckya-Aw0T9vMVf7E@mgeUQOH)tN4!*iQ(0zfBGeRkafbLS(*_qbVK7v%#>XEiOoj2nWQx9VvyhPO zc99l^!@#dGn3XdX+P;Bn`tSu^@j(LNe2Lb+yalRMM2IJ9a-dCNu~PHgBC^wsui>A!7ec$ed9v9nb23Iwk>2OSCZ4-j$0L(J1|u zvUL}f0lhazfiKDn?C_Y1b$;=DD)siLt@0K>nxSW-ujw!Gqs3L3Oy9>3V|z?jT_r#a zP7uQ^0aBGBw3|}kq^5=Y&$W=@d)^cQms|^L)h^)N(TY8~2mku83;5oV;0Rx>aRKMf zT4(i@S{HEcVs(Zu*Ad1xw>}$JBm_MoJj#1|EyDt;g1r)^tMHJCV5$Beeu+M7P{CG1 zD(Xpex#keCw3rOR-`{N*I?6;MLu5Iq(FvF1WVH{{@=zW#9X?X1x%uh03lrsS^xk5 delta 40 wcmdnWzLkA~4#%gbQ)8pV{cJays4y`F`Am*tvg25H%3z$S10Wv-gI{*Lx diff --git a/src/test/resources/org/hedgecode/snooker/json/JsonMatchesTest.ser b/src/test/resources/org/hedgecode/snooker/json/JsonMatchesTest.ser index aa94b374a7a905dc2cc305f82c929d6602e7f7df..02d6906cd123249ea1fe13a897edb8ddfae6d0e1 100644 GIT binary patch delta 76 zcmV-S0JHzRAG{xsE)R>nfuzg4y)gm+0C;i$FO$&&ACX)T2rID8H#ZC@2(fe+0SL3J iZ&x@>!Wpxa0VDzfa%_{j13m~q)+jlk0@EY11iT9;SQ?lB delta 76 zcmV-S0JHzRAG{xsE)Ra!UKvxfyaWOO0C;i$FO$&&ACX)T2$&Ww$c29P?Xh$i0SNNt im0emFPCc`f0VDzfQcRP&13n0@)Ye^=88=h11iTA5r5s=Y diff --git a/src/test/resources/org/hedgecode/snooker/json/JsonOngoingMatchTest.ser b/src/test/resources/org/hedgecode/snooker/json/JsonOngoingMatchTest.ser index 65acd8d743e9e949aabdf1b46a8ba2ce08963626..71ee53e75a8d3df8e15e2c39e525fb5af98601dd 100644 GIT binary patch delta 51 zcmV-30L=f52aN}iGYFGPx0AiiOz)99D+sfzZ&x@>!Wpq{Bmx3*Y?G7%J_tb8C^?`4 J(<77G0<#l76ej=x delta 51 zcmV-30L=f52aN}iGYAU{rzB+%I>?bbD+u!Cm0emFPCc=1Bmx3bOp}xXJ_xSV)?Jnv JH&c_^0<*dK5_A9n diff --git a/src/test/resources/org/hedgecode/snooker/json/JsonOngoingMatchesTest.ser b/src/test/resources/org/hedgecode/snooker/json/JsonOngoingMatchesTest.ser index a94696abdf8fc4a9c1b47f8619e11da222724e1f..609476139b9bac6241729d0410a46f24563a395b 100644 GIT binary patch delta 79 zcmV-V0I>g?3!4j&a|kQ2&Nnvg?3!4j&a|oCgF35#`_U*BI9{~>w3#TMy5IV>L004M$03?%d0Ur%bVRU0? l^5vCXS{F_|v(*740s>M@llucc2(HxDU6vU)Q?o$?S^;EZ8|?r9 diff --git a/src/test/resources/org/hedgecode/snooker/json/JsonPlayerTest.json b/src/test/resources/org/hedgecode/snooker/json/JsonPlayerTest.json index fe2481a..b11ae61 100644 --- a/src/test/resources/org/hedgecode/snooker/json/JsonPlayerTest.json +++ b/src/test/resources/org/hedgecode/snooker/json/JsonPlayerTest.json @@ -1 +1 @@ -[{"ID": 1,"Type": 1,"FirstName": "Mark","MiddleName": "J","LastName": "Williams","TeamName": "","TeamNumber": 0,"TeamSeason": 0,"ShortName": "M J Williams","Nationality": "Wales","Sex": "M","BioPage": "http:\u002F\u002Fsnooker.org\u002Fplr\u002Fbio\u002Fmwilliams.shtml","Born": "1975-03-21","Twitter": "markwil147","SurnameFirst": false,"License": "","Club": "","URL": "","Photo": "http:\u002F\u002Fsnooker.org\u002Fimg\u002Fplayers\u002FMarkWilliams.png","Info": ""}] \ No newline at end of file +[{"ID": 1,"Type": 1,"FirstName": "Mark","MiddleName": "J","LastName": "Williams","TeamName": "","TeamNumber": 0,"TeamSeason": 0,"ShortName": "M J Williams","Nationality": "Wales","Sex": "M","BioPage": "http:\u002F\u002Fsnooker.org\u002Fplr\u002Fbio\u002Fmwilliams.shtml","Born": "1975-03-21","Twitter": "markwil147","SurnameFirst": false,"License": "","Club": "","URL": "","Photo": "http:\u002F\u002Fsnooker.org\u002Fimg\u002Fplayers\u002FMarkWilliams.png","PhotoSource": "","FirstSeasonAsPro": 1992,"LastSeasonAsPro": 0,"Info": ""}] \ No newline at end of file diff --git a/src/test/resources/org/hedgecode/snooker/json/JsonPlayerTest.ser b/src/test/resources/org/hedgecode/snooker/json/JsonPlayerTest.ser index e95ccd56ca6b31fcb9699b421c7a5cdb5be62e23..55712f3226e03de4dce3d797288291e1adfd326b 100644 GIT binary patch delta 182 zcmeBXoy0al*PvqU#iO79e*MhEAnC~U zKu%(1YLO>Mq4mU*@jToG8Tlpo!TF^{$*GeS8OuZ+u4z~w%lQIEalcE7{4X)JIU6vU)Qvv`0cyN;@ N0&P!Yh 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 J6WVU={{j1xYBT@< literal 21312 zcmbVU36LDsd7hnnc6L_>;xvX92800ZYK1H#Y>qv&_oW@-l@!RTccyoycW1gM-91{Z zA}a#SVdGRt446{IAQGSm5QobYHsFYg6OIZob_f+N7iYy7sMs!I5=bEbyZ+bHJ9@1` zwFT2X@Bjbr{qKMO```cmH}`!INw`krOxr28myANu$lC>@-L-6c-f-IIT-$0Ng8xjE z^+m&RZ{Bv@nUD1U+gLOb+2ceyLWp|oa@ojxrfqGpJkwje<+%2zuipRr52BH#kx1Gg z?@Yt>BFjeR^xb+#%`?j#qk8qLd#Zrwc=E2DylWpZt$Cv`pt~jbcH`l(%a)f9e*#Q) zMzVT7Z@BKbQ!pF=v_jJG)``^1PyWMkyM7*vMEfGmWm_-w>3Pp~1|u1-N9YVXxOgM5=O*Jl1GvOkg?iDYJpv@sCNU&Kvo*5;qnWxejZRN2fMmMfYz zSIk18Z1{+?mQD;oFM}O$9*Jtn_5{P2YwQt%VN@Ky;56;81d{R=OwThM-ab}y%Jd@) z#+nd44;Gjy{@{fdFTeT3ujFuy$f%2dO0I9Sz`zQP zwN{UtQcB2-1N`?`mr1BF-T=5gZ&wg;3=xA)<-Y)8H)kB+h!aD4#bglUJ$4nrrnXo` zhV|SK9a9E`G>EnZzX_-{jv(U_;u5u9@ZN>O6cR9wpgW?(XkSg}Gc#aYMQs@V<3-L4 z0a=-?wa-=g_%mf@DAUN=MRtecL_*azZ7@L!-0 zSFdjR&eG|a7_u$m6q_PiL-&@MCZClBpZHCQ4U~;w~B5@Fn29$ceLh-k5<%izv5)yr&fK zC0a`x!fp$|ZsZ!oytQU*t582>Kl|*nH~z-x72`2x7L79PQoI}5F74c!kj@b*ITFb+ z4cTNVZ}PZ)J@Cb@EW@rW3v|W5>KwCvOc=ggDHFZ=ZWC=rjzE|XC+j37l}MY(J5BOy zt-y1Eu;0+RzRqPXN4(1wczdVsc~mI1TX&pAs>;-WzA$gfX(}N%a<_gLGE-udL9=NF z(Ji4D61#@l81PHQ#}SD9;1NxC*Qs6G7d4;&4Z)nDH}FD5F6VpSgO-(*!m}FgFxG63~O3` z?Z>(bv91bWZKM(5A?6lJrU&~x8o=^Z9bCjycinZ1u;wANQeozim?Dko2-Ni_)M*nA z8YPw;N{{nY6N-fz_VMD`DKD5-fqqf9@*Io3JvPu=zY{rc<#Tt7ZgiDrd2EvG zuE9RB!MKE3Jfo9cV8+wj)ji%fGCtNjKIR{f;}GF-3WNtgeRDlWnCh~;l5H_nCr9)B zuqzO{_Qlp=AQ|Bjkh95%Z8Vv*g_;yprxu57MnXs)4 zo%)1EDyD@zYJR3KrYX7tHz`eK>8Qs670h3z*f3yo1rbm=bsib8CojaZw& zBJ~Bt`~?N(OaJGobA?J_k@8FiI?->}$_)aPvqAzCN+!WFJ+{%ei`{y;>;^!dj37@A zfK)|V6-?KOi7fvKz@!4cTPL0!!ovdU6W( zxOFq!jepm2&U)eQ_wI@<=-8A;d{gA+=uOyw&W^XX~+y{37QRW21c z!$h^$XaG!66X&EMeocY@x+~YcFU+M^hy5pWPl+i5##X3VY%ju$g%Smkw-qSc1;Q|( z;?)RowE|+>Z9~FIq!VaRMTLE}qLU>;~50viOo)I$Yy{Nxdoj>Q*7*(kAb%+8IGk5ItELdglUq}2XUt7Y{O-YTg1n@Wql0- zUUMYC@cwr96WmY^FFJfFPeRJ8p~DUHYLo>@m;tp!oF3+{s1mZY5lWyiiH)9(fX@y9 zZZwV}>Z%$uu#zot7x8D}j|*=-VL${;MJKk9#$qU>!w4GzN+$cmMlw|`P}#r^`*1Gy z;oLwU8pRr-4|*jJ23HfwPni)){Um#XaC%*0$w`goM@ehZ4PQVL=1(|fuqQ1#@Sf%% z{M}iH-B}mtj^FM5#qWMs6(~m_iA#ZzWxE7nVxx5i^hbgkrp8v1_bvGvqcS^CdWS-~ zea$g9a_Nvpm1&(g?8TV>1vzr48`+Y7Z6H)Z{iJCohp|I+>k-|01-kslD@KHFUff>E%n{j_Gi~%4#*39$`8oR6$tF z1mhe#_wu9MfG9U8P-aqpg~mT=^DF=a^CO1cAa-dqzx?CM=z=KQ2;*t?UBKaQM35U5 zAUCaCy^*Uj+3)B^g$--G*Y?o(d=oTGlY}gY5eE3UB4rI*bT4n~c?i*qb-w<~i1o_~ ztZUypbUMfC%jKtFD`puD(Sl?-PwVV#6AIj<5ZL;nBhtX}QFC6XruKPktQV+)n#opx zX#u_n4Bdr@?m`8+{I734Urf#68jsbIqxx)_hsn_6B&^m8I_x7u&5M#rfUxBGIJFY~ z?3qGsn}_ELBk^a~FGAoKDS-dszO`bbo0v57^L(2N2VI|QE6(U7bTZKNZ-qmcFHp8% zZV*#acx0`<7=d1_0J=Q&l<+gjKEo;M%*&+4*K|9kFKd92P?(m-DV>Cx>4TGuI$q0W zsT_NT2xy0+aB)D_gNS<+h^s&R_0wXCkWD$yR>RExJpP)JNwiF>X~>KkY=3b)Ns<;| zZo`2)uMVhJb1#DKRe&Bo`L&aUAx|#yjcKfxm@N}2LiVskDIta|!=i+om?Y1XPsV|7 zH=I|XkEqrmL^z~C_@}$7ZwrK71s)+JdTX<@h*7SBfSGumRF@_jB)VYU^a2hPJc5Ep z6oRk4?Pj5xSWd|8*3CvIQDLhuC6lUQs)hd^yRfj(?&_5p!WLi#rpi_Xx>b>!xqbhD z*gD1``2iUi;GEc5f+7PP#iLBk5ON8#nexz(CxMMhycDrrs!-zhfA;DDF?vIKo}aXj zn6ACx&YJ?B9KbL$6Oo4Pean#(C%VUa6wlsggnP6hP)Zu#^M0ewivAhE%r9%zb8 zjtsP7M|V{HMQ8jKra3imR`4LE-LI$ltny2|U zzkHZOtUL2b4{!=GedRO@Tb%@D`@~rP5%)j4u^+nw{a}4%1wGr2eV`!blZ+-)a2kbf zwh0y!iLH?m{F_)#DF-^+t95IBe#BI9?(9L7dlV@5+$UH{hK1A< zd;`UOLm}?swXcXJIaMca!2UdGB|;^KjX`Q2FkusDafXQR0OC8Kz<1MauZ!i-f7a04 z0|gV19{5~`<&riCl>kP7S0TWw6aeS`=@DE^s6U{?kcNUNy#T!y<06ee%%}uq5L*%o zBno>w{Rpo{gjXwK?t9k0CXSS1J4-w}(IaXyq08bh6h0EymL}h6qGg>*zJ~(8r%=gH z-an8Q{s8K%c*$0>M=$x#r=>!;SR%-av{3@qG%$Y-qVR(X;oCp$TE&IO#|(ZTu{3ftq4ib;61(#u~*=V}aNyVL24<3JZifG{7W>1O7GsR_ws7iVpN$bntZEfixV9 zLh%yM>VpCu@fkEF6&SY8=P6y#o?5|4_2^YkH-mYlI}zud3Y=#4Utjf&GG8lGq>{;( zW|JuEhB-Z?@7=p;{dt@H=6pBGy;~u7_1iBV=5poP(}W@Fr|L8l7Ee&JpkkK6Ia3kR zHf3@=2A};9??H(71RypV1hKHs7i>5R^A_umZ^${?Yl|W{iVgEj5uEqY#&kN6-eB4& zY}w?5vOk$&&CjfD-mo~5&drbmd6e%%}Bmw%zZHFntvT=jK}MSYczNW(PJh<}wI z4V3q^^7Y48Ag8z z|4}StllLg1dQ{<(ue?R;<%*2=8f8|%9o>P2+`=P==Fx#x!b$C@X~>NLLsU;9swV@w zYSd+6wsQ+`&ZJvSpj!XT*?sUAxGKr>F>voPJ>1~LJgn}AaH2dU*wm;dxn`=c zdf4T_I{G{!eqMq2&hI`Xjt$^+pjzeU$(fvPSjBl?na5EL;!7y=C524qye2VONzrN* zOvvQ>#nh$Nx-r`>2@~;24Rec}QtGIIk-d!gURD^{6$_7xr8zYLYM3=xS}?v1%uFB? zb2khYXCPs<5H6aZZ=IpNju2m0K-~Yx#$WMykQ}O6r5Z=rOM+!Sy#&IrxQEcpQ692n zH%ne>KF)O*KR}Eh1cq7ZXW&?zl)f$TGi&zkzgxJ;A-lv>oZ{7PqBp){$FR_PP6)y~ zBAFpAzz&^eQ>*AqQ){3z{+a!)iF3utcd}oH(1)2QshyK^`)HTM{@`rx_s!;1QQtdF z=kjV_TDC}K>O@-mf?>>*7yU_^CM=I>TBERVe1j0l?I&} zF*W4ox05D38E@L4Ky~?Z%l}*W^zG&XS7&UL&pO~JaKT5KE{3l(*|JV#4o5Sm7|r~q z={tnTNxQa}X#ff%%!&?IPKC_{G{&R>-@z&?HtuI2qGne?)ZG_-YTAmFo4K0T991#aB?|D+-Z+a98@9T+4W3 zlALf1$eN1&i3!Rj5+UplMg0~<{Z=9Bn>YQ}zjIMCLT;Zls&HW#?AIu0{1%~BbxhmQ zJX?b*Ic>xG&Q01@EtfCB9RS#Y_Zx6))U+`kO!h2R9a0Jv=HYk<-7hC0>jwnV^RTI5 zT!RRt$19 zt1#aiUv0lp403LWQ<>JJBB;D8g5W8?Km@d3C7ET%@P&$QSM zF>Jb)fJy>o>P$l-j=cAu7e~)RY-cI3J^0A=!rwuOC0~b?ca%Fr&S41`ZCu=OmQa_m zFY0eU)0eOZ%E2#ARH!wqbb)@IyjJbYEJpjVOMQwiU3BZue#6zB?loX@MfUe(q03Cz zc4dxi?1b8V+pDl8M{trIg43EM3~|kl_9NDQ1y*PF0LFfUp6;-~B$Mhs&ZjzL5`1L}G8hY6>&7Xa)JPQVWF}ukg(_D9ebAG`ju5#U^r^_&V(Z znHlIZ9&bcd#~VNV#^!yz%c%j|bq($`TfeT)>ahFiTC{5p?o;A}4)I)JixFoetrQ@` zUrUJz*iBf&8`RUsXciwU%;K@B7nY0B8M5KNC^d?1+bkP8Mz(Te5q9Td4p_idaY`LT z+RWeAt2Blut})e#>yyjSq1u&YA*8z@x)Pd+?H8Jf z?N?~#fwzic+9t?Ppl~k?=g?4uDvSyE`fN8W6^OJ!)g)mNnV5jo>PnYLf_ouMMtc>0 zGlN%+#;!!zS1MpX(YNUaF`$!R8gxM8qwtF^^iw_Hu;rSN1+iuUl3Rf<=r~f>>cTJS zkm|F#-#Z|}2|cg5$1=3ox3M4JR%~VdrSpP+7ISDv%_!py(VZo^y zF0h@EeCsUaCWLsCLe&p{kSe39ON&g^iJr2!T2Zg-u!J_HlL(t9fumN(Oj_`Ri0?s# z3ZMSk>jGbDJGda93YYE&0W6s=pcvTa`CGTa$nxjC@ST4mJ%lhGQowlZNB3cnwnJcO zTu#bC@H$a!yli{_@S;0l<@@a|wa%%2KZYM8h94_1Jo}i3dxIS{xMac{M0`r*(Lk~= zCJrJ?z5^4%#_%3TAdc7alNlxyMdfYB!JGci2p21;(uQY#Kzu3eZLTus<#jvFQ?7?kZEF6sq zU!I(T?R}#gc_5+jN|Ja7#l52tclEn3p-bLIe*27$Sx*TPYZ_$pd;vWqJ@iHQXo~D8 zUC<8vsz|OC*ph1%*sdRVMWo#_!w{1|&{Jb8-HH*2s$Fsx2&X&n3kKvT<%j@S@s8p| zFnKnLKU*RGiLT`$ILvh0@GA*AV4y-W!w}AR<~%R_JX*2Opx9>=Vk_<)Vnpa~&+v#G za~B{jkZeqn^d>*tXf5dG96Z2;tB+-pZ=&=!71ICi(rKad1Vr_FS%qbyYkmf5K!lDb z^m%X>O9Gh|N-#~^`Zx2*w@~z33ek@r-YO0ZK_1+Hrt(^IV1$q_skpG)#QL?P8s@iA z?Ar>ldpAG(Q!(KN;1~2NRBUpG?z++qCbkq8t8DNaPq#GwWKVjzLfGvm+f^F9=HnfnfPl71EJ6HV7-Q&NnvF9;OQQet(E3__7NHVBv&F35#`_U*BC8UYAJDo|tOP@o;NmjM$2 R2(HxDU6vU)Q?s%HvK_?W7j6Im -- 2.10.0