package name.abuchen.portfolio.model;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.lang.reflect.Field;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.AlgorithmParameters;
import java.security.GeneralSecurityException;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import org.eclipse.core.runtime.IProgressMonitor;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.XStreamException;
import com.thoughtworks.xstream.converters.reflection.ReflectionConverter;
import com.thoughtworks.xstream.converters.reflection.ReflectionProvider;
import com.thoughtworks.xstream.mapper.Mapper;
import name.abuchen.portfolio.Messages;
import name.abuchen.portfolio.model.Classification.Assignment;
import name.abuchen.portfolio.model.PortfolioTransaction.Type;
import name.abuchen.portfolio.money.CurrencyUnit;
import name.abuchen.portfolio.money.Money;
import name.abuchen.portfolio.money.Values;
import name.abuchen.portfolio.online.impl.YahooFinanceQuoteFeed;
import name.abuchen.portfolio.util.ProgressMonitorInputStream;
import name.abuchen.portfolio.util.XStreamLocalDateConverter;
@SuppressWarnings("deprecation")
public class ClientFactory
{
private static class PortfolioTransactionConverter extends ReflectionConverter
{
public PortfolioTransactionConverter(Mapper mapper, ReflectionProvider reflectionProvider)
{
super(mapper, reflectionProvider);
}
@Override
public boolean canConvert(@SuppressWarnings("rawtypes") Class type)
{
return type == PortfolioTransaction.class;
}
@Override
protected boolean shouldUnmarshalField(Field field)
{
if ("fees".equals(field.getName()) || "taxes".equals(field.getName())) //$NON-NLS-1$ //$NON-NLS-2$
return true;
return super.shouldUnmarshalField(field);
}
}
private static class XmlSerialization
{
public Client load(Reader input) throws IOException
{
try
{
Client client = (Client) xstream().fromXML(input);
if (client.getVersion() > Client.CURRENT_VERSION)
throw new IOException(MessageFormat.format(Messages.MsgUnsupportedVersionClientFiled,
client.getVersion()));
upgradeModel(client);
return client;
}
catch (XStreamException e)
{
throw new IOException(MessageFormat.format(Messages.MsgXMLFormatInvalid, e.getMessage()), e);
}
}
void save(Client client, OutputStream output) throws IOException
{
Writer writer = new OutputStreamWriter(output, StandardCharsets.UTF_8);
xstream().toXML(client, writer);
writer.flush();
}
}
private interface ClientPersister
{
Client load(InputStream input) throws IOException;
void save(Client client, OutputStream output) throws IOException;
}
private static class PlainWriter implements ClientPersister
{
@Override
public Client load(InputStream input) throws IOException
{
return new XmlSerialization().load(new InputStreamReader(input, StandardCharsets.UTF_8));
}
@Override
public void save(Client client, OutputStream output) throws IOException
{
try (Writer writer = new OutputStreamWriter(output, StandardCharsets.UTF_8))
{
xstream().toXML(client, writer);
writer.flush();
}
}
}
private static class Decryptor implements ClientPersister
{
private static final byte[] SIGNATURE = new byte[] { 'P', 'O', 'R', 'T', 'F', 'O', 'L', 'I', 'O' };
private static final byte[] SALT = new byte[] { 112, 67, 103, 107, -92, -125, -112, -95, //
-97, -114, 117, -56, -53, -69, -25, -28 };
private static final String AES = "AES"; //$NON-NLS-1$
private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding"; //$NON-NLS-1$
private static final String SECRET_KEY_ALGORITHM = "PBKDF2WithHmacSHA1"; //$NON-NLS-1$
private static final int ITERATION_COUNT = 65536;
private static final int IV_LENGTH = 16;
private static final int AES128_KEYLENGTH = 128;
private static final int AES256_KEYLENGTH = 256;
private char[] password;
private int keyLength;
public Decryptor(String method, char[] password)
{
this.password = password;
this.keyLength = "AES256".equals(method) ? AES256_KEYLENGTH : AES128_KEYLENGTH; //$NON-NLS-1$
}
@Override
public Client load(final InputStream input) throws IOException
{
InputStream decrypted = null;
try
{
// check signature
byte[] signature = new byte[SIGNATURE.length];
input.read(signature);
if (!Arrays.equals(signature, SIGNATURE))
throw new IOException(Messages.MsgNotAPortflioFile);
// read encryption method
int method = input.read();
this.keyLength = method == 1 ? AES256_KEYLENGTH : AES128_KEYLENGTH;
// check if key length is supported
if (!isKeyLengthSupported(this.keyLength))
throw new IOException(Messages.MsgKeyLengthNotSupported);
// build secret key
SecretKey secret = buildSecretKey();
// read initialization vector
byte[] iv = new byte[IV_LENGTH];
input.read(iv);
// build cipher and stream
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(iv));
decrypted = new CipherInputStream(input, cipher);
// read version information
byte[] bytes = new byte[4];
decrypted.read(bytes); // major version number
int majorVersion = ByteBuffer.wrap(bytes).getInt();
decrypted.read(bytes); // version number
int version = ByteBuffer.wrap(bytes).getInt();
// sanity check if the file was properly decrypted
if (majorVersion < 1 || majorVersion > 10 || version < 1 || version > 100)
throw new IOException(Messages.MsgIncorrectPassword);
if (majorVersion > Client.MAJOR_VERSION || version > Client.CURRENT_VERSION)
throw new IOException(MessageFormat.format(Messages.MsgUnsupportedVersionClientFiled, version));
// wrap with zip input stream
ZipInputStream zipin = new ZipInputStream(decrypted);
zipin.getNextEntry();
Client client = new XmlSerialization().load(new InputStreamReader(zipin, StandardCharsets.UTF_8));
// save secret key for next save
client.setSecret(secret);
return client;
}
catch (GeneralSecurityException e)
{
throw new IOException(MessageFormat.format(Messages.MsgErrorDecrypting, e.getMessage()), e);
}
finally
{
try
{
if (decrypted != null)
decrypted.close();
}
catch (IOException ignore)
{
// starting with a later jdk 1.8.0 (for example 1.8.0_25), a
// javax.crypto.BadPaddingException
// "Given final block not properly padded" is thrown if the
// we do not read the complete stream
}
}
}
@Override
public void save(Client client, final OutputStream output) throws IOException
{
try
{
// check if key length is supported
if (!isKeyLengthSupported(this.keyLength))
throw new IOException(Messages.MsgKeyLengthNotSupported);
// get or build secret key
// if password is given, it is used (when the user chooses
// "save as" from the menu)
SecretKey secret = password != null ? buildSecretKey() : client.getSecret();
if (secret == null)
throw new IOException(Messages.MsgPasswordMissing);
// save secret key for next save
client.setSecret(secret);
// write signature
output.write(SIGNATURE);
// write method
output.write(secret.getEncoded().length * 8 == AES256_KEYLENGTH ? 1 : 0);
// build cipher and stream
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, secret);
// write initialization vector
AlgorithmParameters params = cipher.getParameters();
byte[] iv = params.getParameterSpec(IvParameterSpec.class).getIV();
output.write(iv);
// encrypted stream
OutputStream encrpyted = new CipherOutputStream(output, cipher);
// write version information
encrpyted.write(ByteBuffer.allocate(4).putInt(Client.MAJOR_VERSION).array());
encrpyted.write(ByteBuffer.allocate(4).putInt(client.getVersion()).array());
// wrap with zip output stream
ZipOutputStream zipout = new ZipOutputStream(encrpyted);
zipout.putNextEntry(new ZipEntry("data.xml")); //$NON-NLS-1$
new XmlSerialization().save(client, zipout);
zipout.closeEntry();
zipout.flush();
zipout.finish();
output.flush();
}
catch (GeneralSecurityException e)
{
throw new IOException(MessageFormat.format(Messages.MsgErrorEncrypting, e.getMessage()), e);
}
}
private SecretKey buildSecretKey() throws NoSuchAlgorithmException, InvalidKeySpecException
{
SecretKeyFactory factory = SecretKeyFactory.getInstance(SECRET_KEY_ALGORITHM);
KeySpec spec = new PBEKeySpec(password, SALT, ITERATION_COUNT, keyLength);
SecretKey tmp = factory.generateSecret(spec);
return new SecretKeySpec(tmp.getEncoded(), AES);
}
}
private static XStream xstream;
public static boolean isEncrypted(File file)
{
return file.getName().endsWith(".portfolio"); //$NON-NLS-1$
}
public static boolean isKeyLengthSupported(int keyLength)
{
try
{
return keyLength <= Cipher.getMaxAllowedKeyLength(Decryptor.CIPHER_ALGORITHM);
}
catch (NoSuchAlgorithmException e)
{
throw new IllegalArgumentException(MessageFormat.format(Messages.MsgErrorEncrypting, e.getMessage()), e);
}
}
public static Client load(File file, char[] password, IProgressMonitor monitor) throws IOException
{
if (isEncrypted(file) && password == null)
throw new IOException(Messages.MsgPasswordMissing);
InputStream input = null;
try
{
// progress monitor
long bytesTotal = file.length();
int increment = (int) Math.min(bytesTotal / 20L, Integer.MAX_VALUE);
monitor.beginTask(MessageFormat.format(Messages.MsgReadingFile, file.getName()), 20);
input = new ProgressMonitorInputStream(new FileInputStream(file), increment, monitor);
return buildPersister(file, null, password).load(input);
}
catch (FileNotFoundException e)
{
FileNotFoundException fnf = new FileNotFoundException(
MessageFormat.format(Messages.MsgFileNotFound, file.getAbsolutePath()));
fnf.initCause(e);
throw fnf;
}
finally
{
if (input != null)
input.close();
}
}
public static Client load(Reader input) throws IOException
{
try
{
return new XmlSerialization().load(input);
}
finally
{
if (input != null)
input.close();
}
}
public static Client load(InputStream input) throws IOException
{
return load(new InputStreamReader(input, StandardCharsets.UTF_8));
}
public static void save(final Client client, final File file, String method, char[] password) throws IOException
{
if (isEncrypted(file) && password == null && client.getSecret() == null)
throw new IOException(Messages.MsgPasswordMissing);
try (OutputStream output = new FileOutputStream(file))
{
buildPersister(file, method, password).save(client, output);
}
}
private static ClientPersister buildPersister(File file, String method, char[] password)
{
if (file != null && isEncrypted(file))
return new Decryptor(method, password);
else
return new PlainWriter();
}
private static void upgradeModel(Client client)
{
client.doPostLoadInitialization();
client.setFileVersionAfterRead(client.getVersion());
switch (client.getVersion())
{
case 1:
fixAssetClassTypes(client);
addFeedAndExchange(client);
case 2:
addDecimalPlaces(client);
case 3:
// do nothing --> added industry classification
case 4:
for (Security s : client.getSecurities())
s.generateUUID();
case 5:
// do nothing --> save industry taxonomy in client
case 6:
// do nothing --> added WKN attribute to security
case 7:
// new portfolio transaction types:
// DELIVERY_INBOUND, DELIVERY_OUTBOUND
changePortfolioTransactionTypeToDelivery(client);
case 8:
// do nothing --> added 'retired' property to securities
case 9:
// do nothing --> added 'cross entries' to transactions
case 10:
generateUUIDs(client);
case 11:
// do nothing --> added 'properties' to client
case 12:
// added investment plans
// added security on chart as benchmark *and* performance
fixStoredBenchmarkChartConfigurations(client);
case 13:
// introduce arbitrary taxonomies
addAssetClassesAsTaxonomy(client);
addIndustryClassificationAsTaxonomy(client);
addAssetAllocationAsTaxonomy(client);
fixStoredClassificationChartConfiguration(client);
setDeprecatedFieldsToNull(client);
case 14:
// added shares to track dividends per share
assignSharesToDividendTransactions(client);
case 15:
// do nothing --> added 'isRetired' property to account
case 16:
// do nothing --> added 'feedURL' property to account
case 17:
// do nothing --> added notes attribute
case 18:
// do nothing --> added events (stock split) to securities
case 19:
// do nothing --> added attribute types
case 20:
// do nothing --> added note to investment plan
case 21:
// do nothing --> added taxes to portfolio transaction
case 22:
// do nothing --> added 'isRetired' property to portfolio
case 23:
// do nothing --> added 'latestFeed' and 'latestFeedURL'
// property to security
case 24:
// do nothing --> added 'TAX_REFUND' as account transaction
case 25:
// incremented precision of shares to 6 digits after the decimal
// sign
incrementSharesPrecisionFromFiveToSixDigitsAfterDecimalSign(client);
case 26:
// do nothing --> added client settings
case 27:
// client settings include attribute types
fixStoredChartConfigurationToSupportMultipleViews(client);
case 28:
// added currency support --> designate a default currency (user
// will get a dialog to change)
setAllCurrencies(client, CurrencyUnit.EUR);
bumpUpCPIMonthValue(client);
convertFeesAndTaxesToTransactionUnits(client);
case 29:
// added decimal places to stock quotes
addDecimalPlacesToQuotes(client);
case 30:
// added dashboards to model
fixStoredChartConfigurationWithNewPerformanceSeriesKeys(client);
migrateToConfigurationSets(client);
case 31:
// added INTEREST_CHARGE transaction type
case 32:
// added AED currency
case 33:
// added FEES_REFUND transaction type
client.setVersion(Client.CURRENT_VERSION);
break;
case Client.CURRENT_VERSION:
break;
default:
break;
}
}
private static void fixAssetClassTypes(Client client)
{
for (Security security : client.getSecurities())
{
if ("STOCK".equals(security.getType())) //$NON-NLS-1$
security.setType("EQUITY"); //$NON-NLS-1$
else if ("BOND".equals(security.getType())) //$NON-NLS-1$
security.setType("DEBT"); //$NON-NLS-1$
}
}
private static void addFeedAndExchange(Client client)
{
for (Security s : client.getSecurities())
s.setFeed(YahooFinanceQuoteFeed.ID);
}
private static void addDecimalPlaces(Client client)
{
for (Portfolio p : client.getPortfolios())
for (PortfolioTransaction t : p.getTransactions())
t.setShares(t.getShares() * 100000);
}
private static void changePortfolioTransactionTypeToDelivery(Client client)
{
for (Portfolio p : client.getPortfolios())
{
for (PortfolioTransaction t : p.getTransactions())
{
if (t.getType() == Type.TRANSFER_IN)
t.setType(Type.DELIVERY_INBOUND);
else if (t.getType() == Type.TRANSFER_OUT)
t.setType(Type.DELIVERY_OUTBOUND);
}
}
}
private static void generateUUIDs(Client client)
{
for (Account a : client.getAccounts())
a.generateUUID();
for (Portfolio p : client.getPortfolios())
p.generateUUID();
for (Category c : client.getRootCategory().flatten())
c.generateUUID();
}
@SuppressWarnings("nls")
private static void fixStoredBenchmarkChartConfigurations(Client client)
{
// Until now, the performance chart was showing *only* the benchmark
// series, not the actual performance series. Change keys as benchmark
// values are prefixed with '[b]'
replace(client, "PerformanceChartView-PICKER", //
"Security", "[b]Security", //
"ConsumerPriceIndex", "[b]ConsumerPriceIndex");
}
private static void addAssetClassesAsTaxonomy(Client client)
{
TaxonomyTemplate template = TaxonomyTemplate.byId("assetclasses"); //$NON-NLS-1$
Taxonomy taxonomy = template.buildFromTemplate();
taxonomy.setId("assetclasses"); //$NON-NLS-1$
int rank = 1;
Classification cash = taxonomy.getClassificationById("CASH"); //$NON-NLS-1$
for (Account account : client.getAccounts())
{
Assignment assignment = new Assignment(account);
assignment.setRank(rank++);
cash.addAssignment(assignment);
}
for (Security security : client.getSecurities())
{
Classification classification = taxonomy.getClassificationById(security.getType());
if (classification != null)
{
Assignment assignment = new Assignment(security);
assignment.setRank(rank++);
classification.addAssignment(assignment);
}
}
client.addTaxonomy(taxonomy);
}
private static void addIndustryClassificationAsTaxonomy(Client client)
{
String oldIndustryId = client.getIndustryTaxonomy();
Taxonomy taxonomy = null;
if ("simple2level".equals(oldIndustryId)) //$NON-NLS-1$
taxonomy = TaxonomyTemplate.byId(TaxonomyTemplate.INDUSTRY_SIMPLE2LEVEL).buildFromTemplate();
else
taxonomy = TaxonomyTemplate.byId(TaxonomyTemplate.INDUSTRY_GICS).buildFromTemplate();
taxonomy.setId("industries"); //$NON-NLS-1$
// add industry taxonomy only if at least one security has been assigned
if (assignSecurities(client, taxonomy))
client.addTaxonomy(taxonomy);
}
private static boolean assignSecurities(Client client, Taxonomy taxonomy)
{
boolean hasAssignments = false;
int rank = 0;
for (Security security : client.getSecurities())
{
Classification classification = taxonomy.getClassificationById(security.getIndustryClassification());
if (classification != null)
{
Assignment assignment = new Assignment(security);
assignment.setRank(rank++);
classification.addAssignment(assignment);
hasAssignments = true;
}
}
return hasAssignments;
}
private static void addAssetAllocationAsTaxonomy(Client client)
{
Category category = client.getRootCategory();
Taxonomy taxonomy = new Taxonomy("assetallocation", Messages.LabelAssetAllocation); //$NON-NLS-1$
Classification root = new Classification(category.getUUID(), Messages.LabelAssetAllocation);
taxonomy.setRootNode(root);
buildTree(root, category);
root.assignRandomColors();
client.addTaxonomy(taxonomy);
}
private static void buildTree(Classification node, Category category)
{
int rank = 0;
for (Category child : category.getChildren())
{
Classification classification = new Classification(node, child.getUUID(), child.getName());
classification.setWeight(child.getPercentage() * Values.Weight.factor());
classification.setRank(rank++);
node.addChild(classification);
buildTree(classification, child);
}
for (Object element : category.getElements())
{
Assignment assignment = element instanceof Account ? new Assignment((Account) element)
: new Assignment((Security) element);
assignment.setRank(rank++);
node.addAssignment(assignment);
}
}
@SuppressWarnings("nls")
private static void fixStoredClassificationChartConfiguration(Client client)
{
String name = Classification.class.getSimpleName();
replace(client, "PerformanceChartView-PICKER", //
"AssetClass", name, //
"Category", name);
replace(client, "StatementOfAssetsHistoryView-PICKER", //
"AssetClass", name, //
"Category", name);
}
private static void replace(Client client, String property, String... replacements)
{
if (replacements.length % 2 != 0)
throw new UnsupportedOperationException();
String value = client.getProperty(property);
if (value != null)
replaceAll(client, property, value, replacements);
int index = 0;
while (true)
{
String key = property + '$' + index;
value = client.getProperty(key);
if (value != null)
replaceAll(client, key, value, replacements);
else
break;
index++;
}
}
private static void replaceAll(Client client, String key, String value, String[] replacements)
{
String newValue = value;
for (int ii = 0; ii < replacements.length; ii += 2)
newValue = newValue.replaceAll(replacements[ii], replacements[ii + 1]);
client.setProperty(key, newValue);
}
private static void setDeprecatedFieldsToNull(Client client)
{
client.setRootCategory(null);
client.setIndustryTaxonomy(null);
for (Security security : client.getSecurities())
{
security.setIndustryClassification(null);
security.setType(null);
}
}
private static void assignSharesToDividendTransactions(Client client)
{
for (Security security : client.getSecurities())
{
List<TransactionPair<?>> transactions = security.getTransactions(client);
// sort by date of transaction
Collections.sort(transactions,
(one, two) -> one.getTransaction().getDate().compareTo(two.getTransaction().getDate()));
// count and assign number of shares by account
Map<Account, Long> account2shares = new HashMap<>();
for (TransactionPair<? extends Transaction> t : transactions)
{
if (t.getTransaction() instanceof AccountTransaction)
{
AccountTransaction accountTransaction = (AccountTransaction) t.getTransaction();
switch (accountTransaction.getType())
{
case DIVIDENDS:
case INTEREST:
Long shares = account2shares.get(t.getOwner());
accountTransaction.setShares(shares != null ? shares : 0);
break;
default:
}
}
else if (t.getTransaction() instanceof PortfolioTransaction)
{
PortfolioTransaction portfolioTransaction = (PortfolioTransaction) t.getTransaction();
// determine account: if it exists, take the cross entry.
// otherwise the reference account
Account account = null;
switch (portfolioTransaction.getType())
{
case BUY:
case SELL:
if (portfolioTransaction.getCrossEntry() != null)
account = (Account) portfolioTransaction.getCrossEntry()
.getCrossOwner(portfolioTransaction);
case TRANSFER_IN:
case TRANSFER_OUT:
default:
if (account == null)
account = ((Portfolio) t.getOwner()).getReferenceAccount();
}
long delta = 0;
switch (portfolioTransaction.getType())
{
case BUY:
case TRANSFER_IN:
delta = portfolioTransaction.getShares();
break;
case SELL:
case TRANSFER_OUT:
delta = -portfolioTransaction.getShares();
break;
default:
break;
}
Long shares = account2shares.get(account);
account2shares.put(account, shares != null ? shares + delta : delta);
}
}
}
}
private static void incrementSharesPrecisionFromFiveToSixDigitsAfterDecimalSign(Client client)
{
for (Portfolio portfolio : client.getPortfolios())
for (PortfolioTransaction portfolioTransaction : portfolio.getTransactions())
portfolioTransaction.setShares(portfolioTransaction.getShares() * 10);
for (Account account : client.getAccounts())
for (AccountTransaction accountTransaction : account.getTransactions())
accountTransaction.setShares(accountTransaction.getShares() * 10);
}
private static void fixStoredChartConfigurationToSupportMultipleViews(Client client)
{
@SuppressWarnings("nls")
String[] charts = new String[] { "name.abuchen.portfolio.ui.views.DividendsPerformanceView",
"name.abuchen.portfolio.ui.views.StatementOfAssetsViewer",
"name.abuchen.portfolio.ui.views.SecuritiesTable", //
"PerformanceChartView-PICKER", //
"StatementOfAssetsHistoryView-PICKER", //
"ReturnsVolatilityChartView-PICKER" };
for (String chart : charts)
{
String config = client.removeProperty(chart);
if (config == null) // if other values exist, they are in order
continue;
List<String> values = new ArrayList<>();
values.add("Standard:=" + config); //$NON-NLS-1$
int index = 0;
config = client.getProperty(chart + '$' + index);
while (config != null)
{
values.add(config);
index++;
config = client.getProperty(chart + '$' + index);
}
index = 0;
for (String va : values)
client.setProperty(chart + '$' + index++, va);
}
}
/**
* Previously, January had the index 0 (in line with java.util.Date). Bump
* it up by one since we are using new Java 8 Time API.
*/
private static void bumpUpCPIMonthValue(Client client)
{
for (ConsumerPriceIndex i : client.getConsumerPriceIndices())
i.setMonth(i.getMonth() + 1);
}
/**
* Sets all currency codes of accounts, securities, and transactions to the
* given currency code.
*/
public static void setAllCurrencies(Client client, String currencyCode)
{
client.setBaseCurrency(currencyCode);
client.getAccounts().stream().forEach(a -> a.setCurrencyCode(currencyCode));
client.getSecurities().stream().forEach(s -> s.setCurrencyCode(currencyCode));
client.getAccounts().stream().flatMap(a -> a.getTransactions().stream())
.forEach(t -> t.setCurrencyCode(currencyCode));
client.getPortfolios().stream().flatMap(p -> p.getTransactions().stream())
.forEach(t -> t.setCurrencyCode(currencyCode));
}
private static void convertFeesAndTaxesToTransactionUnits(Client client)
{
for (Portfolio p : client.getPortfolios())
{
for (PortfolioTransaction t : p.getTransactions())
{
long fees = t.fees;
if (fees != 0)
t.addUnit(new Transaction.Unit(Transaction.Unit.Type.FEE, Money.of(t.getCurrencyCode(), fees)));
t.fees = 0;
long taxes = t.taxes;
if (taxes != 0)
t.addUnit(new Transaction.Unit(Transaction.Unit.Type.TAX, Money.of(t.getCurrencyCode(), taxes)));
t.taxes = 0;
}
}
}
private static void addDecimalPlacesToQuotes(Client client)
{
// previously quotes worked in cents (2 decimal places). This change
// adds 2 decimal places to support up to 4.
int decimalPlacesAdded = 100;
for (Security security : client.getSecurities())
{
security.getPrices().stream().forEach(p -> p.setValue(p.getValue() * decimalPlacesAdded));
if (security.getLatest() != null)
{
LatestSecurityPrice l = security.getLatest();
l.setValue(l.getValue() * decimalPlacesAdded);
if (l.getHigh() != -1)
l.setHigh(l.getHigh() * decimalPlacesAdded);
if (l.getLow() != -1)
l.setLow(l.getLow() * decimalPlacesAdded);
if (l.getPreviousClose() != -1)
l.setPreviousClose(l.getPreviousClose() * decimalPlacesAdded);
}
}
List<AttributeType> typesWithQuotes = client.getSettings().getAttributeTypes()
.filter(t -> t.getConverter() instanceof AttributeType.QuoteConverter)
.collect(Collectors.toList());
client.getSecurities().stream().map(s -> s.getAttributes()).forEach(attributes -> {
for (AttributeType t : typesWithQuotes)
{
Object value = attributes.get(t);
if (value != null && value instanceof Long)
attributes.put(t, ((Long) value).longValue() * decimalPlacesAdded);
}
});
}
@SuppressWarnings("nls")
private static void fixStoredChartConfigurationWithNewPerformanceSeriesKeys(Client client)
{
replace(client, "PerformanceChartView-PICKER", //
"Client-transferals;", "Client-delta_percentage;");
}
@SuppressWarnings("nls")
private static void migrateToConfigurationSets(Client client)
{
// charts
migrateToConfigurationSet(client, "PerformanceChartView-PICKER");
migrateToConfigurationSet(client, "StatementOfAssetsHistoryView-PICKER");
migrateToConfigurationSet(client, "ReturnsVolatilityChartView-PICKER");
// columns config
migrateToConfigurationSet(client, "name.abuchen.portfolio.ui.views.SecuritiesPerformanceView");
migrateToConfigurationSet(client, "name.abuchen.portfolio.ui.views.SecuritiesTable");
migrateToConfigurationSet(client, "name.abuchen.portfolio.ui.views.StatementOfAssetsViewer");
// up until version 30, the properties were only used for view
// configurations (which are migrated now into configuration sets).
// Clear all remaining properties.
client.clearProperties();
}
private static void migrateToConfigurationSet(Client client, String key)
{
ConfigurationSet configSet = null;
int index = 0;
while (true)
{
String config = client.removeProperty(key + '$' + index);
if (config == null)
break;
if (configSet == null)
configSet = client.getSettings().getConfigurationSet(key);
String[] split = config.split(":="); //$NON-NLS-1$
if (split.length == 2)
configSet.add(new ConfigurationSet.Configuration(split[0], split[1]));
index++;
}
}
@SuppressWarnings("nls")
private static XStream xstream()
{
if (xstream == null)
{
synchronized (ClientFactory.class)
{
if (xstream == null)
{
xstream = new XStream();
xstream.setClassLoader(ClientFactory.class.getClassLoader());
xstream.registerConverter(new XStreamLocalDateConverter());
xstream.registerConverter(new PortfolioTransactionConverter(xstream.getMapper(),
xstream.getReflectionProvider()));
xstream.useAttributeFor(Money.class, "amount");
xstream.useAttributeFor(Money.class, "currencyCode");
xstream.aliasAttribute(Money.class, "currencyCode", "currency");
xstream.alias("account", Account.class);
xstream.alias("client", Client.class);
xstream.alias("settings", ClientSettings.class);
xstream.alias("bookmark", Bookmark.class);
xstream.alias("portfolio", Portfolio.class);
xstream.alias("unit", Transaction.Unit.class);
xstream.useAttributeFor(Transaction.Unit.class, "type");
xstream.alias("account-transaction", AccountTransaction.class);
xstream.alias("portfolio-transaction", PortfolioTransaction.class);
xstream.alias("security", Security.class);
xstream.alias("latest", LatestSecurityPrice.class);
xstream.alias("category", Category.class);
xstream.alias("watchlist", Watchlist.class);
xstream.alias("investment-plan", InvestmentPlan.class);
xstream.alias("attribute-type", AttributeType.class);
xstream.alias("price", SecurityPrice.class);
xstream.useAttributeFor(SecurityPrice.class, "time");
xstream.aliasField("t", SecurityPrice.class, "time");
xstream.useAttributeFor(SecurityPrice.class, "value");
xstream.aliasField("v", SecurityPrice.class, "value");
xstream.alias("cpi", ConsumerPriceIndex.class);
xstream.useAttributeFor(ConsumerPriceIndex.class, "year");
xstream.aliasField("y", ConsumerPriceIndex.class, "year");
xstream.useAttributeFor(ConsumerPriceIndex.class, "month");
xstream.aliasField("m", ConsumerPriceIndex.class, "month");
xstream.useAttributeFor(ConsumerPriceIndex.class, "index");
xstream.aliasField("i", ConsumerPriceIndex.class, "index");
xstream.alias("buysell", BuySellEntry.class);
xstream.alias("account-transfer", AccountTransferEntry.class);
xstream.alias("portfolio-transfer", PortfolioTransferEntry.class);
xstream.alias("taxonomy", Taxonomy.class);
xstream.alias("classification", Classification.class);
xstream.alias("assignment", Assignment.class);
xstream.alias("dashboard", Dashboard.class);
xstream.useAttributeFor(Dashboard.class, "name");
xstream.alias("column", Dashboard.Column.class);
xstream.alias("widget", Dashboard.Widget.class);
xstream.useAttributeFor(Dashboard.Widget.class, "type");
xstream.alias("event", SecurityEvent.class);
xstream.alias("config-set", ConfigurationSet.class);
xstream.alias("config", ConfigurationSet.Configuration.class);
}
}
}
return xstream;
}
}