package name.abuchen.portfolio.model;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.crypto.SecretKey;
import name.abuchen.portfolio.Messages;
import name.abuchen.portfolio.model.Classification.Assignment;
import name.abuchen.portfolio.money.CurrencyUnit;
public class Client
{
/* package */static final int MAJOR_VERSION = 1;
public static final int CURRENT_VERSION = 34;
public static final int VERSION_WITH_CURRENCY_SUPPORT = 29;
private transient PropertyChangeSupport propertyChangeSupport;
/**
* The (minor) version of the file format. If it is lower than the current
* version, then {@link ClientFactory#upgradeModel} will upgrade the model
* and set the version number to the current version.
*/
private int version = CURRENT_VERSION;
/**
* The (minor) version of the file format as it has been read from file.
*/
private transient int fileVersionAfterRead = CURRENT_VERSION;
private String baseCurrency = CurrencyUnit.EUR;
private List<Security> securities = new ArrayList<>();
private List<Watchlist> watchlists;
// keep typo -> xstream deserialization
private List<ConsumerPriceIndex> consumerPriceIndeces;
private List<Account> accounts = new ArrayList<>();
private List<Portfolio> portfolios = new ArrayList<>();
private List<InvestmentPlan> plans;
private List<Taxonomy> taxonomies;
private List<Dashboard> dashboards;
private Map<String, String> properties;
private ClientSettings settings;
@Deprecated
private String industryTaxonomyId;
@Deprecated
private Category rootCategory;
private transient SecretKey secret;
public Client()
{
doPostLoadInitialization();
}
/* package */final void doPostLoadInitialization()
{
// when loading the Client from XML, attributes that are not (yet)
// persisted in that version are not initialized
if (watchlists == null)
watchlists = new ArrayList<>();
if (consumerPriceIndeces == null)
consumerPriceIndeces = new ArrayList<>();
if (properties == null)
properties = new HashMap<>();
if (propertyChangeSupport == null)
propertyChangeSupport = new PropertyChangeSupport(this);
if (plans == null)
plans = new ArrayList<>();
if (taxonomies == null)
taxonomies = new ArrayList<>();
if (dashboards == null)
dashboards = new ArrayList<>();
if (settings == null)
settings = new ClientSettings();
else
settings.doPostLoadInitialization();
}
/* package */int getVersion()
{
return version;
}
/* package */void setVersion(int version)
{
this.version = version;
}
public int getFileVersionAfterRead()
{
return fileVersionAfterRead;
}
/* package */
void setFileVersionAfterRead(int fileVersionAfterRead)
{
this.fileVersionAfterRead = fileVersionAfterRead;
}
public String getBaseCurrency()
{
return baseCurrency;
}
public void setBaseCurrency(String baseCurrency)
{
propertyChangeSupport.firePropertyChange("baseCurrency", this.baseCurrency, this.baseCurrency = baseCurrency); //$NON-NLS-1$
}
public List<InvestmentPlan> getPlans()
{
return Collections.unmodifiableList(plans);
}
public void addPlan(InvestmentPlan plan)
{
plans.add(plan);
}
public void removePlan(InvestmentPlan plan)
{
plans.remove(plan);
}
public List<Security> getSecurities()
{
return Collections.unmodifiableList(securities);
}
/**
* Returns a sorted list of active securities, i.e. securities that are not
* marked as retired.
*/
public List<Security> getActiveSecurities()
{
return securities.stream() //
.filter(s -> s.getCurrencyCode() != null) //
.filter(s -> !s.isRetired()) //
.sorted(new Security.ByName()) //
.collect(Collectors.toList());
}
public void addSecurity(Security security)
{
Objects.requireNonNull(security);
securities.add(security);
}
public void removeSecurity(final Security security)
{
for (Watchlist w : watchlists)
w.getSecurities().remove(security);
deleteInvestmentPlans(security);
deleteTaxonomyAssignments(security);
deleteAccountTransactions(security);
deletePortfolioTransactions(security);
securities.remove(security);
}
public List<Watchlist> getWatchlists()
{
return watchlists;
}
public List<ConsumerPriceIndex> getConsumerPriceIndices()
{
return Collections.unmodifiableList(consumerPriceIndeces);
}
/**
* Sets the consumer price indices.
*
* @return true if the indices are modified.
*/
public boolean setConsumerPriceIndices(List<ConsumerPriceIndex> indices)
{
if (indices == null)
throw new IllegalArgumentException();
List<ConsumerPriceIndex> newValues = new ArrayList<>(indices);
Collections.sort(newValues, new ConsumerPriceIndex.ByDate());
if (consumerPriceIndeces == null || !consumerPriceIndeces.equals(newValues))
{
// only assign list if indices have actually changed because UI
// elements keep a reference which is not updated if no 'dirty'
// event is fired
this.consumerPriceIndeces = newValues;
return true;
}
else
{
return false;
}
}
public void addConsumerPriceIndex(ConsumerPriceIndex record)
{
consumerPriceIndeces.add(record);
}
public void removeConsumerPriceIndex(ConsumerPriceIndex record)
{
consumerPriceIndeces.remove(record);
}
public void addAccount(Account account)
{
accounts.add(account);
}
public void removeAccount(Account account)
{
deleteReferenceAccount(account);
deleteTransactions(account);
deleteInvestmentPlans(account);
deleteTaxonomyAssignments(account);
accounts.remove(account);
}
public List<Account> getAccounts()
{
return Collections.unmodifiableList(accounts);
}
/**
* Returns a sorted list of active accounts, i.e. accounts that are not
* marked as retired.
*/
public List<Account> getActiveAccounts()
{
return accounts.stream() //
.filter(a -> !a.isRetired()) //
.sorted(new Account.ByName()) //
.collect(Collectors.toList());
}
public void addPortfolio(Portfolio portfolio)
{
portfolios.add(portfolio);
}
public void removePortfolio(Portfolio portfolio)
{
deleteTransactions(portfolio);
deleteInvestmentPlans(portfolio);
portfolios.remove(portfolio);
}
public List<Portfolio> getPortfolios()
{
return Collections.unmodifiableList(portfolios);
}
/**
* Returns a sorted list of active portfolios, i.e. portfolios that are not
* marked as retired.
*/
public List<Portfolio> getActivePortfolios()
{
return portfolios.stream() //
.filter(p -> !p.isRetired()) //
.sorted(new Portfolio.ByName()) //
.collect(Collectors.toList());
}
@Deprecated
/* package */
Category getRootCategory()
{
return this.rootCategory;
}
@Deprecated
/* package */
void setRootCategory(Category rootCategory)
{
this.rootCategory = rootCategory;
}
@Deprecated
/* package */
String getIndustryTaxonomy()
{
return industryTaxonomyId;
}
@Deprecated
/* package */
void setIndustryTaxonomy(String industryTaxonomyId)
{
this.industryTaxonomyId = industryTaxonomyId;
}
public List<Taxonomy> getTaxonomies()
{
return Collections.unmodifiableList(taxonomies);
}
public void addTaxonomy(Taxonomy taxonomy)
{
taxonomies.add(taxonomy);
}
public void addTaxonomy(int index, Taxonomy taxonomy)
{
taxonomies.add(index, taxonomy);
}
public void removeTaxonomy(Taxonomy taxonomy)
{
taxonomies.remove(taxonomy);
}
public Taxonomy getTaxonomy(String id)
{
return taxonomies.stream() //
.filter(t -> id.equals(t.getId())) //
.findAny().orElse(null);
}
public Stream<Dashboard> getDashboards()
{
return dashboards.stream();
}
public void addDashboard(Dashboard dashboard)
{
this.dashboards.add(dashboard);
}
public void removeDashboard(Dashboard dashboard)
{
this.dashboards.remove(dashboard);
}
public ClientSettings getSettings()
{
return settings;
}
public void setProperty(String key, String value)
{
String oldValue = properties.put(key, value);
propertyChangeSupport.firePropertyChange("properties", oldValue, value); //$NON-NLS-1$
}
public String removeProperty(String key)
{
String oldValue = properties.remove(key);
propertyChangeSupport.firePropertyChange("properties", oldValue, null); //$NON-NLS-1$
return oldValue;
}
public String getProperty(String key)
{
return properties.get(key);
}
/* package */void clearProperties()
{
properties.clear();
}
/* package */
SecretKey getSecret()
{
return secret;
}
/* package */
void setSecret(SecretKey secret)
{
this.secret = secret;
}
/**
* Removes the given account as reference account from any portfolios. As
* the model expects that there is always a reference account, an arbitrary
* other account is picked as reference account instead. Or, if no other
* account exists, a new account is created and used as reference account.
*/
private void deleteReferenceAccount(Account account)
{
for (Portfolio portfolio : portfolios)
{
if (account.equals(portfolio.getReferenceAccount()))
{
portfolio.setReferenceAccount(null);
accounts.stream().filter(a -> !account.equals(a)).findAny().ifPresent(portfolio::setReferenceAccount);
if (portfolio.getReferenceAccount() == null)
{
Account referenceAccount = new Account();
referenceAccount.setName(MessageFormat.format(Messages.LabelDefaultReferenceAccountName,
portfolio.getName()));
addAccount(referenceAccount);
portfolio.setReferenceAccount(referenceAccount);
}
}
}
}
/**
* Delete all transactions including cross entries and transactions created
* by an investment plan.
*/
private <T extends Transaction> void deleteTransactions(TransactionOwner<T> owner)
{
// use a copy because #removeTransaction modifies the list
for (T t : new ArrayList<T>(owner.getTransactions()))
owner.deleteTransaction(t, this);
}
private void deleteInvestmentPlans(Portfolio portfolio)
{
for (InvestmentPlan plan : new ArrayList<InvestmentPlan>(plans))
{
if (portfolio.equals(plan.getPortfolio()))
removePlan(plan);
}
}
private void deleteInvestmentPlans(Account account)
{
for (InvestmentPlan plan : new ArrayList<InvestmentPlan>(plans))
{
if (account.equals(plan.getAccount()))
removePlan(plan);
}
}
private void deleteInvestmentPlans(Security security)
{
for (InvestmentPlan plan : new ArrayList<InvestmentPlan>(plans))
{
if (security.equals(plan.getSecurity()))
removePlan(plan);
}
}
private void deleteTaxonomyAssignments(final InvestmentVehicle vehicle)
{
for (Taxonomy taxonomy : taxonomies)
{
taxonomy.foreach(new Taxonomy.Visitor()
{
@Override
public void visit(Classification classification, Assignment assignment)
{
if (vehicle.equals(assignment.getInvestmentVehicle()))
classification.removeAssignment(assignment);
}
});
}
}
private void deleteAccountTransactions(Security security)
{
for (Account account : accounts)
{
for (AccountTransaction t : new ArrayList<AccountTransaction>(account.getTransactions()))
{
if (t.getSecurity() == null || !security.equals(t.getSecurity()))
continue;
account.deleteTransaction(t, this);
}
}
}
private void deletePortfolioTransactions(Security security)
{
for (Portfolio portfolio : portfolios)
{
for (PortfolioTransaction t : new ArrayList<PortfolioTransaction>(portfolio.getTransactions()))
{
if (!security.equals(t.getSecurity()))
continue;
portfolio.deleteTransaction(t, this);
}
}
}
public void markDirty()
{
propertyChangeSupport.firePropertyChange("dirty", false, true); //$NON-NLS-1$
}
public void addPropertyChangeListener(PropertyChangeListener listener)
{
propertyChangeSupport.addPropertyChangeListener(listener);
}
public void addPropertyChangeListener(String propertyName, PropertyChangeListener listener)
{
propertyChangeSupport.addPropertyChangeListener(propertyName, listener);
}
public void removePropertyChangeListener(PropertyChangeListener listener)
{
propertyChangeSupport.removePropertyChangeListener(listener);
}
public void removePropertyChangeListener(String propertyName, PropertyChangeListener listener)
{
propertyChangeSupport.removePropertyChangeListener(propertyName, listener);
}
}