Ja, bestimmt. Ich kann zwar nicht mit Eclipse umgehen und Java kann ich auch nicht, aber man kann ja mal mit AI-Hilfe ein wenig rumprobieren.
Ausgangspunkt: name.abuchen.portfolio/src/name/abuchen/portfolio/online/impl/CSQuoteFeed.java ist wahrscheinlich für das alte Credit Suisse Zeug, was jetzt nicht mehr geht.
Das habe ich (bzw. gemini) so verändert, das ein allgemeiner konfigurierbarer CSV-Provider dabei herauskommen soll. Funktioniert auch grundsätzlich
aber ich finde die Stelle nicht wo ich die Felder zum Konfigurieren (Datumsspalte, Kursspalte, Datumsformat, Trennzeichen usw.) in der? dem? UI zum Leben erwecke. In dem (geänderten) Code für CSQuoteFeed.java habe ich gemogelt
indem ich die Standardwerte einfach an lbswiss.ch angepasst habe. Aber so ist das ja eigentlich nicht gedacht, da sollen eigentlich Eingabefelder zu sehen sein, wie
im JSON-Provider. An welcher Stelle muss man da noch drehen?
Ich vermute irgendwo in name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/wizards/security/ und irgendwo in name.abuchen.portfolio.tests/src/name/abuchen/portfolio/online/impl/ aber ich weiß überhaupt nicht was genau man da tun sollte.
Die veränderte CSQuoteFeed.java
package name.abuchen.portfolio.online.impl;
import java.io.IOException;
import java.text.MessageFormat;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import name.abuchen.portfolio.Messages;
import name.abuchen.portfolio.model.LatestSecurityPrice;
import name.abuchen.portfolio.model.Security;
import name.abuchen.portfolio.model.SecurityProperty;
import name.abuchen.portfolio.money.Values;
import name.abuchen.portfolio.online.QuoteFeed;
import name.abuchen.portfolio.online.QuoteFeedData;
import name.abuchen.portfolio.util.WebAccess;
/**
* Ein flexibler Feed zur Verarbeitung von CSV-Dateien, dessen Konfiguration
* über separate Security-Eigenschaften verwaltet wird.
*/
public class CSQuoteFeed implements QuoteFeed
{
public static final String ID = "CONFIGURABLE_CSV_FEED"; //$NON-NLS-1$
// Konfigurations-Konstanten, analog zum GenericJSONQuoteFeed
public static final String SEPARATOR_PROPERTY_NAME = "CSV-SEPARATOR"; //$NON-NLS-1$
public static final String DATE_COLUMN_PROPERTY_NAME = "CSV-DATE-COLUMN"; //$NON-NLS-1$
public static final String PRICE_COLUMN_PROPERTY_NAME = "CSV-PRICE-COLUMN"; //$NON-NLS-1$
public static final String DATE_FORMAT_PROPERTY_NAME = "CSV-DATE-FORMAT"; //$NON-NLS-1$
public static final String SKIP_LINES_PROPERTY_NAME = "CSV-SKIP-LINES"; //$NON-NLS-1$
// Standardwerte
private static final String DEFAULT_SEPARATOR = ";"; //$NON-NLS-1$
private static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd"; //$NON-NLS-1$
private static final int DEFAULT_DATE_COLUMN = 0;
private static final int DEFAULT_PRICE_COLUMN = 7;
private static final int DEFAULT_SKIP_LINES = 4;
@Override
public String getId()
{
return ID;
}
@Override
public String getName()
{
return "Konfigurierbarer CSV Feed"; //$NON-NLS-1$
}
@Override
public QuoteFeedData getHistoricalQuotes(Security security, boolean collectRawResponse)
{
// Da es keine Unterscheidung zwischen Latest/Historic wie beim
// JSON-Feed gibt,
// verwenden wir nur einen Satz von Konstanten.
return internalGetQuotes(security, security.getFeedURL(), collectRawResponse);
}
private QuoteFeedData internalGetQuotes(Security security, String feedURL, boolean collectRawResponse)
{
try
{
if (feedURL == null || feedURL.length() == 0)
throw new IOException(MessageFormat.format(Messages.MsgMissingFeedURL, security.getName()));
// 1. Konfiguration aus den Security-Eigenschaften abrufen
QuoteConfig config = getConfigFromSecurity(security);
// 2. CSV-Inhalt herunterladen (hier wird nur die FeedURL verwendet,
// keine Parameter)
String content = new WebAccess(feedURL).get();
QuoteFeedData result = new QuoteFeedData();
if (collectRawResponse)
{
result.addResponse(feedURL, content);
}
// 3. Inhalt zeilenweise verarbeiten
String[] lines = content.lines().toArray(String[]::new);
// Schleife beginnt nach den zu überspringenden Zeilen
for (int i = config.getSkipLines(); i < lines.length; i++)
{
String line = lines[i];
if (line.trim().isEmpty())
continue;
try
{
result.addPrice(getPrice(line, config));
}
catch (Exception e)
{
// Fehler beim Parsen einer Zeile protokollieren und
// fortfahren
System.err.println(MessageFormat.format("Fehler beim Parsen der Zeile für {0}: {1}. Fehler: {2}", //$NON-NLS-1$
security.getName(), line, e.getMessage()));
}
}
return result;
}
catch (Exception e)
{
return QuoteFeedData.withError(e);
}
}
/**
* Ruft die Konfigurationswerte aus den Security-Eigenschaften ab.
*/
private QuoteConfig getConfigFromSecurity(Security security)
{
// Trennzeichen
String separator = security.getPropertyValue(SecurityProperty.Type.FEED, SEPARATOR_PROPERTY_NAME)
.orElse(DEFAULT_SEPARATOR);
// Datumsspalte
int dateCol = security.getPropertyValue(SecurityProperty.Type.FEED, DATE_COLUMN_PROPERTY_NAME)
.map(Integer::parseInt).orElse(DEFAULT_DATE_COLUMN);
// Kursspalte
int priceCol = security.getPropertyValue(SecurityProperty.Type.FEED, PRICE_COLUMN_PROPERTY_NAME)
.map(Integer::parseInt).orElse(DEFAULT_PRICE_COLUMN);
// Datumsformat
String dateFormat = security.getPropertyValue(SecurityProperty.Type.FEED, DATE_FORMAT_PROPERTY_NAME)
.orElse(DEFAULT_DATE_FORMAT);
// Zu überspringende Zeilen
int skipLines = security.getPropertyValue(SecurityProperty.Type.FEED, SKIP_LINES_PROPERTY_NAME)
.map(Integer::parseInt).orElse(DEFAULT_SKIP_LINES);
// Hier könnte man auch DATE_TIMEZONE und DATE_LOCALE übernehmen, falls
// nötig
// (analog zum JSON-Feed)
return new QuoteConfig(separator, dateCol, priceCol, dateFormat, skipLines);
}
/**
* Parsed eine einzelne Datenzeile basierend auf der Konfiguration.
*/
private LatestSecurityPrice getPrice(String line, QuoteConfig config)
throws DateTimeParseException, NumberFormatException
{
// Entfernt führende 'sep=;' falls vorhanden (für das SFD-Beispiel
// relevant)
String cleanedLine = line.trim().replaceFirst("sep=;", ""); //$NON-NLS-1$//$NON-NLS-2$
// Split mit dem konfigurierten Separator
// Regex.quote schützt vor Sonderzeichen im Separator
String regexSeparator = java.util.regex.Pattern.quote(config.getSeparator());
String[] tokens = cleanedLine.split(regexSeparator);
if (tokens.length <= config.getDateColumn() || tokens.length <= config.getPriceColumn())
{
throw new IllegalArgumentException("Nicht genügend Spalten in der Zeile."); //$NON-NLS-1$
}
// Datum parsen
String dateString = tokens[config.getDateColumn()].trim();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(config.getDateFormat());
LocalDate localDate = LocalDate.parse(dateString, formatter);
// Preis parsen: Ersetzt Kommas durch Punkte (für deutsche/schweizer
// Zahlenformate)
String priceString = tokens[config.getPriceColumn()].trim().replace(',', '.');
double rawPrice = Double.parseDouble(priceString);
// Faktorisiert den Preis
long price = Values.Quote.factorize(rawPrice);
return new LatestSecurityPrice(localDate, price);
}
}
/**
* Hilfsklasse zur Speicherung der Konfigurationsparameter. Die baseURL
* entfällt, da sie die normale FeedURL ist.
*/
class QuoteConfig
{
private final String separator;
private final int dateColumn;
private final int priceColumn;
private final String dateFormat;
private final int skipLines;
public QuoteConfig(String separator, int dateColumn, int priceColumn, String dateFormat, int skipLines)
{
this.separator = separator;
this.dateColumn = dateColumn;
this.priceColumn = priceColumn;
this.dateFormat = dateFormat;
this.skipLines = skipLines;
}
public String getSeparator()
{
return separator;
}
public int getDateColumn()
{
return dateColumn;
}
public int getPriceColumn()
{
return priceColumn;
}
public int getSkipLines()
{
return skipLines;
}
public String getDateFormat()
{
return dateFormat;
}
}