diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 71d6ce6..c17e057 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -12,12 +12,6 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Set up JDK 25 - uses: actions/setup-java@v4 - with: - java-version: '25' - distribution: 'temurin' - cache: 'maven' - name: Build with Maven run: mvn -B compile --file pom.xml @@ -25,3 +19,14 @@ jobs: - name: Test with Maven run: mvn -B test --file pom.xml + - name: Build site + JavaDocs & JaCoCo + run: mvn clean verify site + + - name: Deploy to Github Pages + if: github.event_name == 'push' + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: target/site/ + publish_branch: gh-pages + diff --git a/README.md b/README.md index b16466c..01d477e 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,94 @@ The exam of IDATT2003, "Programming 2" subject. +# Overview + +- [Summary](#summary) +- [Packages](#packages) +- [How to run](#how-to-run) +- [Guide to tests](#guide-to-tests) + + +# Summary +This is a game emulating a evolving stock market where the player is encouraged to take their own risks and go from rags to riches. With strategic planning and unsuspected events how far can you go? + +What you can expect from the game: +- Something 1 +- Something 2 +- Something 3 + +# Packages +``` +. +├── java +│   └── edu +│   └── ntnu +│   └── idi +│   └── idatt +│   ├── common +│   │   └── Observer.java +│   ├── Launcher.java +│   ├── model +│   │   ├── Exchange.java +│   │   ├── market +│   │   │   └── Stock.java +│   │   ├── player +│   │   │   └── Player.java +│   │   ├── portfolio +│   │   │   ├── Portfolio.java +│   │   │   └── Share.java +│   │   └── transaction +│   │   ├── Purchase.java +│   │   ├── Sale.java +│   │   ├── TransactionArchive.java +│   │   └── Transaction.java +│   ├── service +│   │   └── transaction +│   │   ├── PurchaseCalculator.java +│   │   ├── SaleCalculator.java +│   │   └── TransactionCalculator.java +│   ├── session +│   │   └── UserSession.java +│   ├── storage +│   │   ├── SessionManager.java +│   │   ├── StockParser.java +│   │   └── StorageFile.java +│   └── view +│   ├── components +│   │   ├── AbstractController.java +│   │   ├── AbstractView.java +│   │   ├── AbstractViewUI.java +│   │   ├── elements +│   │   │   ├── IconComponent.java +│   │   │   └── SearchBarComponent.java +│   │   ├── Model.java +│   │   ├── primitives +│   │   │   └── ActionEventHandler.java +│   │   └── ui +│   ├── entry +│   │   ├── StartController.java +│   │   ├── StartModel.java +│   │   └── StartView.java +│   ├── primary +│   │   └── MainView.java +│   └── SceneManager.java +└── resources + ├── icons + │   ├── portfolio.png + │   ├── quit.png + │   ├── search.png + │   └── user.png + ├── save.json + ├── stocks.csv + ├── themes + │   └── default.css + └── user.png +``` + +# How to run +- Running program: `mvn clean javafx:run` +- Running tests: `mvn clean test` + # Guide to tests - The test generally start with a @BeforeEach method that sets up everything necessary for positive testing. diff --git a/pom.xml b/pom.xml index bff6a52..b27cd17 100644 --- a/pom.xml +++ b/pom.xml @@ -20,40 +20,18 @@ maven-compiler-plugin 3.14.1 - - - org.apache.maven.plugins - maven-javadoc-plugin - 3.12.0 - - - - org.openjfx - javafx-controls - 25.0.1 - - - org.openjfx - javafx-fxml - 25.0.1 - - org.openjfx - javafx-web - 25.0.1 + com.google.code.gson + gson + 2.13.2 + compile org.openjfx - javafx-swing - 25.0.1 - - - - org.openjfx - javafx-media + javafx-controls 25.0.1 @@ -62,9 +40,9 @@ junit-jupiter 6.0.1 test - + - + @@ -82,34 +60,76 @@ javafx-maven-plugin 0.0.8 - edu.ntnu.idi.idatt.gui.javafx + edu.ntnu.idi.idatt.Launcher - - org.codehaus.mojo - exec-maven-plugin - 3.6.3 - - edu.ntnu.idi.idatt.Main - - - - - org.apache.maven.plugins - maven-jar-plugin - 3.3.0 - - - - true - edu.ntnu.idi.idatt.Main - - - - + + org.apache.maven.plugins + maven-javadoc-plugin + 3.12.0 + + /usr/lib/jvm/java-25-openjdk/bin/javadoc + + + + + + org.jacoco + jacoco-maven-plugin + 0.8.14 + + + + + prepare-agent + + prepare-agent + + + + + + report + verify + + report + + + + + + + org.apache.maven.plugins + maven-site-plugin + 4.0.0-M9 + + + + org.apache.maven.plugins + maven-jar-plugin + 3.3.0 + + + + true + edu.ntnu.idi.idatt.Launcher + + + + - + + + + + + + maven-javadoc-plugin + 3.12.0 + + + diff --git a/src/main/java/edu/ntnu/idi/idatt/Launcher.java b/src/main/java/edu/ntnu/idi/idatt/Launcher.java new file mode 100644 index 0000000..18c49a6 --- /dev/null +++ b/src/main/java/edu/ntnu/idi/idatt/Launcher.java @@ -0,0 +1,31 @@ +package edu.ntnu.idi.idatt; + +import edu.ntnu.idi.idatt.view.SceneFactory; +import edu.ntnu.idi.idatt.view.SceneManager; +import javafx.application.Application; +import javafx.stage.Stage; + +public final class Launcher { + + /** + * Entry point of application. + */ + static void main() { + Application.launch(StockGame.class); + } + + public static final class StockGame extends Application { + + @Override + public void start(Stage stage) { + stage.setWidth(1200); + stage.setHeight(700); + stage.setTitle("Stock Game"); + + SceneManager.init(stage, SceneFactory.createStartView()); + stage.show(); + } + + } + +} diff --git a/src/main/java/edu/ntnu/idi/idatt/Main.java b/src/main/java/edu/ntnu/idi/idatt/Main.java deleted file mode 100644 index a7ee785..0000000 --- a/src/main/java/edu/ntnu/idi/idatt/Main.java +++ /dev/null @@ -1,11 +0,0 @@ -package edu.ntnu.idi.idatt; - -public class Main { - - // Logger system !! - - static void main() { - System.out.println("Hello world!"); - } - -} diff --git a/src/main/java/edu/ntnu/idi/idatt/Player.java b/src/main/java/edu/ntnu/idi/idatt/Player.java deleted file mode 100644 index 3a5e61d..0000000 --- a/src/main/java/edu/ntnu/idi/idatt/Player.java +++ /dev/null @@ -1,57 +0,0 @@ -package edu.ntnu.idi.idatt; - -import edu.ntnu.idi.idatt.marked.Portfolio; -import edu.ntnu.idi.idatt.transaction.TransactionArchive; - -import java.math.BigDecimal; - -public class Player { - - private final String name; - private final BigDecimal startingMoney; - private BigDecimal money; - private Portfolio portfolio = new Portfolio(); - private TransactionArchive transactionArchive = new TransactionArchive(); - - public Player(String name, BigDecimal startingMoney) { - this.name = name; - this.startingMoney = startingMoney; - this.money = this.startingMoney; - } - - /** - * Getters - * - * @return - Their corresponding variables. - */ - - public String getName() { - return name; - } - - public BigDecimal getMoney() { - return money; - } - - public Portfolio getPortfolio() { - return portfolio; - } - - public TransactionArchive getTransactionArchive() { - return transactionArchive; - } - - /** - * Setters for money - * - * @param amount - Amount to be changed correspondingly. - */ - - public void addMoney(BigDecimal amount) { - this.money = this.money.add(amount); - } - - public void withdrawMoney(BigDecimal amount) { - this.money = this.money.subtract(amount); - } -} diff --git a/src/main/java/edu/ntnu/idi/idatt/gui/javafx.java b/src/main/java/edu/ntnu/idi/idatt/gui/javafx.java deleted file mode 100644 index 2e29e2f..0000000 --- a/src/main/java/edu/ntnu/idi/idatt/gui/javafx.java +++ /dev/null @@ -1,44 +0,0 @@ -package edu.ntnu.idi.idatt.gui; - -import javafx.application.Application; -import javafx.scene.Group; -import javafx.scene.Scene; -import javafx.scene.control.Button; -import javafx.scene.image.Image; -import javafx.scene.image.ImageView; -import javafx.scene.paint.Color; -import javafx.stage.Stage; - -import java.awt.*; -import java.io.IOException; - -public class javafx extends Application { - @Override - public void start (Stage stage) throws IOException { - Image stonk = new Image(getClass().getResource("/edu.ntnu.idi.idatt/stonks.png").toExternalForm()); - Button week = new Button("Week 5: Play"); - week.setPrefSize(150, 50); - week.setTranslateX(565); - Button logo = new Button(); - Button user = new Button(); - ImageView user_image = new ImageView(new Image(getClass().getResource("/edu.ntnu.idi.idatt/user.png").toExternalForm())); - ImageView logo_image = new ImageView(new Image(getClass().getResource("/edu.ntnu.idi.idatt/logo.png").toExternalForm())); - logo_image.setFitHeight(100); - logo_image.setFitWidth(100); - user_image.setFitHeight(100); - user_image.setFitWidth(100); - logo.setGraphic(logo_image); - user.setGraphic(user_image); - user.setTranslateX(1170); - ImageView stonks = new ImageView(stonk); - stonks.setX(440); - stonks.setY(220); - Group noe = new Group(stonks, week, logo, user); - Scene scene1 = new Scene(noe, 1280, 720, Color.DEEPSKYBLUE); - logo.setOnAction(e -> stage.setScene(new Scene(new Group(stonks,week,logo,user),1280, 720, Color.DEEPSKYBLUE))); - user.setOnAction(e -> stage.setScene(new Scene(new Group(logo,user),1280, 720, Color.BLACK))); - week.setOnAction(e -> stage.close()); - stage.setScene(scene1); - stage.show(); - } -} diff --git a/src/main/java/edu/ntnu/idi/idatt/marked/Portfolio.java b/src/main/java/edu/ntnu/idi/idatt/marked/Portfolio.java deleted file mode 100644 index 5b4a147..0000000 --- a/src/main/java/edu/ntnu/idi/idatt/marked/Portfolio.java +++ /dev/null @@ -1,70 +0,0 @@ -package edu.ntnu.idi.idatt.marked; - -import java.util.ArrayList; -import java.util.List; - -/** - * Portfolio class - * - *

