/* * Copyright (C) 2007 Snorre Gylterud, Stein Magnus Jodal, Johannes Knutsen, * Erik Bagge Ottesen, Ralf Bjarne Taraldset, and Iterate AS * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2 * as published by the Free Software Foundation. */ package no.ntnu.mmfplanner.model; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * Contains all information related to an IFM Project. This will handle involved * MMFs, data-consistency, calculations that include more than one MMF. If will * also forward any events from MMFs and Categories so that it is easier for * observers to observe only the Project object and not handle which MMFs and * Categories are a part of the Project. */ public class Project implements PropertyChangeListener { public static final String EVENT_NAME = "project.name"; public static final String EVENT_PERIODS = "project.periods"; public static final String EVENT_INTEREST_RATE = "project.interestRate"; public static final String EVENT_CATEGORIES = "project.categories"; public static final String EVENT_MMFS = "project.mmfs"; public static final String EVENT_MAX_MMFS = "project.maxMMFs"; private String name; private int periods; private double interestRate; private List<Category> categories; private List<Mmf> mmfs; private String nextId; private int maxMmfsPerPeriod; private PropertyChangeSupport changeSupport; /** * Creates a new IFM Project with default values for all properties. */ public Project() { this.name = "New MMF Project"; this.periods = 12; this.interestRate = 0.008; this.nextId = "A"; this.categories = new ArrayList<Category>(); this.mmfs = new ArrayList<Mmf>(); this.maxMmfsPerPeriod = 1; this.changeSupport = new PropertyChangeSupport(this); } public String getName() { return name; } /** * Sets the project name and fires the event EVENT_NAME. */ public void setName(String name) { String oldValue = this.name; this.name = name; changeSupport.firePropertyChange(EVENT_NAME, oldValue, name); } public int getPeriods() { return periods; } /** * Sets the number of periods and fires the event EVENT_PERIODS. Must be * greater than 0. Will not make any changes to the revenue data of MMFs. * That means MMF will remember revenue data that is entered for periods * beyond this setting. * * @throws MmfException */ public void setPeriods(int periods) throws MmfException { if ((periods < 1) || (periods > 105)) { throw new MmfException("Invalid number of periods: " + periods); } int oldValue = this.periods; this.periods = periods; changeSupport.firePropertyChange(EVENT_PERIODS, oldValue, periods); } public double getInterestRate() { return interestRate; } /** * Sets the interest rate per period and fires the event * EVENT_INTEREST_RATE. The value should be absolute, not in percent. (i.e. * 12% = 0.12) */ public void setInterestRate(double interestRate) { double oldValue = this.interestRate; this.interestRate = interestRate; changeSupport.firePropertyChange(EVENT_INTEREST_RATE, oldValue, interestRate); } /** * Adds the category to the list of categories and fires the * EVENT_CATEGORIES event. */ public void addCategory(Category category) { if (categories.indexOf(category) >= 0) { throw new IllegalArgumentException("This category already exists: " + category); } categories.add(category); category.addPropertyChangeListener(this); changeSupport.firePropertyChange(EVENT_CATEGORIES, null, category); } public int getFirstFreeSwimlane(int period, int swimlane) { // find occupied count of all swimlanes int occupied[] = new int[mmfs.size() + 1]; for (int i = 0; i < mmfs.size(); i++) { int s = mmfs.get(i).getSwimlane() - 1; if ((mmfs.get(i).getPeriod() == period) && (s >= 0) && (s < occupied.length)) { occupied[s]++; } } // return first free swimlane for (int i = swimlane - 1; i < occupied.length; i++) { if (occupied[i] == 0) { return i + 1; } } // should not come this far return 1; } /** * * @param category * @return true of this is a category of the current project, false if not */ public boolean isValidCategory(Category category) { return (null == category) || categories.contains(category); } public Category getCategory(int index) { return categories.get(index); } /** * @return a copy of the category list. Modifications to this list will not * have any effect on the category list. */ public List<Category> getCategories() { return Collections.unmodifiableList(categories); } /** * Removes the category from the list of categories and fires the * EVENT_CATEGORIES event. */ public void removeCategory(Category category) { category.removePropertyChangeListener(this); // remove as parent category for (Category cat : categories) { if (category == cat.getParent()) { try { cat.setParent(null); } catch (Exception e) { // This should never occur, but we print the stack trace // just in case e.printStackTrace(); } } } // remove as category from MMFs for (Mmf mmf : mmfs) { if (category == mmf.getCategory()) { try { mmf.setCategory(null); } catch (Exception e) { // This should never occur, but we print the stack trace // just in case e.printStackTrace(); } } } categories.remove(category); changeSupport.firePropertyChange(EVENT_CATEGORIES, category, null); } public int getCategorySize() { return categories.size(); } /** * @return the maxMMFsPerPeriod */ public int getMaxMmfsPerPeriod() { return maxMmfsPerPeriod; } /** * @param maxMmfsPerPeriod the maxMMFsPerPeriod to set * @throws MmfException */ public void setMaxMmfsPerPeriod(int maxMmfsPerPeriod) throws MmfException { if (maxMmfsPerPeriod < 0) { throw new MmfException("Invalid maxMMFsPerPeriod: " + maxMmfsPerPeriod); } int oldValue = this.maxMmfsPerPeriod; this.maxMmfsPerPeriod = maxMmfsPerPeriod; changeSupport.firePropertyChange(EVENT_MAX_MMFS, oldValue, maxMmfsPerPeriod); } /** * Adds the mmf to the list of mmfs and fires the EVENT_MMFS event. * * If the MMF contains an invalid or duplicate id it will be reassigned a * new id. */ public void add(Mmf mmf) { if (mmfs.indexOf(mmf) >= 0) { throw new IllegalArgumentException("This MMF already exists: " + mmf); } if (!isValidId(mmf.getId())) { try { mmf.setId(getNextId()); } catch (Exception e) { // Should never happend so we rethrow as a RuntimeException throw new IllegalArgumentException( "isValidId() != isValidId()", e); } } mmf.setProject(this); try { mmf.setSwimlane(getFirstFreeSwimlane(mmf.getPeriod(), 1)); } catch (MmfException e) { // Should never happen! e.printStackTrace(); } mmf.addPropertyChangeListener(this); mmfs.add(mmf); changeSupport.firePropertyChange(EVENT_MMFS, null, mmf); } public void setNextId(String nextId) { this.nextId = nextId; } /** * Checks if the given id is valid and has no duplicates in the current * project * * @return true of valid, false if not or a duplicate exists */ public boolean isValidId(String id) { return (null != id) && id.matches("Z*[A-Y]") && (null == get(id)); } /** * @return the id that should be used for the next MMF that is added. The * value is not increased until the next MMF is actually added. */ public String getNextId() { // check if next id is correct. while (!isValidId(nextId)) { // find next id value char nextChar = (char) (1 + nextId.charAt(nextId.length() - 1)); String pre = nextId.substring(0, nextId.length() - 1); nextId = pre + nextChar; if (nextChar == 'Z') { // We're at the last usable character in this set. We retry all // previous characters // in an attempt to avoid multiple characters, otherwise we add // another 'A' character for (int i = 0; i < 25 * 10 + 1; i++) { nextId = "ZZZZZZZZZZ".substring(0, i / 25) + (char) ('A' + i % 25); if (isValidId(nextId)) { return nextId; } } } } return nextId; } public Mmf get(int index) { return mmfs.get(index); } public Mmf get(String id) { for (Mmf mmf : mmfs) { if (id.equals(mmf.getId())) { return mmf; } } return null; } /** * @return a unmodifiable copy of the mmf list */ public List<Mmf> getMmfs() { return Collections.unmodifiableList(mmfs); } /** * Removes the mmf from the list of mmfs and fires the EVENT_MMFS event. */ public void remove(Mmf mmf) { mmf.removePropertyChangeListener(this); for (Mmf m : mmfs) { if (m.getPrecursors().contains(mmf)) { m.removePrecursor(mmf); } } mmf.setProject(null); mmfs.remove(mmf); changeSupport.firePropertyChange(EVENT_MMFS, mmf, null); } /** * @return the number of MMFs in the project */ public int size() { return mmfs.size(); } /** * @return An array of SANPV arrays. The first element is the SANPV array of * the first MMF, etc. */ public int[][] getSaNpvTable() { int table[][] = new int[size()][getPeriods()]; for (int i = 0; i < table.length; i++) { table[i] = get(i).getSaNpvList(interestRate); } return table; } /** * Is called whenever there is a change in a child MMF or Category. Project * does not directly use this, but forwards all events to the * PropertyChangeListeners of this project. */ public void propertyChange(PropertyChangeEvent evt) { changeSupport.firePropertyChange(evt); } /** * Add a PropertyChangeListener to be notified of changes to this object or * child objects (MMFs and Categories) */ public void addPropertyChangeListener(PropertyChangeListener listener) { changeSupport.addPropertyChangeListener(listener); } /** * Remove a PropertyChangeListener */ public void removePropertyChangeListener(PropertyChangeListener listener) { changeSupport.removePropertyChangeListener(listener); } }