diff options
22 files changed, 750 insertions, 172 deletions
@@ -42,6 +42,11 @@ <artifactId>jackson-datatype-jdk8</artifactId> <version>2.20.0</version> </dependency> + <dependency> + <groupId>com.fasterxml.jackson.dataformat</groupId> + <artifactId>jackson-dataformat-xml</artifactId> + <version>2.20.0</version> + </dependency> </dependencies> <build> <plugins> @@ -67,8 +72,8 @@ <artifactId>jacoco-maven-plugin</artifactId> <version>0.8.11</version> <configuration> - <destfile>/target/coverage-reports/jacoco-unit.exec</destfile> - <datafile>/target/coverage-reports/jacoco-unit.exec</datafile> + <destFile>/target/coverage-reports/jacoco-unit.exec</destFile> + <destFile>/target/coverage-reports/jacoco-unit.exec</destFile> </configuration> <executions> <execution> diff --git a/src/main/java/design/controller/userinput/Menu.java b/src/main/java/design/controller/userinput/Menu.java index ab633e1..01e80a9 100644 --- a/src/main/java/design/controller/userinput/Menu.java +++ b/src/main/java/design/controller/userinput/Menu.java @@ -23,7 +23,7 @@ public abstract class Menu { try { int i = Integer.parseInt(line) - 1; menuOptions.get(i).onAction(); - } catch (IndexOutOfBoundsException ex) { + } catch (IndexOutOfBoundsException | NumberFormatException ex) { System.err.printf("Invalid option \"%s\"\n", line); present(); } diff --git a/src/main/java/design/controller/userinput/menus/ImportExportMenu.java b/src/main/java/design/controller/userinput/menus/ImportExportMenu.java new file mode 100644 index 0000000..21ffa6c --- /dev/null +++ b/src/main/java/design/controller/userinput/menus/ImportExportMenu.java @@ -0,0 +1,81 @@ +package design.controller.userinput.menus; + +import design.controller.userinput.Menu; +import design.controller.userinput.MenuOption; +import design.persistence.*; +import design.persistence.importexport.DataHandler; +import design.persistence.importexport.DataSource; +import design.persistence.importexport.JSONHandler; +import design.persistence.importexport.XMLHandler; + +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.Scanner; + +public class ImportExportMenu extends Menu { + + @Override + public String getTitle() { + return "import export menu"; + } + + @Override + public List<MenuOption> getMenuOptions() { + List<MenuOption> opts = new java.util.ArrayList<>(); + + + opts.add(new MenuOption("return to main menu", () -> new MainMenu().present())); + + opts.add(new MenuOption("import leagues...", () -> promptForPath(true, false))); + opts.add(new MenuOption("export leagues...", () -> promptForPath(false, false))); + opts.add(new MenuOption("import profiles...", () -> promptForPath(true, true))); + opts.add(new MenuOption("export profiles...", () -> promptForPath(false, true))); + + return opts; + } + + private void promptForPath(boolean isImporting, boolean isPersonalProfile) + { + System.out.print("Enter file path: "); + Scanner sc = new Scanner(System.in); + String filePath = sc.nextLine(); + File file = new File(filePath); + + DataSource source; + if (isPersonalProfile) { + source = PersonalDatabase.instance(); + } else { + source = LeagueDatabase.instance(); + } + + DataHandler handler; + String ext = getExtension(filePath); + switch (ext) { + case "json" -> handler = new JSONHandler(source); + case "xml" -> handler = new XMLHandler(source); + default -> { + System.out.println("Unsupported file type: " + ext); + this.present(); + return; + } + } + + try { + if (isImporting) { + handler.importData(file); + } else { + handler.exportData(file); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + + this.present(); + } + + private static String getExtension(String fileName) { + int i = fileName.lastIndexOf('.'); + return (i >= 0) ? fileName.substring(i + 1).toLowerCase() : "unknown"; + } +} diff --git a/src/main/java/design/controller/userinput/menus/MainMenu.java b/src/main/java/design/controller/userinput/menus/MainMenu.java index 4331e56..3f520e4 100644 --- a/src/main/java/design/controller/userinput/menus/MainMenu.java +++ b/src/main/java/design/controller/userinput/menus/MainMenu.java @@ -27,6 +27,7 @@ public class MainMenu extends Menu { options.add(new MenuOption("statistics...", () -> new StatisticsMenu().present())); options.add(new MenuOption("log round...", () -> new HolePlayMenu().present())); options.add(new MenuOption("league play...", () -> new SelectLeague().present())); + options.add(new MenuOption("manage data...", () -> new ImportExportMenu().present())); } return options; } diff --git a/src/main/java/design/controller/userinput/menus/SelectLeague.java b/src/main/java/design/controller/userinput/menus/SelectLeague.java index 08f6c91..e1675e9 100644 --- a/src/main/java/design/controller/userinput/menus/SelectLeague.java +++ b/src/main/java/design/controller/userinput/menus/SelectLeague.java @@ -26,7 +26,7 @@ public class SelectLeague extends Menu { public List<MenuOption> getMenuOptions() { List<MenuOption> options = new ArrayList<>(); for (League l : LeagueDatabase.instance().getLeagues()) { - options.add(new MenuOption(l.getName(), () -> { + options.add(new MenuOption(l.toString(), () -> { if (l instanceof ScrambleLeague sl && sl.locateTeam(golfer) == null) { Scanner sc = new Scanner(System.in); System.out.print("You are not a member of a team for this league. Would you like to create one (Y/n): "); diff --git a/src/main/java/design/model/Golfer.java b/src/main/java/design/model/Golfer.java index 6ea9d32..4fb521e 100644 --- a/src/main/java/design/model/Golfer.java +++ b/src/main/java/design/model/Golfer.java @@ -1,6 +1,7 @@ package design.model; import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyOrder; import java.util.ArrayList; @@ -18,10 +19,14 @@ public class Golfer implements Originator { private final List<Round> rounds; private final List<Club> clubs; // Keep track of golfer's clubs private int nextClubId; + private final List<Invite> invites; + private Team joinedTeam; + + @JsonCreator private Golfer(String username, int passwordHash, String fullName, List<Course> courses, List<Round> rounds, - List<Club> clubs) { + List<Club> clubs, List<Invite> invites, Team joinedTeam) { this.username = username; this.passwordHash = passwordHash; this.fullName = fullName; @@ -29,6 +34,8 @@ public class Golfer implements Originator { this.rounds = rounds; this.clubs = clubs; this.nextClubId = this.clubs.stream().mapToInt(Club::getId).max().orElse(0) + 1; + this.invites = invites; + this.joinedTeam = joinedTeam; } public Golfer(String fullName, String username, String password) { @@ -39,6 +46,8 @@ public class Golfer implements Originator { this.rounds = new ArrayList<>(); this.clubs = new ArrayList<>(); this.nextClubId = 1; + this.invites = new ArrayList<>(); + this.joinedTeam = null; } public String getUsername() { @@ -160,5 +169,21 @@ public class Golfer implements Originator { this.clubs.addAll(gm.clubs); this.nextClubId = gm.nextClubId; - } + } + + public boolean joinTeam(Team team){ + for(Invite invite : invites){ + if(invite.getTeam().equals(team)){ + this.joinedTeam = team; + team.addMember(this); + invites.remove(invite); + return true; + } + } + return false; + } + + public Team getTeam(){ + return joinedTeam; + } } diff --git a/src/main/java/design/model/League.java b/src/main/java/design/model/League.java index 881e25f..8793c86 100644 --- a/src/main/java/design/model/League.java +++ b/src/main/java/design/model/League.java @@ -25,6 +25,7 @@ public abstract class League { private final Date endDate; private final Golfer owner; private final List<Match> schedule; + private boolean completed; @JsonCreator protected League(int id, String name, Date registrationDate, Date startDate, Date endDate, Golfer owner, List<Match> schedule) { @@ -35,6 +36,7 @@ public abstract class League { this.endDate = endDate; this.owner = owner; this.schedule = schedule != null ? schedule : new ArrayList<>(); + this.completed = false; } public League(String name, Date registrationDate, Date startDate, Date endDate, Golfer owner) { @@ -45,6 +47,7 @@ public abstract class League { this.endDate = endDate; this.owner = owner; this.schedule = new ArrayList<>(); + this.completed = false; } public int getId() { @@ -75,9 +78,12 @@ public abstract class League { return schedule.toArray(Match[]::new); } + public boolean isCompleted() { + return completed; + } + public void addMatchToSchedule(Match match) { - Date date = match.getDateScheduled(); - if(date.after(endDate)){ + if(match.getDateScheduled().after(endDate)){ throw new IllegalArgumentException("Cannot create match after league has ended"); } schedule.add(match); @@ -87,4 +93,23 @@ public abstract class League { assert this.id == -1; this.id = id; } + + public abstract String getType(); + + @Override + public String toString() { + return String.format("%s - %s", name, getType()); + } + public boolean isPlayable() { + Date now = new Date(); + return now.after(startDate) && now.before(endDate); + } + + public void markCompleted(){ + this.completed = true; + } + + public abstract void recordPlay(Golfer player, Match match, Round round); + + public abstract void finalizeLeague(); } diff --git a/src/main/java/design/model/ScrambleLeague.java b/src/main/java/design/model/ScrambleLeague.java index b0d65f8..1fb8f1d 100644 --- a/src/main/java/design/model/ScrambleLeague.java +++ b/src/main/java/design/model/ScrambleLeague.java @@ -4,30 +4,41 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonTypeName; import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; import java.util.Date; +import java.util.HashMap; import java.util.List; +import java.util.Map; @JsonTypeName("scramble") public class ScrambleLeague extends League { - private final List<Team> participants; + private List<Team> participants; + private Map<Team, Integer> totalTeamScores; @JsonCreator private ScrambleLeague(int id, String name, Date registrationDate, Date startDate, Date endDate, Golfer owner, List<Team> participants, List<Match> schedule) { super(id, name, registrationDate, startDate, endDate, owner, schedule); this.participants = participants; + this.totalTeamScores = new HashMap<>(); + participants.forEach(t -> totalTeamScores.putIfAbsent(t, 0)); } public ScrambleLeague(String name, Date registrationDate, Date startDate, Date endDate, Golfer owner) { super(name, registrationDate, startDate, endDate, owner); this.participants = new ArrayList<>(); + this.totalTeamScores = new HashMap<>(); } - public boolean addParticipants(Team e) { - return participants.add(e); + public boolean addParticipants(Team t) { + boolean added = participants.add(t); + if(added) totalTeamScores.putIfAbsent(t, 0); + return added; } - public boolean removeParticipants(Team o) { - return participants.remove(o); + public boolean removeParticipants(Team t) { + totalTeamScores.remove(t); + return participants.remove(t); } public Team[] getParticipants() { @@ -42,4 +53,32 @@ public class ScrambleLeague extends League { } return null; } + + @Override + public String getType() { + return "scramble"; + } + @Override + public void recordPlay(Golfer player, Match match, Round round){ + if(!isPlayable()) return; + Team team = player.getTeam(); + if (team == null) return; + int strokes = round.getTotalSwings(); + team.addMemberRound(player, strokes); + match.addRound(round); + } + + @Override + public void finalizeLeague(){ + markCompleted(); + for(Team team : participants){ + int score = team.computeTotalBestScore(); + totalTeamScores.put(team, score); + } + participants.sort(Comparator.comparingInt(totalTeamScores::get)); + } + + public Map<Team, Integer> getTotalTeamScores(){ + return Collections.unmodifiableMap(totalTeamScores); + } } diff --git a/src/main/java/design/model/StrokeLeague.java b/src/main/java/design/model/StrokeLeague.java index 2b787b5..d051fd0 100644 --- a/src/main/java/design/model/StrokeLeague.java +++ b/src/main/java/design/model/StrokeLeague.java @@ -4,33 +4,65 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonTypeName; import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; import java.util.Date; +import java.util.HashMap; import java.util.List; +import java.util.Map; @JsonTypeName("stroke") public class StrokeLeague extends League { - private final List<Golfer> participants; + private List<Golfer> participants; + private Map<Golfer, Integer> totalStrokes; @JsonCreator private StrokeLeague(int id, String name, Date registrationDate, Date startDate, Date endDate, Golfer owner, List<Golfer> participants, List<Match> schedule) { super(id, name, registrationDate, startDate, endDate, owner, schedule); this.participants = participants; + this.totalStrokes = new HashMap<>(); + participants.forEach(p -> totalStrokes.putIfAbsent(p, 0)); } public StrokeLeague(String name, Date registrationDate, Date startDate, Date endDate, Golfer owner) { super(name, registrationDate, startDate, endDate, owner); this.participants = new ArrayList<>(); + this.totalStrokes = new HashMap<>(); } - public boolean addParticipants(Golfer e) { - return participants.add(e); + public boolean addParticipants(Golfer g) { + boolean added = participants.add(g); + if(added) totalStrokes.putIfAbsent(g, 0); + return added; } - public boolean removeParticipants(Golfer o) { - return participants.remove(o); + public boolean removeParticipants(Golfer g) { + totalStrokes.remove(g); + return participants.remove(g); } public Golfer[] getParticipants() { return participants.toArray(Golfer[]::new); } + + @Override + public String getType() { + return "stroke"; + } + public void recordPlay(Golfer player, Match match, Round round){ + if(!isPlayable() || !participants.contains(player)) return; + int strokes = round.getTotalSwings(); + totalStrokes.merge(player, strokes, Integer::sum); + match.addRound(round); + } + + @Override + public void finalizeLeague(){ + markCompleted(); + participants.sort(Comparator.comparingInt(totalStrokes::get)); + } + + public Map<Golfer, Integer> getTotalStrokes(){ + return Collections.unmodifiableMap(totalStrokes); + } } diff --git a/src/main/java/design/model/Team.java b/src/main/java/design/model/Team.java index 53b276e..8f30468 100644 --- a/src/main/java/design/model/Team.java +++ b/src/main/java/design/model/Team.java @@ -1,17 +1,22 @@ package design.model; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Objects; public class Team { private String name; private final List<Golfer> members; + private final Map<Golfer, Integer> memberBestRounds; private final Golfer owner; public Team(String name, Golfer owner) { this.name = name; this.owner = owner; this.members = new ArrayList<>(); + this.memberBestRounds = new HashMap<>(); } public String getName() { @@ -27,14 +32,40 @@ public class Team { } public boolean addMember(Golfer golfer) { + if(members.size() >= 4){ + throw new IllegalArgumentException("Team size limit reached!"); + } return members.add(golfer); } public boolean removeMember(Golfer g) { + memberBestRounds.remove(g); return members.remove(g); } public Golfer getOwner() { return owner; } + + public void addMemberRound(Golfer g, int strokes){ + memberBestRounds.merge(g, strokes, Math::min); + } + + public int computeTotalBestScore(){ + if(memberBestRounds.isEmpty()) return 100; // Bad Score Penalty for not playing + return memberBestRounds.values().stream().min(Integer::compareTo).orElse(100); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + Team other = (Team) obj; + return Objects.equals(name, other.name) && Objects.equals(owner, other.owner); + } + + @Override + public int hashCode() { + return Objects.hash(name, owner); + } } diff --git a/src/main/java/design/persistence/JSONLeagueDatabase.java b/src/main/java/design/persistence/JSONLeagueDatabase.java index 42fa000..b5f4bc3 100644 --- a/src/main/java/design/persistence/JSONLeagueDatabase.java +++ b/src/main/java/design/persistence/JSONLeagueDatabase.java @@ -1,14 +1,7 @@ package design.persistence; -import com.fasterxml.jackson.annotation.JsonAutoDetect; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.PropertyAccessor; -import com.fasterxml.jackson.core.StreamReadFeature; import com.fasterxml.jackson.databind.*; -import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.databind.module.SimpleModule; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; import design.model.Course; import design.model.Golfer; import design.model.League; @@ -44,18 +37,10 @@ public class JSONLeagueDatabase implements LeagueDatabase { public JSONLeagueDatabase(String filename) { this.file = new File(filename); this.cache = new HashMap<>(); - this.mapper = JsonMapper.builder().enable(StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION).build(); + this.mapper = new ObjectMapper(); - SimpleModule module = new SimpleModule(); - module.addDeserializer(Course.class, new Serializers.CourseIdDeserializer()); - module.addSerializer(Course.class, new Serializers.CourseIdSerializer()); - module.addDeserializer(Golfer.class, new Serializers.GolferUsernameDeserializer()); - module.addSerializer(Golfer.class, new Serializers.GolferUsernameSerializer()); - mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - mapper.registerModule(module); - mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); - mapper.registerModule(new ParameterNamesModule(JsonCreator.Mode.PROPERTIES)); - mapper.registerModule(new JavaTimeModule()); + Serializers.configureMapper(mapper); + mapper.registerModule(this.getJacksonModule()); try { load(); @@ -105,4 +90,36 @@ public class JSONLeagueDatabase implements LeagueDatabase { cache.put(league.getId(), league); save(); } + + @Override + public void importData(Object rawData) throws IOException { + if (!(rawData instanceof League[] data)) { + throw new ClassCastException(); + } + cache.clear(); + for (League league : data) { + cache.put(league.getId(), league); + } + save(); + } + + @Override + public League[] exportData() { + return getLeagues(); + } + + @Override + public SimpleModule getJacksonModule() { + SimpleModule module = new SimpleModule(); + module.addDeserializer(Course.class, new Serializers.CourseIdDeserializer()); + module.addSerializer(Course.class, new Serializers.CourseIdSerializer()); + module.addDeserializer(Golfer.class, new Serializers.GolferUsernameDeserializer()); + module.addSerializer(Golfer.class, new Serializers.GolferUsernameSerializer()); + return module; + } + + @Override + public Class<?> getTargetClass() { + return League[].class; + } } diff --git a/src/main/java/design/persistence/JSONPersonalDatabase.java b/src/main/java/design/persistence/JSONPersonalDatabase.java index 7f55f78..e035ae4 100644 --- a/src/main/java/design/persistence/JSONPersonalDatabase.java +++ b/src/main/java/design/persistence/JSONPersonalDatabase.java @@ -1,14 +1,7 @@ package design.persistence; -import com.fasterxml.jackson.annotation.JsonAutoDetect; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.PropertyAccessor; -import com.fasterxml.jackson.core.StreamReadFeature; import com.fasterxml.jackson.databind.*; -import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.databind.module.SimpleModule; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; import design.model.Course; import design.model.Golfer; import design.model.League; @@ -42,21 +35,10 @@ public class JSONPersonalDatabase implements PersonalDatabase { private JSONPersonalDatabase(String filename) { this.file = new File(filename); this.cache = new HashMap<>(); - this.mapper = JsonMapper.builder().enable(StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION).build();; + this.mapper = new ObjectMapper(); - // TODO: Once the saved JSON matches the model, consider removing. - // TEMP: tolerate unknown props while the model stabilizes - mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - - SimpleModule module = new SimpleModule(); - module.addDeserializer(Course.class, new Serializers.CourseIdDeserializer()); - module.addSerializer(Course.class, new Serializers.CourseIdSerializer()); - module.addSerializer(League.class, new Serializers.LeagueIDSerializer()); - module.addDeserializer(League.class, new Serializers.LeagueIDDeserializer()); - mapper.registerModule(module); - mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); - mapper.registerModule(new ParameterNamesModule(JsonCreator.Mode.PROPERTIES)); - mapper.registerModule(new JavaTimeModule()); + Serializers.configureMapper(mapper); + mapper.registerModule(this.getJacksonModule()); try { load(); @@ -106,4 +88,36 @@ public class JSONPersonalDatabase implements PersonalDatabase { cache.put(golfer.getUsername(), golfer); save(); } + + @Override + public void importData(Object rawData) throws IOException { + if (!(rawData instanceof Golfer[] data)) { + throw new ClassCastException(); + } + cache.clear(); + for (Golfer golfer : data) { + cache.put(golfer.getUsername(), golfer); + } + save(); + } + + @Override + public Golfer[] exportData() { + return getGolfers(); + } + + @Override + public SimpleModule getJacksonModule() { + SimpleModule module = new SimpleModule(); + module.addDeserializer(Course.class, new Serializers.CourseIdDeserializer()); + module.addSerializer(Course.class, new Serializers.CourseIdSerializer()); + module.addSerializer(League.class, new Serializers.LeagueIDSerializer()); + module.addDeserializer(League.class, new Serializers.LeagueIDDeserializer()); + return module; + } + + @Override + public Class<?> getTargetClass() { + return Golfer[].class; + } } diff --git a/src/main/java/design/persistence/LeagueDatabase.java b/src/main/java/design/persistence/LeagueDatabase.java index 953624d..ed6ddae 100644 --- a/src/main/java/design/persistence/LeagueDatabase.java +++ b/src/main/java/design/persistence/LeagueDatabase.java @@ -1,10 +1,11 @@ package design.persistence; import design.model.League; +import design.persistence.importexport.DataSource; import java.io.IOException; -public interface LeagueDatabase { +public interface LeagueDatabase extends DataSource { static LeagueDatabase instance() { return JSONLeagueDatabase.instance(); } @@ -18,4 +19,10 @@ public interface LeagueDatabase { void removeLeague(League league) throws IOException; void updateLeague(League league) throws IOException; + + @Override + void importData(Object data) throws IOException; + + @Override + Object exportData() throws IOException; } diff --git a/src/main/java/design/persistence/PersonalDatabase.java b/src/main/java/design/persistence/PersonalDatabase.java index adb865d..70dd37d 100644 --- a/src/main/java/design/persistence/PersonalDatabase.java +++ b/src/main/java/design/persistence/PersonalDatabase.java @@ -1,9 +1,11 @@ package design.persistence; import design.model.Golfer; +import design.persistence.importexport.DataSource; + import java.io.IOException; -public interface PersonalDatabase { +public interface PersonalDatabase extends DataSource { static PersonalDatabase instance() { return JSONPersonalDatabase.instance(); @@ -18,4 +20,10 @@ public interface PersonalDatabase { void removeGolfer(Golfer golfer) throws IOException; void updateGolfer(Golfer golfer) throws IOException; + + @Override + void importData(Object data) throws IOException; + + @Override + Object exportData() throws IOException; } diff --git a/src/main/java/design/persistence/Serializers.java b/src/main/java/design/persistence/Serializers.java index 3940b44..246dbf1 100644 --- a/src/main/java/design/persistence/Serializers.java +++ b/src/main/java/design/persistence/Serializers.java @@ -1,18 +1,20 @@ package design.persistence; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.util.DefaultIndenter; import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; import design.model.Course; import design.model.Golfer; import design.model.League; import java.io.IOException; +import java.time.LocalDateTime; +import java.util.List; public class Serializers { public static class CustomPrettyPrinter extends DefaultPrettyPrinter { @@ -76,4 +78,19 @@ public class Serializers { return personalDB.getGolfer(username); } } + + public static class DateTimeStringSerializer extends JsonSerializer<LocalDateTime> { + @Override + public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeString(value.toString()); + } + } + + public static void configureMapper(ObjectMapper mapper) { + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); + mapper.registerModule(new ParameterNamesModule(JsonCreator.Mode.PROPERTIES)); + mapper.registerModule(new JavaTimeModule()); + mapper.configOverride(List.class).setSetterInfo(JsonSetter.Value.forValueNulls(Nulls.AS_EMPTY)); + } } diff --git a/src/main/java/design/persistence/importexport/DataHandler.java b/src/main/java/design/persistence/importexport/DataHandler.java new file mode 100644 index 0000000..59df9be --- /dev/null +++ b/src/main/java/design/persistence/importexport/DataHandler.java @@ -0,0 +1,11 @@ +package design.persistence.importexport; + +import java.io.File; +import java.io.IOException; + +public interface DataHandler { + + void importData(File file) throws IOException; + + void exportData(File file) throws IOException; +} diff --git a/src/main/java/design/persistence/importexport/DataSource.java b/src/main/java/design/persistence/importexport/DataSource.java new file mode 100644 index 0000000..e735c7e --- /dev/null +++ b/src/main/java/design/persistence/importexport/DataSource.java @@ -0,0 +1,16 @@ +package design.persistence.importexport; + +import com.fasterxml.jackson.databind.module.SimpleModule; + +import java.io.IOException; + +public interface DataSource { + + void importData(Object data) throws IOException; + + Object exportData() throws IOException; + + SimpleModule getJacksonModule(); + + Class<?> getTargetClass(); +}
\ No newline at end of file diff --git a/src/main/java/design/persistence/importexport/JSONHandler.java b/src/main/java/design/persistence/importexport/JSONHandler.java new file mode 100644 index 0000000..9c04281 --- /dev/null +++ b/src/main/java/design/persistence/importexport/JSONHandler.java @@ -0,0 +1,35 @@ +package design.persistence.importexport; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import design.persistence.Serializers; + +import java.io.File; +import java.io.IOException; + +public class JSONHandler implements DataHandler { + + private final DataSource dataSource; + private final ObjectMapper jsonMapper = new JsonMapper(); + + public JSONHandler(DataSource dataSource) { + this.dataSource = dataSource; + + Serializers.configureMapper(jsonMapper); + jsonMapper.registerModule(dataSource.getJacksonModule()); + } + + @Override + public void importData(File file) throws IOException { + Object data = jsonMapper.readValue(file, dataSource.getTargetClass()); + dataSource.importData(data); + } + + @Override + public void exportData(File file) throws IOException{ + Object data = dataSource.exportData(); + jsonMapper.writer(new Serializers.CustomPrettyPrinter()) + .writeValue(file, data); + } +} + + diff --git a/src/main/java/design/persistence/importexport/XMLHandler.java b/src/main/java/design/persistence/importexport/XMLHandler.java new file mode 100644 index 0000000..0a07d6e --- /dev/null +++ b/src/main/java/design/persistence/importexport/XMLHandler.java @@ -0,0 +1,38 @@ +package design.persistence.importexport; +import java.io.File; +import java.io.IOException; +import java.time.LocalDateTime; + +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import design.persistence.Serializers; + +public class XMLHandler implements DataHandler { + + private final DataSource dataSource; + private final XmlMapper xmlMapper = new XmlMapper(); + + public XMLHandler(DataSource dataSource) { + this.dataSource = dataSource; + + Serializers.configureMapper(xmlMapper); + SimpleModule module = dataSource.getJacksonModule(); + module.addSerializer(LocalDateTime.class, new Serializers.DateTimeStringSerializer()); + xmlMapper.registerModule(module); + } + + @Override + public void importData(File file) throws IOException { + Object data = xmlMapper.readValue(file, dataSource.getTargetClass()); + dataSource.importData(data); + } + + @Override + public void exportData(File file) throws IOException { + Object data = dataSource.exportData(); + xmlMapper.writerWithDefaultPrettyPrinter() + .writeValue(file, data); + } +} + + diff --git a/src/test/java/design/model/GolferTest.java b/src/test/java/design/model/GolferTest.java index 5f113ab..98dad20 100644 --- a/src/test/java/design/model/GolferTest.java +++ b/src/test/java/design/model/GolferTest.java @@ -150,5 +150,4 @@ public class GolferTest { } -} - +}
\ No newline at end of file diff --git a/test.json b/test.json new file mode 100644 index 0000000..4e42e0d --- /dev/null +++ b/test.json @@ -0,0 +1,160 @@ +[ + { + "clubs": [], + "nextClubId": 1, + "username": "test", + "passwordHash": 3556498, + "fullName": "test", + "courses": [], + "rounds": [], + "invites": [] + }, + { + "clubs": [], + "nextClubId": 1, + "username": "GUYHERE", + "passwordHash": 3556498, + "fullName": "GUYHERE", + "courses": [], + "rounds": [], + "invites": [] + }, + { + "clubs": [ + { + "id": 1, + "manufacture": "Bobby", + "nickname": "swen 261", + "clubType": "DRIVER" + }, + { + "id": 2, + "manufacture": "Bobby", + "nickname": "swen 262", + "clubType": "PUTTER" + }, + { + "id": 3, + "manufacture": "sowclub", + "nickname": "man", + "clubType": "WOOD" + } + ], + "nextClubId": 4, + "username": "john_doe", + "passwordHash": 46792755, + "fullName": "John Doe", + "courses": [ + 2, + 1 + ], + "rounds": [ + { + "course": 1, + "dateTime": [ + 2025, + 10, + 7, + 13, + 54, + 37, + 429963500 + ], + "startingHole": { + "number": 1, + "par": 5 + }, + "plays": [ + { + "holeNumber": 1, + "swings": [ + { + "distance": 100, + "clubUsed": 1 + }, + { + "distance": 5, + "clubUsed": 2 + }, + { + "distance": 1, + "clubUsed": 2 + } + ], + "swingCount": 3, + "distance": 106 + }, + { + "holeNumber": 2, + "swings": [ + { + "distance": 1000, + "clubUsed": 1 + }, + { + "distance": 2, + "clubUsed": 1 + } + ], + "swingCount": 2, + "distance": 1002 + } + ], + "currentHoleIndex": 2, + "totalSwings": 5, + "totalDistance": 1108.0, + "currentHole": { + "number": 3, + "par": 4 + } + }, + { + "course": 1, + "dateTime": [ + 2025, + 10, + 8, + 20, + 19, + 38, + 968996400 + ], + "startingHole": { + "number": 9, + "par": 3 + }, + "plays": [ + { + "holeNumber": 9, + "swings": [ + { + "distance": 204, + "clubUsed": 1 + } + ], + "swingCount": 1, + "distance": 204 + } + ], + "currentHoleIndex": 9, + "totalSwings": 1, + "totalDistance": 204.0, + "currentHole": { + "number": 10, + "par": 3 + } + } + ], + "invites": [] + }, + { + "clubs": [], + "nextClubId": 1, + "username": "sowgro", + "passwordHash": -896456343, + "fullName": "sowgro", + "courses": [], + "rounds": [], + "invites": [] + } +]
\ No newline at end of file @@ -1,127 +1,144 @@ -<export> - <items> +<Golfers> + <item> + <clubs/> <nextClubId>1</nextClubId> <username>test</username> <passwordHash>3556498</passwordHash> <fullName>test</fullName> - </items> - <items> + <courses/> + <rounds/> + <invites/> + </item> + <item> + <clubs/> <nextClubId>1</nextClubId> <username>GUYHERE</username> <passwordHash>3556498</passwordHash> <fullName>GUYHERE</fullName> - </items> - <items> + <courses/> + <rounds/> + <invites/> + </item> + <item> <clubs> - <id>1</id> - <manufacture>Bobby</manufacture> - <nickname>swen 261</nickname> - <clubType>DRIVER</clubType> - </clubs> - <clubs> - <id>2</id> - <manufacture>Bobby</manufacture> - <nickname>swen 262</nickname> - <clubType>PUTTER</clubType> - </clubs> - <clubs> - <id>3</id> - <manufacture>sowclub</manufacture> - <nickname>man</nickname> - <clubType>WOOD</clubType> + <clubs> + <id>1</id> + <manufacture>Bobby</manufacture> + <nickname>swen 261</nickname> + <clubType>DRIVER</clubType> + </clubs> + <clubs> + <id>2</id> + <manufacture>Bobby</manufacture> + <nickname>swen 262</nickname> + <clubType>PUTTER</clubType> + </clubs> + <clubs> + <id>3</id> + <manufacture>sowclub</manufacture> + <nickname>man</nickname> + <clubType>WOOD</clubType> + </clubs> </clubs> <nextClubId>4</nextClubId> <username>john_doe</username> <passwordHash>46792755</passwordHash> <fullName>John Doe</fullName> - <courses>2</courses> - <courses>1</courses> - <rounds> - <course>1</course> - <dateTime>2025</dateTime> - <dateTime>10</dateTime> - <dateTime>7</dateTime> - <dateTime>13</dateTime> - <dateTime>54</dateTime> - <dateTime>37</dateTime> - <dateTime>429963500</dateTime> - <startingHole> - <number>1</number> - <par>5</par> - </startingHole> - <plays> - <holeNumber>1</holeNumber> - <swings> - <distance>100</distance> - <clubUsed>1</clubUsed> - </swings> - <swings> - <distance>5</distance> - <clubUsed>2</clubUsed> - </swings> - <swings> - <distance>1</distance> - <clubUsed>2</clubUsed> - </swings> - <swingCount>3</swingCount> - <distance>106</distance> - </plays> - <plays> - <holeNumber>2</holeNumber> - <swings> - <distance>1000</distance> - <clubUsed>1</clubUsed> - </swings> - <swings> - <distance>2</distance> - <clubUsed>1</clubUsed> - </swings> - <swingCount>2</swingCount> - <distance>1002</distance> - </plays> - <currentHoleIndex>2</currentHoleIndex> - <totalDistance>1108.0</totalDistance> - <currentHole> - <number>3</number> - <par>4</par> - </currentHole> - <totalSwings>5</totalSwings> - </rounds> + <courses> + <courses>2</courses> + <courses>1</courses> + </courses> <rounds> - <course>1</course> - <dateTime>2025</dateTime> - <dateTime>10</dateTime> - <dateTime>8</dateTime> - <dateTime>20</dateTime> - <dateTime>19</dateTime> - <dateTime>38</dateTime> - <dateTime>968996400</dateTime> - <startingHole> - <number>9</number> - <par>3</par> - </startingHole> - <plays> - <holeNumber>9</holeNumber> - <swings> - <distance>204</distance> - <clubUsed>1</clubUsed> - </swings> - <swingCount>1</swingCount> - <distance>204</distance> - </plays> - <currentHoleIndex>9</currentHoleIndex> - <totalDistance>204.0</totalDistance> - <currentHole> - <number>10</number> - <par>3</par> - </currentHole> - <totalSwings>1</totalSwings> + <rounds> + <course>1</course> + <dateTime>2025-10-07T13:54:37.429963500</dateTime> + <startingHole> + <number>1</number> + <par>5</par> + </startingHole> + <plays> + <plays> + <holeNumber>1</holeNumber> + <swings> + <swings> + <distance>100</distance> + <clubUsed>1</clubUsed> + </swings> + <swings> + <distance>5</distance> + <clubUsed>2</clubUsed> + </swings> + <swings> + <distance>1</distance> + <clubUsed>2</clubUsed> + </swings> + </swings> + <swingCount>3</swingCount> + <distance>106</distance> + </plays> + <plays> + <holeNumber>2</holeNumber> + <swings> + <swings> + <distance>1000</distance> + <clubUsed>1</clubUsed> + </swings> + <swings> + <distance>2</distance> + <clubUsed>1</clubUsed> + </swings> + </swings> + <swingCount>2</swingCount> + <distance>1002</distance> + </plays> + </plays> + <currentHoleIndex>2</currentHoleIndex> + <totalSwings>5</totalSwings> + <totalDistance>1108.0</totalDistance> + <currentHole> + <number>3</number> + <par>4</par> + </currentHole> + </rounds> + <rounds> + <course>1</course> + <dateTime>2025-10-08T20:19:38.968996400</dateTime> + <startingHole> + <number>9</number> + <par>3</par> + </startingHole> + <plays> + <plays> + <holeNumber>9</holeNumber> + <swings> + <swings> + <distance>204</distance> + <clubUsed>1</clubUsed> + </swings> + </swings> + <swingCount>1</swingCount> + <distance>204</distance> + </plays> + </plays> + <currentHoleIndex>9</currentHoleIndex> + <totalSwings>1</totalSwings> + <totalDistance>204.0</totalDistance> + <currentHole> + <number>10</number> + <par>3</par> + </currentHole> + </rounds> </rounds> - </items> - <items> + <invites/> + </item> + <item> + <clubs/> <nextClubId>1</nextClubId> <username>sowgro</username> <passwordHash>-896456343</passwordHash> <fullName>sowgro</fullName> - </items> -</export> + <courses/> + <rounds/> + <invites/> + </item> +</Golfers> |