- * Class that functions as a wallet for a player - * storing all results of transactions made. - *

- * - */ -public class Portfolio { - - private ArrayList shares = new ArrayList<>(); - - /** - * Setter for ArrayList shares. - * - * @param share - The bought share - * @return - was the list modified? - */ - public boolean addShare(Share share) { - return shares.add(share); - } - - /** - * Setter for ArrayList shares. - * - * @param share - The sold share - * @return - was the list modified? - */ - public boolean removeShare(Share share) { - if (!contains(share)) { - throw new IllegalArgumentException("Portfolio doesn't contain this share."); - } - return shares.remove(share); - } - - /** - * Getter for ArrayList shares. - * - * @return - List of all shares owned. - */ - public List getShares() { - return shares; - } - - /** - * Getter for ArrayList shares. - * - * @param symbol - The symbol of the stock corresponding to the share. - * @return - List of shares owned that corresponds with a company symbol. - */ - public List getShares(String symbol) { - return shares.stream().filter(s -> s.getStock().getSymbol().equals(symbol)).toList(); - } - - /** - * Method for checking if the portfolio contains a specific share - * - * @return - If the share was found. - */ - public boolean contains(Share share) { - return shares.contains(share); - } - -} diff --git a/src/main/java/edu/ntnu/idi/idatt/marked/Stock.java b/src/main/java/edu/ntnu/idi/idatt/marked/Stock.java deleted file mode 100644 index 40f071e..0000000 --- a/src/main/java/edu/ntnu/idi/idatt/marked/Stock.java +++ /dev/null @@ -1,72 +0,0 @@ -package edu.ntnu.idi.idatt.marked; - -import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.List; - -/** - * Stock class - * - *

- * Class that describes an object of a unique stock. - *

- * - */ -public class Stock { - - private final String symbol; - private final String company; - private final ArrayList prices = new ArrayList<>(); - - /** - * Constructor for a Stock. - * - * @param symbol - String that indicates the symbol of a stock, ex. "APPL" as a - * short form of company. - * @param company - String, company name, ex. "Apple Inc." - * @param prices - An array of BigInteger that indicates the price - * corresponding with time. - */ - public Stock(String symbol, String company, List prices) { - this.symbol = symbol; - this.company = company; - this.prices.addAll(prices); - } - - /** - * Getters - * - * @return - Their corresponding variables. - */ - - public String getSymbol() { - return symbol; - } - - public String getCompany() { - return company; - } - - public List getPrices() { - return prices; - } - - /** - * Getter for sale price - * - * @return - BigDecimal with current (newest in array) stock price. - */ - public BigDecimal getSalesPrice() { - return prices.getLast(); - } - - /** - * Method that adds new price to the price array. - * - * @param price - BigDecimal, new price. - */ - public void addNewSalesPrice(BigDecimal price) { - prices.add(price); - } - -} diff --git a/src/main/java/edu/ntnu/idi/idatt/Exchange.java b/src/main/java/edu/ntnu/idi/idatt/model/Exchange.java similarity index 62% rename from src/main/java/edu/ntnu/idi/idatt/Exchange.java rename to src/main/java/edu/ntnu/idi/idatt/model/Exchange.java index 2153bd9..e41a2c0 100644 --- a/src/main/java/edu/ntnu/idi/idatt/Exchange.java +++ b/src/main/java/edu/ntnu/idi/idatt/model/Exchange.java @@ -1,14 +1,16 @@ -package edu.ntnu.idi.idatt; - -import edu.ntnu.idi.idatt.marked.Share; -import edu.ntnu.idi.idatt.marked.Stock; -import edu.ntnu.idi.idatt.transaction.Purchase; -import edu.ntnu.idi.idatt.transaction.Sale; -import edu.ntnu.idi.idatt.transaction.Transaction; +package edu.ntnu.idi.idatt.model; import java.math.BigDecimal; +import java.math.RoundingMode; import java.util.*; +import edu.ntnu.idi.idatt.model.market.Stock; +import edu.ntnu.idi.idatt.model.player.Player; +import edu.ntnu.idi.idatt.model.portfolio.Share; +import edu.ntnu.idi.idatt.model.transaction.Purchase; +import edu.ntnu.idi.idatt.model.transaction.Sale; +import edu.ntnu.idi.idatt.model.transaction.Transaction; + /** * Exchange class * @@ -24,21 +26,18 @@ public class Exchange { private final String name; private int week; private HashMap stockMap = new HashMap<>(); - private Random random = new Random(); /** * Constructor for Exchange class * * @param name - Name of the current stock Exchange - * @param stocks - List of aviable stocks for this exchange. + * @param stocks - List of stocks for this exchange */ public Exchange(String name, List stocks) { this.name = name; this.week = 1; - for (Stock stock : stocks) { - stockMap.put(stock.getSymbol(), stock); - } + stocks.forEach(stock -> stockMap.put(stock.getSymbol(), stock)); } @@ -57,6 +56,10 @@ public int getWeek() { return week; } + public List getStocks() { + return stockMap.values().stream().toList(); + } + /** * Method for checking if a specific stock exists in the exchange. * @@ -99,6 +102,44 @@ public List findStocks(String searchTerm) { return stocksFound; } + /** + * Method for obtaining gainers + * + *

+ * Returns the stocks that have done it the best + * in the latest week. + *

+ * + * @param limit - Amount of stocks to be returned. + * @return A list of stocks sorted in declining order. + */ + public List getGainers(int limit) { + return stockMap.values().stream() + .filter(stock -> stock.getLatestPriceChange().compareTo(BigDecimal.ZERO) > 0) + .sorted(Comparator.comparing(Stock::getLatestPriceChange).reversed()) + .limit(limit) + .toList(); + } + + /** + * Method for obtaining losers + * + *

+ * Returns the stocks that have done it the worst + * in value in the latest week. + *

+ * + * @param limit - Amount of stocks to be returned. + * @return A list of stocks sorted in inclining order. + */ + public List getLosers(int limit) { + return stockMap.values().stream() + .filter(stock -> stock.getLatestPriceChange().compareTo(BigDecimal.ZERO) < 0) + .sorted(Comparator.comparing(Stock::getLatestPriceChange)) + .limit(limit) + .toList(); + } + /** * Method to allow a player to buy a stock. * @@ -116,7 +157,8 @@ public List findStocks(String searchTerm) { * @see Transaction */ public Transaction buy(String symbol, BigDecimal quantity, Player player) { - Share share = new Share(getStock(symbol), quantity, BigDecimal.valueOf(random.nextDouble())); + Stock stock = getStock(symbol); + Share share = new Share(stock, quantity, stock.getSalesPrice()); Purchase purchase = new Purchase(share, this.week); purchase.commit(player); return player.getTransactionArchive().getPurchases(this.week).getLast(); @@ -132,7 +174,7 @@ public Transaction buy(String symbol, BigDecimal quantity, Player player) { * * @see Sale * - * @param Share - The instance of the sold share. + * @param share - The instance of the sold share. * @param player - which player did this event. * @return The given transaction details. (Transaction). * @see Transaction @@ -153,9 +195,11 @@ public Transaction sell(Share share, Player player) { * @see Stock */ public void advance() { - for (Stock stocks : stockMap.values()) { - stocks.addNewSalesPrice(BigDecimal.valueOf(random.nextDouble())); - } + this.week += 1; + Random random = new Random(); + stockMap.values() + .forEach(s -> s.addNewSalesPrice(s.getSalesPrice() + .multiply(BigDecimal.valueOf(random.nextDouble(0.8, 1.4))).setScale(2, RoundingMode.HALF_UP))); } } diff --git a/src/main/java/edu/ntnu/idi/idatt/model/market/Stock.java b/src/main/java/edu/ntnu/idi/idatt/model/market/Stock.java new file mode 100644 index 0000000..cff8ad3 --- /dev/null +++ b/src/main/java/edu/ntnu/idi/idatt/model/market/Stock.java @@ -0,0 +1,128 @@ +package edu.ntnu.idi.idatt.model.market; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +/** + * Stock class + * + *

+ * Class that describes an object of a unique stock. + *

+ * + */ +public class Stock { + + private final String symbol; + private final String company; + private final ArrayList prices = new ArrayList<>(); + + /** + * Constructor for a Stock. + * + * @param symbol - String that indicates the symbol of a stock, ex. "APPL" as a + * short form of company. + * @param company - String, company name, ex. "Apple Inc." + * @param prices - An array of BigInteger that indicates the price + * corresponding with time. + */ + public Stock(String symbol, String company, List prices) { + this.symbol = symbol; + this.company = company; + this.prices.addAll(prices); + } + + /** + * Getters + * + * @return - Their corresponding variables. + */ + + public String getSymbol() { + return symbol; + } + + public String getCompany() { + return company; + } + + /** + * Method for obtaining the whole price history. + * + */ + public List getHistoricalPrices() { + return prices; + } + + /** + * Method for obtaining highest price. + * + */ + public BigDecimal getHighestPrice() { + return prices.stream().sorted(Comparator.reverseOrder()).toList().getFirst(); + } + + /** + * Method for obtaining lowest price. + * + */ + public BigDecimal getLowestPrice() { + return prices.stream().sorted(Comparator.naturalOrder()).toList().getFirst(); + } + + /** + * Method for obtaining recent price change. + * + *

+ * If prices is empty or contains only one value, + * the change will result to 0. Else, the difference between + * latest and next to latest stock will be calculated. + *

+ * + */ + public BigDecimal getLatestPriceChange() { + if (prices.isEmpty() || prices.size() == 1) { + return BigDecimal.ZERO; + } + int size = prices.size(); + return prices.get(size - 1).subtract(prices.get(size - 2)); + } + + // TODO: JAVADOCS, JUNIT + public BigDecimal getLatestPriceChangePercent() { + if (prices.isEmpty() || prices.size() == 1) { + return BigDecimal.ZERO; + } + + return (getLatestPriceChange().divide(prices.get(prices.size() - 2), 2, RoundingMode.HALF_UP)) + .multiply(new BigDecimal("100")); + } + + /** + * Getter for current sale price + * + * @return - BigDecimal with current (newest in array) stock price. + */ + public BigDecimal getSalesPrice() { + return prices.getLast(); + } + + /** + * Method that adds new price to the price array. + * + * @param price - BigDecimal, new price. + */ + public void addNewSalesPrice(BigDecimal price) { + prices.add(price); + } + + // TODO: JavaDocs + @Override + public String toString() { + return this.getCompany() + " (" + this.getSymbol() + ")"; + } + +} diff --git a/src/main/java/edu/ntnu/idi/idatt/model/player/Player.java b/src/main/java/edu/ntnu/idi/idatt/model/player/Player.java new file mode 100644 index 0000000..d0a21e2 --- /dev/null +++ b/src/main/java/edu/ntnu/idi/idatt/model/player/Player.java @@ -0,0 +1,96 @@ +package edu.ntnu.idi.idatt.model.player; + +import edu.ntnu.idi.idatt.model.portfolio.Portfolio; +import edu.ntnu.idi.idatt.model.transaction.TransactionArchive; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +public class Player { + + private final String name; + private final BigDecimal startingMoney; + private BigDecimal money; + private Portfolio portfolio = new Portfolio(); + private TransactionArchive transactionArchive = new TransactionArchive(); + + public Player(String name, BigDecimal startingMoney) { + this.name = name; + this.startingMoney = startingMoney; + this.money = this.startingMoney; + } + + /** + * Getters + * + * @return - Their corresponding variables. + */ + + public String getName() { + return name; + } + + public BigDecimal getMoney() { + return money; + } + + public Portfolio getPortfolio() { + return portfolio; + } + + public TransactionArchive getTransactionArchive() { + return transactionArchive; + } + + /** + * Setters for money + * + * @param amount - Amount to be changed correspondingly. + */ + + public void addMoney(BigDecimal amount) { + this.money = this.money.add(amount); + } + + public void withdrawMoney(BigDecimal amount) { + this.money = this.money.subtract(amount); + } + + /** + * Method for obtaining players net worth. + * + * @return players net worth. + */ + public BigDecimal getNetWorth() { + return money.add(portfolio.getNetWorth()); + } + + /** + * Method for obtaining players trading status. + * + *

+ * A symbolic title/rank that describes the way the + * players trading career has been going. + *

+ * + * @return String of corresponding title. TODO: Change to ENUM!!! + */ + public String getStatus() { + int tradingWeeks = transactionArchive.countDistinctWeeks(); + BigDecimal netWorth = this.getNetWorth().divide(this.startingMoney, 2, RoundingMode.HALF_UP); + + if (tradingWeeks >= 20 && netWorth.compareTo(new BigDecimal("2")) >= 0) { + return "Speculator"; + } + + if (tradingWeeks >= 10 && netWorth.compareTo(new BigDecimal("1.2")) >= 0) { + return "Investor"; + } + + return "Novice"; + } + public BigDecimal getStartingMoney(){ + return startingMoney; + } + +} diff --git a/src/main/java/edu/ntnu/idi/idatt/model/portfolio/Portfolio.java b/src/main/java/edu/ntnu/idi/idatt/model/portfolio/Portfolio.java new file mode 100644 index 0000000..c255340 --- /dev/null +++ b/src/main/java/edu/ntnu/idi/idatt/model/portfolio/Portfolio.java @@ -0,0 +1,131 @@ +package edu.ntnu.idi.idatt.model.portfolio; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.List; + +import edu.ntnu.idi.idatt.service.transaction.SaleCalculator; + +/** + * Portfolio class + * + *

+ * Class that functions as a wallet for a player + * storing all results of transactions made. + *

+ * + */ +public class Portfolio { + + private ArrayList shares = new ArrayList<>(); + + /** + * Setter for ArrayList shares. + * + * @param share - The bought share + * @return - was the list modified? + */ + public boolean addShare(Share share) { + return shares.add(share); + } + + /** + * Setter for ArrayList shares. + * + * @param share - The sold share + * @return - was the list modified? + */ + public boolean removeShare(Share share) { + if (!contains(share)) { + throw new IllegalArgumentException("Portfolio doesn't contain this share."); + } + return shares.remove(share); + } + + // TODO: JavaDocs, Junit + public void removeShares() { + shares.clear(); + } + + /** + * Getter for ArrayList shares. + * + * @return - List of all shares owned. + */ + public List getShares() { + return shares; + } + + /** + * Getter for ArrayList shares. + * + * @param symbol - The symbol of the stock corresponding to the share. + * @return - List of shares owned that corresponds with a company symbol. + */ + public List getShares(String symbol) { + return shares.stream().filter(s -> s.getStock().getSymbol().equals(symbol)).toList(); + } + + // TODO: JAVADOCS, JUNIT + public BigDecimal getOwnedAmount(String symbol) { + return getShares(symbol).stream().map(s -> s.getQuantity()) + .reduce(BigDecimal.ZERO, BigDecimal::add); + } + + public BigDecimal getOwnedAmount() { + return getShares().stream().map(s -> s.getQuantity()) + .reduce(BigDecimal.ZERO, BigDecimal::add); + } + + public BigDecimal getProfitFromStock(String symbol) { + return getShares(symbol).stream().map(s -> s.getProfit()).reduce(BigDecimal.ZERO, BigDecimal::add); + } + + public BigDecimal getProfitFromStock() { + return getShares().stream().map(s -> s.getProfit()).reduce(BigDecimal.ZERO, BigDecimal::add); + } + + public BigDecimal getChangeFromStock(String symbol) { + BigDecimal profitTotal = getProfitFromStock(symbol); + BigDecimal costTotal = getShares(symbol).stream().map(s -> s.getTotalPurchasePrice()) + .reduce(BigDecimal.ZERO, BigDecimal::add); + + if (costTotal.compareTo(BigDecimal.ZERO) <= 0) + return BigDecimal.ZERO; + + return profitTotal.divide(costTotal, 2, RoundingMode.HALF_UP).multiply(BigDecimal.valueOf(100)); + } + + public BigDecimal getChangeFromStock() { + BigDecimal profitTotal = getProfitFromStock(); + BigDecimal costTotal = getShares().stream().map(s -> s.getTotalPurchasePrice()) + .reduce(BigDecimal.ZERO, BigDecimal::add); + + if (costTotal.compareTo(BigDecimal.ZERO) <= 0) + return BigDecimal.ZERO; + + return profitTotal.divide(costTotal, 2, RoundingMode.HALF_UP).multiply(BigDecimal.valueOf(100)); + } + + /** + * Method for checking if the portfolio contains a specific share + * + * @return - If the share was found. + */ + public boolean contains(Share share) { + return shares.contains(share); + } + + /** + * Method for getting the net value of the portfolio.. + * + * @return - Net value of the portfolio. + */ + public BigDecimal getNetWorth() { + return shares.stream() + .map(s -> new SaleCalculator(s).calculateTotal()) + .reduce(BigDecimal.ZERO, BigDecimal::add); + } + +} diff --git a/src/main/java/edu/ntnu/idi/idatt/marked/Share.java b/src/main/java/edu/ntnu/idi/idatt/model/portfolio/Share.java similarity index 61% rename from src/main/java/edu/ntnu/idi/idatt/marked/Share.java rename to src/main/java/edu/ntnu/idi/idatt/model/portfolio/Share.java index 0cc7432..186cb43 100644 --- a/src/main/java/edu/ntnu/idi/idatt/marked/Share.java +++ b/src/main/java/edu/ntnu/idi/idatt/model/portfolio/Share.java @@ -1,6 +1,10 @@ -package edu.ntnu.idi.idatt.marked; +package edu.ntnu.idi.idatt.model.portfolio; import java.math.BigDecimal; +import java.math.RoundingMode; + +import edu.ntnu.idi.idatt.model.market.Stock; +import edu.ntnu.idi.idatt.service.transaction.SaleCalculator; /** * Share class @@ -48,4 +52,17 @@ public BigDecimal getPurchasePrice() { return purchasePrice; } + // TODO: JAVADOCS, JUNIT + public BigDecimal getTotalPurchasePrice() { + return purchasePrice.multiply(quantity); + } + + public BigDecimal getProfit() { + return new SaleCalculator(this).calculateGross().subtract(getTotalPurchasePrice()); + } + + public BigDecimal getProfitPercent() { + return getProfit().divide(getTotalPurchasePrice(), 2, RoundingMode.HALF_UP).multiply(BigDecimal.valueOf(100)); + } + } diff --git a/src/main/java/edu/ntnu/idi/idatt/transaction/Purchase.java b/src/main/java/edu/ntnu/idi/idatt/model/transaction/Purchase.java similarity index 79% rename from src/main/java/edu/ntnu/idi/idatt/transaction/Purchase.java rename to src/main/java/edu/ntnu/idi/idatt/model/transaction/Purchase.java index ac9152f..6423f5c 100644 --- a/src/main/java/edu/ntnu/idi/idatt/transaction/Purchase.java +++ b/src/main/java/edu/ntnu/idi/idatt/model/transaction/Purchase.java @@ -1,8 +1,8 @@ -package edu.ntnu.idi.idatt.transaction; +package edu.ntnu.idi.idatt.model.transaction; -import edu.ntnu.idi.idatt.Player; -import edu.ntnu.idi.idatt.calculator.PurchaseCalculator; -import edu.ntnu.idi.idatt.marked.Share; +import edu.ntnu.idi.idatt.model.player.Player; +import edu.ntnu.idi.idatt.model.portfolio.Share; +import edu.ntnu.idi.idatt.service.transaction.PurchaseCalculator; /** * Purchase class diff --git a/src/main/java/edu/ntnu/idi/idatt/transaction/Sale.java b/src/main/java/edu/ntnu/idi/idatt/model/transaction/Sale.java similarity index 79% rename from src/main/java/edu/ntnu/idi/idatt/transaction/Sale.java rename to src/main/java/edu/ntnu/idi/idatt/model/transaction/Sale.java index e0b6292..d0debd8 100644 --- a/src/main/java/edu/ntnu/idi/idatt/transaction/Sale.java +++ b/src/main/java/edu/ntnu/idi/idatt/model/transaction/Sale.java @@ -1,8 +1,8 @@ -package edu.ntnu.idi.idatt.transaction; +package edu.ntnu.idi.idatt.model.transaction; -import edu.ntnu.idi.idatt.Player; -import edu.ntnu.idi.idatt.calculator.SaleCalculator; -import edu.ntnu.idi.idatt.marked.Share; +import edu.ntnu.idi.idatt.model.player.Player; +import edu.ntnu.idi.idatt.model.portfolio.Share; +import edu.ntnu.idi.idatt.service.transaction.SaleCalculator; /** * Sale class diff --git a/src/main/java/edu/ntnu/idi/idatt/transaction/Transaction.java b/src/main/java/edu/ntnu/idi/idatt/model/transaction/Transaction.java similarity index 85% rename from src/main/java/edu/ntnu/idi/idatt/transaction/Transaction.java rename to src/main/java/edu/ntnu/idi/idatt/model/transaction/Transaction.java index f28629e..e42a892 100644 --- a/src/main/java/edu/ntnu/idi/idatt/transaction/Transaction.java +++ b/src/main/java/edu/ntnu/idi/idatt/model/transaction/Transaction.java @@ -1,8 +1,8 @@ -package edu.ntnu.idi.idatt.transaction; +package edu.ntnu.idi.idatt.model.transaction; -import edu.ntnu.idi.idatt.Player; -import edu.ntnu.idi.idatt.calculator.TransactionCalculator; -import edu.ntnu.idi.idatt.marked.Share; +import edu.ntnu.idi.idatt.model.player.Player; +import edu.ntnu.idi.idatt.model.portfolio.Share; +import edu.ntnu.idi.idatt.service.transaction.TransactionCalculator; /** * Transaction class diff --git a/src/main/java/edu/ntnu/idi/idatt/transaction/TransactionArchive.java b/src/main/java/edu/ntnu/idi/idatt/model/transaction/TransactionArchive.java similarity index 72% rename from src/main/java/edu/ntnu/idi/idatt/transaction/TransactionArchive.java rename to src/main/java/edu/ntnu/idi/idatt/model/transaction/TransactionArchive.java index 0567591..6112c89 100644 --- a/src/main/java/edu/ntnu/idi/idatt/transaction/TransactionArchive.java +++ b/src/main/java/edu/ntnu/idi/idatt/model/transaction/TransactionArchive.java @@ -1,4 +1,4 @@ -package edu.ntnu.idi.idatt.transaction; +package edu.ntnu.idi.idatt.model.transaction; import java.util.ArrayList; import java.util.List; @@ -44,6 +44,11 @@ public List getTransactions(int week) { return transactions.stream().filter(transaction -> transaction.getWeek() == week).toList(); } + // TODO: java, junit + public List getTransactions() { + return transactions; + } + /** * Getter for purchases done * @@ -56,6 +61,13 @@ public List getPurchases(int week) { .toList(); } + // TODO: java, junit + public List getPurchases() { + return getTransactions().stream().filter(t -> t instanceof Purchase) + .map(t -> (Purchase) t) + .toList(); + } + /** * Getter for sales done * @@ -68,13 +80,23 @@ public List getSales(int week) { .toList(); } + // TODO: java, junit + public List getSales() { + return getTransactions().stream().filter(t -> t instanceof Sale) + .map(t -> (Sale) t) + .toList(); + } + /** * Part 2 * * @return */ - public int countDistinctWeeks() { // TODO: HERE - return -1; + public int countDistinctWeeks() { + return (int) transactions.stream() + .map(Transaction::getWeek) + .distinct() + .count(); } } diff --git a/src/main/java/edu/ntnu/idi/idatt/calculator/PurchaseCalculator.java b/src/main/java/edu/ntnu/idi/idatt/service/transaction/PurchaseCalculator.java similarity index 93% rename from src/main/java/edu/ntnu/idi/idatt/calculator/PurchaseCalculator.java rename to src/main/java/edu/ntnu/idi/idatt/service/transaction/PurchaseCalculator.java index fd7847f..e43a80f 100644 --- a/src/main/java/edu/ntnu/idi/idatt/calculator/PurchaseCalculator.java +++ b/src/main/java/edu/ntnu/idi/idatt/service/transaction/PurchaseCalculator.java @@ -1,9 +1,9 @@ -package edu.ntnu.idi.idatt.calculator; - -import edu.ntnu.idi.idatt.marked.Share; +package edu.ntnu.idi.idatt.service.transaction; import java.math.BigDecimal; +import edu.ntnu.idi.idatt.model.portfolio.Share; + /** * PurchaseCalculator class * diff --git a/src/main/java/edu/ntnu/idi/idatt/calculator/SaleCalculator.java b/src/main/java/edu/ntnu/idi/idatt/service/transaction/SaleCalculator.java similarity index 87% rename from src/main/java/edu/ntnu/idi/idatt/calculator/SaleCalculator.java rename to src/main/java/edu/ntnu/idi/idatt/service/transaction/SaleCalculator.java index 6c65302..47819d5 100644 --- a/src/main/java/edu/ntnu/idi/idatt/calculator/SaleCalculator.java +++ b/src/main/java/edu/ntnu/idi/idatt/service/transaction/SaleCalculator.java @@ -1,8 +1,9 @@ -package edu.ntnu.idi.idatt.calculator; +package edu.ntnu.idi.idatt.service.transaction; -import edu.ntnu.idi.idatt.marked.Share; +import edu.ntnu.idi.idatt.model.portfolio.Share; import java.math.BigDecimal; +import java.math.RoundingMode; /** * SaleCalculator class @@ -76,4 +77,9 @@ public BigDecimal calculateTotal() { return calculateGross().subtract(calculateCommision()).subtract(calculateTax()); } + // TODO: Javadocs, junit + public BigDecimal calculateProfit() { + return calculateGross().subtract(purchasePrice.multiply(quantity)); + } + } diff --git a/src/main/java/edu/ntnu/idi/idatt/calculator/TransactionCalculator.java b/src/main/java/edu/ntnu/idi/idatt/service/transaction/TransactionCalculator.java similarity index 92% rename from src/main/java/edu/ntnu/idi/idatt/calculator/TransactionCalculator.java rename to src/main/java/edu/ntnu/idi/idatt/service/transaction/TransactionCalculator.java index fdfa9b1..8b75337 100644 --- a/src/main/java/edu/ntnu/idi/idatt/calculator/TransactionCalculator.java +++ b/src/main/java/edu/ntnu/idi/idatt/service/transaction/TransactionCalculator.java @@ -1,4 +1,4 @@ -package edu.ntnu.idi.idatt.calculator; +package edu.ntnu.idi.idatt.service.transaction; import java.math.BigDecimal; diff --git a/src/main/java/edu/ntnu/idi/idatt/session/UserSession.java b/src/main/java/edu/ntnu/idi/idatt/session/UserSession.java new file mode 100644 index 0000000..2fef3e5 --- /dev/null +++ b/src/main/java/edu/ntnu/idi/idatt/session/UserSession.java @@ -0,0 +1,87 @@ +package edu.ntnu.idi.idatt.session; + +import java.math.BigDecimal; + +import edu.ntnu.idi.idatt.model.Exchange; +import edu.ntnu.idi.idatt.model.player.Player; +import edu.ntnu.idi.idatt.storage.SessionManager; +import javafx.beans.property.SimpleObjectProperty; + +public class UserSession { + + // Singleton instance + private static UserSession INSTANCE; + + // Disable constructor initialization. + private UserSession() { + } + + public static UserSession getInstance() { + if (INSTANCE == null) { + INSTANCE = new UserSession(); + } + return INSTANCE; + } + + private Player player; + private Exchange exchange; + + public Player getPlayer() { + return player; + } + + public void setPlayer(Player player) { + this.player = player; + updateGameState(); // Startup hook + } + + public Exchange getExchange() { + return exchange; + } + + public void setExchange(Exchange exchange) { + this.exchange = exchange; + } + + private final SimpleObjectProperty moneyProperty = new SimpleObjectProperty<>(BigDecimal.ZERO); + private final SimpleObjectProperty netWorthProperty = new SimpleObjectProperty<>(BigDecimal.ZERO); + + public SimpleObjectProperty moneyProperty() { + return moneyProperty; + } + + public SimpleObjectProperty netWorthProperty() { + return netWorthProperty; + } + + public void updateGameState() { + moneyProperty.set(player.getMoney()); + netWorthProperty.set(player.getNetWorth()); + SessionManager.saveSession(); + } + + public SessionBundle getSession() { + return new SessionBundle(player, exchange); + } + + public class SessionBundle { + + private Player player; + private Exchange exchange; + + public SessionBundle(Player player, Exchange exchange) { + this.player = player; + this.exchange = exchange; + } + + public Player getPlayer() { + return player; + } + + public Exchange getExchange() { + return exchange; + } + + } + +} diff --git a/src/main/java/edu/ntnu/idi/idatt/storage/SessionManager.java b/src/main/java/edu/ntnu/idi/idatt/storage/SessionManager.java new file mode 100644 index 0000000..94626d1 --- /dev/null +++ b/src/main/java/edu/ntnu/idi/idatt/storage/SessionManager.java @@ -0,0 +1,153 @@ +package edu.ntnu.idi.idatt.storage; + +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.Writer; +import java.lang.reflect.Type; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.List; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonIOException; +import com.google.gson.JsonSyntaxException; +import com.google.gson.reflect.TypeToken; + +import edu.ntnu.idi.idatt.model.Exchange; +import edu.ntnu.idi.idatt.model.market.Stock; +import edu.ntnu.idi.idatt.model.player.Player; +import edu.ntnu.idi.idatt.model.portfolio.Portfolio; +import edu.ntnu.idi.idatt.model.portfolio.Share; +import edu.ntnu.idi.idatt.model.transaction.Transaction; +import edu.ntnu.idi.idatt.service.transaction.TransactionCalculator; +import edu.ntnu.idi.idatt.session.UserSession; +import edu.ntnu.idi.idatt.session.UserSession.SessionBundle; +import edu.ntnu.idi.idatt.storage.util.TransactionAdapter; +import edu.ntnu.idi.idatt.storage.util.TransactionCalculatorAdapter; + +/** + * Class for managing user sessions. + * + *

+ * Utilizes gson serialization and deserialization to manage + * player and game state. + */ +public class SessionManager { + + // Gson type to list format. Instead of using wrapper class. + private static Type SESSION_BUNDLE_TYPE = new TypeToken>() { + }.getType(); + + private static Gson gson = new GsonBuilder() + .setPrettyPrinting() + .registerTypeAdapter(Transaction.class, new TransactionAdapter()) + .registerTypeAdapter(TransactionCalculator.class, new TransactionCalculatorAdapter()) + .create(); + + // Static initiator to ensure persistent storage file. + static { + StorageFile.ensureAppDataDirectoryExists(); + } + + /** + * Method for setting new session. + * + * @see UserSession + */ + public static void newSession(Player player, Exchange exchange) { + UserSession.getInstance().setPlayer(player); + UserSession.getInstance().setExchange(exchange); + } + + /** + * Method for saving current session. + */ + public static void saveSession() { + // don't save if current session is null accidentally + if (UserSession.getInstance().getPlayer() == null || UserSession.getInstance().getExchange() == null) { + return; + } + + // Load all sessions + List bundles = loadAllSessions(); + + try (Writer writer = new FileWriter(StorageFile.getStorageFile().toFile())) { + + // Append current session + SessionBundle existing = bundles.stream() + .filter(s -> s.getPlayer().getName().equals(UserSession.getInstance().getPlayer().getName())) + .findFirst().orElse(null); + + if (existing != null) { + bundles.set(bundles.indexOf(existing), UserSession.getInstance().getSession()); + } else { + bundles.add(UserSession.getInstance().getSession()); + } + + gson.toJson(bundles, writer); + + } catch (IOException e) { + throw new RuntimeException("Failed to save current session!", e); + } + } + + /** + * Method for loading a user session by player name. + * + * @return if session was found or not. + */ + public static boolean loadSession(String playerName) { + + List bundles = loadAllSessions(); + + for (SessionBundle session : bundles) { + if (session.getPlayer().getName().equals(playerName)) { + UserSession.getInstance().setPlayer(session.getPlayer()); + UserSession.getInstance().setExchange(session.getExchange()); + + // Reseed portfolio based on exchange's stock instances. + // This has to be done due to Gson loading via reflection. + Portfolio portfolio = UserSession.getInstance().getPlayer().getPortfolio(); + ArrayList validatedShares = new ArrayList<>(); + + for (Share oldShare : portfolio.getShares()) { // Create new instances + Stock stock = UserSession.getInstance().getExchange().getStock(oldShare.getStock().getSymbol()); + Share newShare = new Share(stock, oldShare.getQuantity(), oldShare.getPurchasePrice()); + validatedShares.add(newShare); + } + + portfolio.removeShares(); // Remove all old + validatedShares.forEach(share -> portfolio.addShare(share)); // Add new + + return true; + } + } + return false; + } + + /** + * Method for serialization of all sessions. + * + * @return List or empty list if none entires were made. + */ + private static List loadAllSessions() { + try { + if (!Files.exists(StorageFile.getStorageFile()) || Files.size(StorageFile.getStorageFile()) == 0) { + return new ArrayList<>(); + } + } catch (IOException e) { + return new ArrayList<>(); + } + + try { + return gson.fromJson(new FileReader(StorageFile.getStorageFile().toString()), SESSION_BUNDLE_TYPE); + } catch (JsonSyntaxException | JsonIOException | FileNotFoundException e) { + return new ArrayList<>(); + } + + } + +} diff --git a/src/main/java/edu/ntnu/idi/idatt/storage/StockParser.java b/src/main/java/edu/ntnu/idi/idatt/storage/StockParser.java new file mode 100644 index 0000000..a4936dc --- /dev/null +++ b/src/main/java/edu/ntnu/idi/idatt/storage/StockParser.java @@ -0,0 +1,76 @@ +package edu.ntnu.idi.idatt.storage; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.math.BigDecimal; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; + +import edu.ntnu.idi.idatt.model.market.Stock; + +/** + * Utility class for parsing stocks. + * + *

+ * Decomponents files into the .csv (comma separated valuus) + * format, and creats stocks out of them. + *

+ */ +public class StockParser { + + // Disable initialization. + private StockParser() { + + } + + /** + * Method for loading from stocks from file + * + * @param path - The path to the .csv file. + * + * @return a list of loaded stocks. + * @throws IOException on BufferedReader error + */ + public static List load(String path) throws IOException { + File file = new File(path.toString()); + if (!file.exists()) { + throw new IOException("File at this path doesn't exist!"); + } + + if (!Files.probeContentType(Paths.get(file.getPath())).equals("text/csv")) { + throw new IOException("Please choose a .csv file!"); + } + + ArrayList stocks = new ArrayList<>(); + + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + List stockStringList = new ArrayList<>(reader.readAllLines()); + + // Remove comments and inproper syntax + stockStringList.removeIf(s -> s.isBlank()); + stockStringList.removeIf(s -> s.startsWith("#")); + stockStringList.removeIf(s -> !s.contains(",")); + + for (String stockString : stockStringList) { + String[] stockValues = stockString.split(","); + if (stockValues.length != 3) { + throw new IOException("Invalid CSV format!"); + } + + Stock stock = new Stock(stockValues[0], stockValues[1], List.of(new BigDecimal(stockValues[2]))); + stocks.add(stock); + } + + } catch (IOException e) { + throw new IOException("File loading failed!"); + } + + return stocks; + + } + +} diff --git a/src/main/java/edu/ntnu/idi/idatt/storage/StorageFile.java b/src/main/java/edu/ntnu/idi/idatt/storage/StorageFile.java new file mode 100644 index 0000000..9fa36e7 --- /dev/null +++ b/src/main/java/edu/ntnu/idi/idatt/storage/StorageFile.java @@ -0,0 +1,75 @@ +package edu.ntnu.idi.idatt.storage; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +/** + * Utility class for system-specific path obtaining. + */ +public class StorageFile { + private static final String STORAGE_FOLDER = "Millions"; + + /** + * Method for obtaining storage file path. + * + * @return Path object of storage file. + */ + public static Path getStorageFile() { + return getAppDataDirectory().resolve("storage.json"); + } + + /** + * Method for ensuring that storage directory exists. + * + *

+ * Attempts to create the application data directory if + * one does not exist. + *

+ */ + public static void ensureAppDataDirectoryExists() { + Path dir = getAppDataDirectory(); + try { + Files.createDirectories(dir); + + Path storageFile = dir.resolve("storage.json"); + if (!Files.exists(storageFile)) { + Files.createFile(storageFile); + } + } catch (IOException e) { + throw new RuntimeException("Could not create app data directory: " + dir, e); + } + } + + /** + * Method for obtaining system-specific directory for application data. + * + * @return Path object of the found directory. + */ + private static Path getAppDataDirectory() { + String userHome = System.getProperty("user.home"); + String os = System.getProperty("os.name").toLowerCase(); + + // AppData folder for windows + if (os.contains("win")) { + String appData = System.getenv("APPDATA"); + + if (appData != null) { + return Paths.get(appData, STORAGE_FOLDER); + } + // If AppData not a environmental variable, sets to user directory. + return Paths.get(userHome, STORAGE_FOLDER); + + } + // Library folder in MacOS + if (os.contains("mac")) { + return Paths.get(userHome, "Library", "Application Support", STORAGE_FOLDER); + } + + // All linux and unix-like systems. + return Paths.get(userHome, ".local", "share", STORAGE_FOLDER); + + } + +} diff --git a/src/main/java/edu/ntnu/idi/idatt/storage/util/TransactionAdapter.java b/src/main/java/edu/ntnu/idi/idatt/storage/util/TransactionAdapter.java new file mode 100644 index 0000000..acc53be --- /dev/null +++ b/src/main/java/edu/ntnu/idi/idatt/storage/util/TransactionAdapter.java @@ -0,0 +1,56 @@ +package edu.ntnu.idi.idatt.storage.util; + +import java.lang.reflect.Type; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; + +import edu.ntnu.idi.idatt.model.transaction.Purchase; +import edu.ntnu.idi.idatt.model.transaction.Sale; +import edu.ntnu.idi.idatt.model.transaction.Transaction; + +public class TransactionAdapter implements JsonSerializer, JsonDeserializer { + + @Override + public JsonElement serialize(Transaction t, Type typeOfT, JsonSerializationContext context) { + + JsonObject obj = context.serialize(t).getAsJsonObject(); + + if (t instanceof Purchase) { + obj.addProperty("type", "purchase"); + } else if (t instanceof Sale) { + obj.addProperty("type", "sale"); + } + + return obj; + } + + @Override + public Transaction deserialize(JsonElement json, + Type typeOfT, + JsonDeserializationContext context) + throws JsonParseException { + + JsonObject obj = json.getAsJsonObject(); + + String type = obj.get("type").getAsString(); + + switch (type) { + case "purchase": + return context.deserialize(obj, Purchase.class); + + case "sale": + return context.deserialize(obj, Sale.class); + + default: + throw new UnsupportedOperationException("Unknown type " + type); + } + + } + +} diff --git a/src/main/java/edu/ntnu/idi/idatt/storage/util/TransactionCalculatorAdapter.java b/src/main/java/edu/ntnu/idi/idatt/storage/util/TransactionCalculatorAdapter.java new file mode 100644 index 0000000..89f3204 --- /dev/null +++ b/src/main/java/edu/ntnu/idi/idatt/storage/util/TransactionCalculatorAdapter.java @@ -0,0 +1,57 @@ +package edu.ntnu.idi.idatt.storage.util; + +import java.lang.reflect.Type; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; + +import edu.ntnu.idi.idatt.service.transaction.PurchaseCalculator; +import edu.ntnu.idi.idatt.service.transaction.SaleCalculator; +import edu.ntnu.idi.idatt.service.transaction.TransactionCalculator; + +public class TransactionCalculatorAdapter + implements JsonSerializer, JsonDeserializer { + + @Override + public JsonElement serialize(TransactionCalculator calc, Type typeOfT, JsonSerializationContext context) { + + JsonObject obj = context.serialize(calc).getAsJsonObject(); + + if (calc instanceof PurchaseCalculator) { + obj.addProperty("type", "purchase"); + } else if (calc instanceof SaleCalculator) { + obj.addProperty("type", "sale"); + } + + return obj; + } + + @Override + public TransactionCalculator deserialize(JsonElement json, + Type typeOfT, + JsonDeserializationContext context) + throws JsonParseException { + + JsonObject obj = json.getAsJsonObject(); + + String type = obj.get("type").getAsString(); + + switch (type) { + case "purchase": + return context.deserialize(obj, PurchaseCalculator.class); + + case "sale": + return context.deserialize(obj, SaleCalculator.class); + + default: + throw new UnsupportedOperationException("Unknown type " + type); + } + + } + +} diff --git a/src/main/java/edu/ntnu/idi/idatt/view/SceneFactory.java b/src/main/java/edu/ntnu/idi/idatt/view/SceneFactory.java new file mode 100644 index 0000000..165182a --- /dev/null +++ b/src/main/java/edu/ntnu/idi/idatt/view/SceneFactory.java @@ -0,0 +1,149 @@ +package edu.ntnu.idi.idatt.view; + +import edu.ntnu.idi.idatt.model.market.Stock; +import edu.ntnu.idi.idatt.session.UserSession; +import edu.ntnu.idi.idatt.view.entry.StartController; +import edu.ntnu.idi.idatt.view.entry.StartModel; +import edu.ntnu.idi.idatt.view.entry.StartView; +import edu.ntnu.idi.idatt.view.primary.newspaper.NewspaperController; +import edu.ntnu.idi.idatt.view.primary.newspaper.NewspaperModel; +import edu.ntnu.idi.idatt.view.primary.newspaper.NewspaperView; +import edu.ntnu.idi.idatt.view.primary.portfolio.PortfolioController; +import edu.ntnu.idi.idatt.view.primary.portfolio.PortfolioModel; +import edu.ntnu.idi.idatt.view.primary.portfolio.PortfolioView; +import edu.ntnu.idi.idatt.view.primary.exchange.ExchangeController; +import edu.ntnu.idi.idatt.view.primary.exchange.ExchangeModel; +import edu.ntnu.idi.idatt.view.primary.exchange.ExchangeView; +import edu.ntnu.idi.idatt.view.primary.stock.StockController; +import edu.ntnu.idi.idatt.view.primary.stock.StockModel; +import edu.ntnu.idi.idatt.view.primary.stock.StockView; +import edu.ntnu.idi.idatt.view.primary.transactions.TransactionController; +import edu.ntnu.idi.idatt.view.primary.transactions.TransactionModel; +import edu.ntnu.idi.idatt.view.primary.transactions.TransactionView; +import javafx.scene.Parent; + +import java.util.ArrayDeque; +import java.util.Deque; + +public class SceneFactory { + + @FunctionalInterface + public interface MVCInitInterface { + Parent execute(); + } + + private static Deque navigation = new ArrayDeque<>(); + private static boolean navigatingBack = false; + + public static void goBack() { + if (navigation.size() > 1) { + navigation.pop(); + navigatingBack = true; + SceneManager.switchTo(navigation.peek().execute()); + navigatingBack = false; + } + } + + public static void reloadCurrent() { + navigatingBack = true; + SceneManager.switchTo(navigation.peek().execute()); + navigatingBack = false; + } + + private static void mark(MVCInitInterface initializer) { + if (!navigatingBack) { + navigation.push(initializer); + } + } + + public static boolean isFinal() { + return navigation.size() == 1; + } + + public static Parent createStartView() { + + navigation.clear(); + + StartModel model = new StartModel(); + StartView view = new StartView(); + StartController controller = new StartController(model); + + view.setModel(model); + view.setController(controller); + + return view.getInstance(); + + } + + public static Parent createPortfolioView() { + + mark(() -> createPortfolioView()); + + PortfolioModel model = new PortfolioModel(); + PortfolioView view = new PortfolioView(); + PortfolioController controller = new PortfolioController(model); + + view.setModel(model); + view.setController(controller); + + return view.getInstance(); + } + + public static Parent createExchangeView() { + + mark(() -> createExchangeView()); + + ExchangeModel model = new ExchangeModel(); + ExchangeView view = new ExchangeView(); + ExchangeController controller = new ExchangeController(model); + + view.setModel(model); + view.setController(controller); + + return view.getInstance(); + + } + + public static Parent createStockView(String symbol) { + + mark(() -> createStockView(symbol)); + Stock stock = UserSession.getInstance().getExchange().getStock(symbol); + + StockModel model = new StockModel(); + StockView view = new StockView(); + StockController controller = new StockController(model, stock); + + view.setModel(model); + view.setController(controller); + + return view.getInstance(); + } + + public static Parent createTransactionView() { + + mark(() -> createTransactionView()); + + TransactionModel model = new TransactionModel(); + TransactionView view = new TransactionView(); + TransactionController controller = new TransactionController(model); + + view.setModel(model); + view.setController(controller); + + return view.getInstance(); + } + public static Parent createNewspaperView() { + + mark(() -> createNewspaperView()); + + NewspaperModel model = new NewspaperModel(); + NewspaperView view = new NewspaperView(); + NewspaperController controller = new NewspaperController(model); + + view.setModel(model); + view.setController(controller); + + return view.getInstance(); + } + +} diff --git a/src/main/java/edu/ntnu/idi/idatt/view/SceneManager.java b/src/main/java/edu/ntnu/idi/idatt/view/SceneManager.java new file mode 100644 index 0000000..be87d13 --- /dev/null +++ b/src/main/java/edu/ntnu/idi/idatt/view/SceneManager.java @@ -0,0 +1,55 @@ +package edu.ntnu.idi.idatt.view; + +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.stage.Stage; + +/** + * SceneManager class for managing objects to the stage. + * + *

+ * This class provides methods for changing the root object to + * the scene, providing simple use of components to change between the + * pages + *

+ * + * @author Pawel Sapula + */ +public final class SceneManager { + + // Store scene reference. + private static Scene scene; + + // Disable initialization. + private SceneManager() { + } + + /** + * Method for intialization of the Scene Manager. + * + *

+ * This method enables initialization of the primary scene, + * based on the stage from the program entry point, and a + * page based on the page's root object. + *

+ * + * @param stage - Application's stage + * @param root - The root object / fundamental object of a page + */ + public static void init(Stage stage, Parent root) { + scene = new Scene(root); + // Add styling sheet permamently for the scene. + scene.getStylesheets().add(SceneManager.class.getResource("/themes/default.css").toExternalForm()); + stage.setScene(scene); + } + + /** + * Method for switching the scene's root variable. + * + * @param root - Parent JavaFX object. + */ + public static void switchTo(Parent root) { + scene.setRoot(root); + } + +} diff --git a/src/main/java/edu/ntnu/idi/idatt/view/components/AbstractController.java b/src/main/java/edu/ntnu/idi/idatt/view/components/AbstractController.java new file mode 100644 index 0000000..a334769 --- /dev/null +++ b/src/main/java/edu/ntnu/idi/idatt/view/components/AbstractController.java @@ -0,0 +1,11 @@ +package edu.ntnu.idi.idatt.view.components; + +public abstract class AbstractController { + + protected final M model; + + public AbstractController(M model) { + this.model = model; + } + +} diff --git a/src/main/java/edu/ntnu/idi/idatt/view/components/AbstractView.java b/src/main/java/edu/ntnu/idi/idatt/view/components/AbstractView.java new file mode 100644 index 0000000..b4f96ec --- /dev/null +++ b/src/main/java/edu/ntnu/idi/idatt/view/components/AbstractView.java @@ -0,0 +1,72 @@ +package edu.ntnu.idi.idatt.view.components; + +import javafx.scene.Parent; +import javafx.scene.layout.Pane; + +/** + * INFO! + * + *

+ * This class is based on the generic Type parameter - T. It's experimental + * for the time of creating the class (24.04.26), since parent don't have access + * to the + * getChildren() method which could provide usefuleness in creating a standalone + * abstract class. + * + * Since most views will be based on the Pane subclasses, it is fully + * functional, but + * may throw errors if the typeparameter isn't of this type. + *

+ * + * Reference: https://docs.oracle.com/javase/8/javafx/api/overview-tree.html + */ + +/** + * AbstractView class + * + *

+ * Provides a simple abstraction of the Pane subclasses, + * making it a useful framework for creating different views. + *

+ */ +public abstract class AbstractView { + + private T instance; + + /** + * Constructor for AbstractView. + */ + protected AbstractView(T type) { + instance = type; + instance.getChildren().add(this.createContent()); + } + + /** + * Constructor for AbstractView without initialization. + */ + protected AbstractView() { + + } + + /** + * Getter for current view instance. + * + * @return Instance of current view. + */ + public T getInstance() { + return instance; + } + + /** + * Setter for current view instance. + */ + public void setInstance(T instance) { + this.instance = instance; + } + + /** + * Abstract method for creating view content. + */ + public abstract Parent createContent(); + +} diff --git a/src/main/java/edu/ntnu/idi/idatt/view/components/AbstractViewUI.java b/src/main/java/edu/ntnu/idi/idatt/view/components/AbstractViewUI.java new file mode 100644 index 0000000..8c1a119 --- /dev/null +++ b/src/main/java/edu/ntnu/idi/idatt/view/components/AbstractViewUI.java @@ -0,0 +1,106 @@ +package edu.ntnu.idi.idatt.view.components; + +import javafx.beans.property.SimpleBooleanProperty; +import javafx.geometry.Pos; +import javafx.scene.Parent; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; + +public abstract class AbstractViewUI extends AbstractView { + + private VBox navigation; + private HBox header; + private HBox toolbar; + private VBox menu; + private SimpleBooleanProperty isMenuVisible = new SimpleBooleanProperty(false); + + public AbstractViewUI() { + HBox wrapper = new HBox(); + BorderPane layout = new BorderPane(); + + createUIComponents(); + navigation.getChildren().add(createNavigation()); + header.getChildren().add(createHeader()); + toolbar.getChildren().add(createToolbar()); + + layout.setTop(header); + layout.setBottom(toolbar); + layout.setCenter(createContent()); + + HBox.setHgrow(layout, Priority.ALWAYS); + wrapper.getChildren().addAll(navigation, layout); + + this.setInstance(new StackPane()); + + menu.getChildren().add(createMenu()); + menu.setVisible(false); + + Region disableMenu = new Region(); + disableMenu.setVisible(false); + + disableMenu.setOnMouseClicked(e -> { + menu.setVisible(false); + disableMenu.setVisible(false); + }); + + isMenuVisible.addListener((observer, oldVal, newVal) -> { + menu.setVisible(true); + disableMenu.setVisible(true); + }); + + this.getInstance().getChildren().addAll(wrapper, disableMenu, menu); + } + + public void createUIComponents() { + navigation = new VBox(); + navigation.setMaxHeight(Double.MAX_VALUE); + navigation.getStyleClass().add("dark"); + navigation.setPrefWidth(150); // ScrollPane's affect this massively + navigation.setMaxWidth(150); + navigation.setMinWidth(150); + + header = new HBox(); + header.getStyleClass().add("light"); + header.setMaxHeight(80); + + toolbar = new HBox(); + toolbar.getStyleClass().add("light"); + toolbar.setMaxHeight(80); + + menu = new VBox(); + StackPane.setAlignment(menu, Pos.CENTER_RIGHT); + menu.getStyleClass().add("dark"); + menu.setMaxWidth(300); + } + + public abstract Parent createContent(); + + public abstract Parent createNavigation(); + + public abstract Parent createHeader(); + + public abstract Parent createToolbar(); + + public abstract Parent createMenu(); + + public VBox getNavigation() { + return navigation; + } + + public HBox getHeader() { + return header; + } + + public HBox getToolbar() { + return toolbar; + } + + public void toggleMenu() { + isMenuVisible.set(!isMenuVisible.get()); + } + +} diff --git a/src/main/java/edu/ntnu/idi/idatt/view/components/Model.java b/src/main/java/edu/ntnu/idi/idatt/view/components/Model.java new file mode 100644 index 0000000..2465fd7 --- /dev/null +++ b/src/main/java/edu/ntnu/idi/idatt/view/components/Model.java @@ -0,0 +1,4 @@ +package edu.ntnu.idi.idatt.view.components; + +public interface Model { +} diff --git a/src/main/java/edu/ntnu/idi/idatt/view/components/elements/IconComponent.java b/src/main/java/edu/ntnu/idi/idatt/view/components/elements/IconComponent.java new file mode 100644 index 0000000..a3ef67a --- /dev/null +++ b/src/main/java/edu/ntnu/idi/idatt/view/components/elements/IconComponent.java @@ -0,0 +1,32 @@ +package edu.ntnu.idi.idatt.view.components.elements; + +import edu.ntnu.idi.idatt.view.components.primitives.ActionEventHandler; +import javafx.scene.control.Button; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.StackPane; + +public class IconComponent extends StackPane { + + private Button icon; + + public IconComponent(Image image, String description, int size) { + + ImageView iv = new ImageView(); + iv.setImage(image); + iv.setFitHeight(size); // TODO: Fix? + iv.setFitWidth(size); + + icon = new Button(description, iv); + icon.getStyleClass().clear(); // Remove parent buffers in case. + icon.getStyleClass().add("icon"); + icon.setMaxHeight(Double.MAX_VALUE); + + this.getChildren().add(icon); + } + + public void onIconClick(ActionEventHandler handler) { + this.icon.setOnAction(e -> handler.handle()); + } + +} diff --git a/src/main/java/edu/ntnu/idi/idatt/view/components/elements/NewspaperComponent.java b/src/main/java/edu/ntnu/idi/idatt/view/components/elements/NewspaperComponent.java new file mode 100644 index 0000000..8661c9b --- /dev/null +++ b/src/main/java/edu/ntnu/idi/idatt/view/components/elements/NewspaperComponent.java @@ -0,0 +1,33 @@ +package edu.ntnu.idi.idatt.view.components.elements; + +import edu.ntnu.idi.idatt.model.portfolio.Share; +import edu.ntnu.idi.idatt.view.components.ui.UICompositor; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Label; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; + +public class NewspaperComponent extends HBox { + private final Label newsTitle; + private final Label newsText; + + public NewspaperComponent(Share share) { + this.setPadding(new Insets(30)); + this.getStyleClass().add("newspaper-article"); + + newsTitle = new Label("Earthquake in Norway"); + newsText = new Label("Intel are crashing"); + newsTitle.getStyleClass().add("newspaper-title"); + newsText.getStyleClass().add("newspaper-med-text"); + + UICompositor shareComponent = new UICompositor.Builder() + .parent(new VBox()) + .growWithAlignment(Pos.TOP_CENTER) + .addAllContent(newsTitle, newsText) + .build(); + + this.getChildren().add(shareComponent.makeUI()); + } + +} diff --git a/src/main/java/edu/ntnu/idi/idatt/view/components/elements/SearchBarComponent.java b/src/main/java/edu/ntnu/idi/idatt/view/components/elements/SearchBarComponent.java new file mode 100644 index 0000000..0e56051 --- /dev/null +++ b/src/main/java/edu/ntnu/idi/idatt/view/components/elements/SearchBarComponent.java @@ -0,0 +1,45 @@ +package edu.ntnu.idi.idatt.view.components.elements; + +import edu.ntnu.idi.idatt.view.components.primitives.ActionEventHandler; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.geometry.Pos; +import javafx.scene.control.TextField; +import javafx.scene.image.Image; +import javafx.scene.layout.HBox; + +public class SearchBarComponent extends HBox { + + private final StringProperty query = new SimpleStringProperty(); + private final IconComponent searchIcon; + + public SearchBarComponent(String placeholder) { + + HBox wrapper = new HBox(); + wrapper.getStyleClass().add("searchbar"); + wrapper.setAlignment(Pos.CENTER); + + TextField searchBar = new TextField(); + searchBar.getStyleClass().add("searchbar-field"); + searchBar.setPromptText(placeholder); + searchBar.setMaxHeight(Double.MAX_VALUE); + searchBar.textProperty().bindBidirectional(query); + searchBar.setFocusTraversable(false); + + Image image = new Image(this.getClass().getResource("/icons/search.png/").toExternalForm()); + searchIcon = new IconComponent(image, null, 32); + + wrapper.getChildren().addAll(searchBar, searchIcon); + + this.getChildren().addAll(wrapper); + } + + public String getQuery() { + return query.get(); + } + + public void onSearchQuery(ActionEventHandler handler) { + this.searchIcon.onIconClick(() -> handler.handle()); + } + +} diff --git a/src/main/java/edu/ntnu/idi/idatt/view/components/elements/ShareComponent.java b/src/main/java/edu/ntnu/idi/idatt/view/components/elements/ShareComponent.java new file mode 100644 index 0000000..486b994 --- /dev/null +++ b/src/main/java/edu/ntnu/idi/idatt/view/components/elements/ShareComponent.java @@ -0,0 +1,64 @@ +package edu.ntnu.idi.idatt.view.components.elements; + +import edu.ntnu.idi.idatt.model.portfolio.Share; +import edu.ntnu.idi.idatt.view.components.ui.UICompositor; +import edu.ntnu.idi.idatt.view.util.CssUtils; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; + +import java.util.List; +import java.util.function.Consumer; + +public class ShareComponent extends HBox { + + private final Share share; + private Button sellButton; + + public ShareComponent(Share share) { + this.share = share; + + this.setMaxSize(Double.MAX_VALUE, 300); + this.setPadding(new Insets(30)); + this.getStyleClass().add("rowBox"); + + Label title = new Label(share.getStock().toString()); + Label quantity = new Label("Owned shares: " + share.getQuantity().toString()); + Label latestValueLabel = new Label("Net profit:"); + Label latestValue = new Label(String.format("%.2f $", share.getProfit())); + Label latestValueChange = new Label(String.format("%.2f %%", share.getProfitPercent())); + + sellButton = new Button("Sell"); + + List