Java tutorial
/* * To change this license header, choose License Headers in Project Properties. * To change this template file, choose Tools | Templates * and open the template in the editor. */ package com.ndemyanovskyi.backend; import com.ndemyanovskyi.app.Settings; import com.ndemyanovskyi.backend.loader.SimpleLoader; import com.ndemyanovskyi.backend.loader.SimpleSubscribedLoader; import com.ndemyanovskyi.backend.site.DocumentParseException; import com.ndemyanovskyi.backend.site.InterbankSite; import com.ndemyanovskyi.backend.site.Site; import com.ndemyanovskyi.derby.Cursor; import com.ndemyanovskyi.derby.Database; import com.ndemyanovskyi.derby.Derby; import com.ndemyanovskyi.derby.Row; import com.ndemyanovskyi.map.unmodifiable.UnmodifiableMapWrapper; import com.ndemyanovskyi.throwable.Exceptions; import com.ndemyanovskyi.throwable.RuntimeSQLException; import static com.ndemyanovskyi.util.Compare.greaterOrEquals; import static com.ndemyanovskyi.util.DateTimeFormatters.of; import java.io.IOException; import java.sql.SQLException; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import org.apache.commons.collections4.ListUtils; import org.apache.commons.collections4.SetUtils; import static org.apache.commons.collections4.map.AbstractReferenceMap.ReferenceStrength.HARD; import static org.apache.commons.collections4.map.AbstractReferenceMap.ReferenceStrength.SOFT; import org.apache.commons.collections4.map.ReferenceMap; /** * * @author ? */ public final class DataManager { //Rate list cache private static final Set<RateListImpl<?>> CACHE = Collections.newSetFromMap(new ReferenceMap<>(SOFT, HARD)); private static final Logger LOG = Logger.getLogger(DataManager.class.getName()); private static Database database; public static <R extends Rate> RateList<R> getCachedRateList(Bank<R> bank, Currency currency) { return getCachedRateListImpl(bank, currency); } static <R extends Rate> RateListImpl<R> getCachedRateListImpl(Bank<R> bank, Currency currency) { for (RateList<?> list : CACHE) { if (list.is(bank, currency)) { return (RateListImpl<R>) list; } } return null; } static <R extends Rate> RateListImpl<R> getRateListImpl(Bank<R> bank, Currency currency) { RateListImpl<R> list = getCachedRateListImpl(bank, currency); return list != null ? list : RateListLoader.of(bank, currency).sync(); } public static final Database getDatabase() { return database != null ? database : (database = Derby.connect("assets/db/currency_rates")); } public static <R extends Rate> Cursor getTable(Bank<R> bank, Currency currency) { return DataManager.getTable(bank, currency, Cursor.Type.FORWARD_ONLY, Cursor.Concurrency.READ_ONLY); } public static <R extends Rate> Cursor getTable(Bank<R> bank, Currency currency, Cursor.Type type, Cursor.Concurrency concurrency) { String table = getTableName(bank, currency); Cursor cursor = getDatabase().query("SELECT * FROM " + table, type, concurrency); try { cursor.init(); return cursor; } catch (RuntimeSQLException ex) { createTable(bank, currency); return getTable(bank, currency, type, concurrency); } } public static <R extends Rate> RateList<R> getRateList(Bank<R> bank, Currency currency) { return getRateListImpl(bank, currency); } @SuppressWarnings("unchecked") public static <R extends Rate> void writeRate(R rate, boolean updateIfExists) { Bank<R> bank = (Bank<R>) rate.getBank(); String table = getTableName(bank, rate.getCurrency()); try { getDatabase().queryUpdate(bank.getDatabaseHelper().getInsertSql(table, rate)); } catch (RuntimeSQLException ex) { SQLException sqlCause = Database.Utils.extractCause(ex); LOG.log(Level.SEVERE, "Rate(" + rate + ") does not writed: Message: " + ex); switch (sqlCause.getSQLState()) { case "23505": //Duplicate value if (updateIfExists) { getDatabase().queryUpdate(bank.getDatabaseHelper().getUpdateSql(table, rate)); } break; case "42X05": //Table does not exists createTable(bank, rate.getCurrency()); writeRate(rate, updateIfExists); break; } } } private static <R extends Rate> void createTable(Bank<R> bank, Currency currency) { try { getDatabase().queryUpdate(String.format("CREATE TABLE %s (%s)", getTableName(bank, currency), bank.getDatabaseHelper().getLayout(bank, currency))); } catch (RuntimeSQLException ex) { } } static <R extends Rate> LocalDate firstDate(Bank<R> bank, Currency currency) { RateListImpl<R> list = getCachedRateListImpl(bank, currency); if (list != null) { return !list.isEmpty() ? list.getPeriod().first() : null; } return null; /*String table = getTableName(bank, currency); return getDatabase().queryFirst( "SELECT MIN(DATE) FROM " + table).toLocalDate();*/ } static <R extends Rate> LocalDate lastDate(Bank<R> bank, Currency currency) { RateListImpl<R> list = getCachedRateListImpl(bank, currency); if (list != null) { return list.getPeriod().last(); } else { String table = getTableName(bank, currency); return getDatabase().queryFirst("SELECT MAX(DATE) FROM " + table).toLocalDate(); } } public static <R extends Rate> boolean containsDate(Bank<R> bank, Currency currency, LocalDate date) { RateListImpl<R> list = getCachedRateListImpl(bank, currency); if (list != null) { return list.getPeriod().contains(date); } else { String table = getTableName(bank, currency); return getDatabase() .queryFirst(String.format("SELECT DATE FROM %s WHERE DATE = DATE('%s')", table, date)) .nonNull(); } } /** * Loaded rate from any exist site. * @param <R> rate type * @param bank any bank, not null. * @param currency currency, supported by bank, not null. * @param date any date in bounds from minimum to today date, not null. * @return loaded rate, not null * @throws IOException for any io error. * @throws DocumentParseException if document from the last exist site can`t be parsed, or parsed elements count equals zero. */ public static <R extends Rate> R loadRate(Bank<R> bank, Currency currency, LocalDate date) throws IOException, DocumentParseException { if (bank.getSite() != null) { Set<InterbankSite> sites = Site.supported(InterbankSite.class, bank); BankSiteLoader<R> loader = BankSiteLoader.of(bank, date); try { R rate = loader.sync().get(currency); if (rate != null || sites.isEmpty()) return rate; } catch (IOException ex) { if (sites.isEmpty()) throw ex; } } InterbankSiteLoader<R> loader = InterbankSiteLoader.of(currency, date); R rate = loader.subscribe(bank); if (rate != null) return rate; String errorMessage = String.format( "Rate can`t be loaded on any exists sites (date = %s, bank = %s, currency = %s", date, bank, currency); LOG.log(Level.SEVERE, errorMessage); throw new IOException(errorMessage); } static String getTableName(Bank<?> bank, Currency currency) { return bank.getName() + "_" + currency.name(); } private static class BankSiteLoader<R extends Rate> extends SimpleLoader<Map<Currency, R>> { private static final Set<BankSiteLoader<?>> LOADERS = new HashSet<>(); private final Bank<R> bank; private final LocalDate date; public BankSiteLoader(Bank<R> bank, LocalDate date) { this.bank = Objects.requireNonNull(bank, "bank"); this.date = Objects.requireNonNull(date, "date"); } public static <R extends Rate> BankSiteLoader<R> of(Bank<R> bank, LocalDate date) { synchronized (LOADERS) { for (BankSiteLoader<?> loader : LOADERS) { if (loader.getBank().equals(bank) && loader.getDate().equals(date) && !loader.isFinished()) { return (BankSiteLoader<R>) loader; } } BankSiteLoader<R> loader = new BankSiteLoader<>(bank, date); LOADERS.add(loader); return loader; } } public LocalDate getDate() { return date; } public Bank<R> getBank() { return bank; } @Override protected void onFinish() { LOADERS.remove(this); } @Override public Map<Currency, R> load() throws IOException { Map<Currency, R> rates = bank.getSite().load(getDate()); for (Map.Entry<Currency, R> e : rates.entrySet()) { RateListImpl<R> list = getCachedRateListImpl(bank, e.getValue().getCurrency()); LocalDate firstDate = firstDate(bank, e.getValue().getCurrency()); if (firstDate == null) { firstDate = LocalDate.now().minusYears(Settings.STORED_DATA_YEARS_COUNT.get()); } R rate = e.getValue(); if (rate.isZero() || greaterOrEquals(rate.getDate(), firstDate)) { if (list != null) { R other = list.get(getDate()); if (other != null) { list.modifier().replaceOrThrow(e.getValue()); } else { list.modifier().addOrThrow(e.getValue()); } } writeRate(e.getValue(), false); } } return rates; } } private static class InterbankSiteLoader<R extends Rate> extends SimpleSubscribedLoader<Bank<?>, R> { private static final Set<InterbankSiteLoader<?>> LOADERS = SetUtils.synchronizedSet(new HashSet<>()); private final Currency currency; private final List<InterbankSite<Bank<?>, ?>> sites = ListUtils.synchronizedList(new ArrayList<>()); private final LocalDate date; private InterbankSiteLoader(Currency currency, LocalDate date) { this.currency = Objects.requireNonNull(currency, "currency"); this.date = Objects.requireNonNull(date, "date"); setResult(new UnmodifiableMapWrapper<>(new HashMap<>())); } public static <R extends Rate> InterbankSiteLoader<R> of(Currency currency, LocalDate date) { synchronized (LOADERS) { for (InterbankSiteLoader<?> loader : LOADERS) { if (loader.getCurrency().equals(currency) && loader.getDate().equals(date) && !loader.isFinished()) { return (InterbankSiteLoader<R>) loader; } } InterbankSiteLoader<R> loader = new InterbankSiteLoader<>(currency, date); LOADERS.add(loader); return loader; } } @Override protected void onFinish() { LOADERS.remove(this); } @Override public Map<Bank<?>, R> load() throws IOException { while (!sites.isEmpty()) { InterbankSite<Bank<?>, ?> site = sites.get(0); if (getSubscribedLocks().isEmpty()) { return getResult(); } try { Map<Bank<?>, ? extends Rate> rates = site.load(currency, getDate()); sites.remove(site); if (rates != null) { getResult().putAll((Map<Bank<?>, R>) rates); for (Map.Entry<Bank<?>, ? extends Rate> e : rates.entrySet()) { RateListImpl<R> list = getCachedRateListImpl((Bank<R>) e.getKey(), currency); if (list != null) { list.modifier().add((R) e.getValue()); } writeRate((R) e.getValue(), false); if (!e.getValue().isZero() || sites.isEmpty()) { getSubscribedLocks().removeIf(lock -> lock.is(e.getKey())); } } } } catch (IOException ex) { sites.remove(site); if (sites.isEmpty()) throw ex; } } return getResult(); } @Override protected void onSubscribe(Bank<?> subscriber) { if (!subscriber.getCurrencies().contains(getCurrency())) { throw new IllegalArgumentException(String.format( "Subscriber bank(%s) not supported loader currency(%s)", subscriber, getCurrency())); } for (InterbankSite<Bank<?>, ?> site : Site.supported(InterbankSite.class, subscriber)) { if (!sites.contains(site)) { sites.add(site); } } } public LocalDate getDate() { return date; } public Currency getCurrency() { return currency; } } private static class RateListLoader<R extends Rate> extends SimpleLoader<RateListImpl<R>> { private static final Set<RateListLoader<?>> LOADERS = SetUtils.synchronizedSet(new HashSet<>()); private final Bank<R> bank; private final Currency currency; public RateListLoader(Bank<R> bank, Currency currency) { this.bank = Objects.requireNonNull(bank, "bank"); this.currency = Objects.requireNonNull(currency, "currency"); } public Currency getCurrency() { return currency; } public Bank<R> getBank() { return bank; } public boolean is(Bank<?> bank, Currency currency) { return getBank().equals(bank) && getCurrency().equals(currency); } public static <R extends Rate> RateListLoader<R> of(Bank<R> bank, Currency currency) { synchronized (LOADERS) { for (RateListLoader<?> loader : LOADERS) { if (loader.is(bank, currency)) { return (RateListLoader<R>) loader; } } RateListLoader<R> loader = new RateListLoader<>(bank, currency); LOADERS.add(loader); return loader; } } @Override public RateListImpl<R> load() { RateListImpl<R> list = getCachedRateListImpl(bank, currency); if (list == null) { List<R> rates = new ArrayList<>(); for (Row row : getTable(bank, currency)) { R rate = (bank.getDatabaseHelper().getRate(bank, currency, row)); rates.add(rate); } list = new RateListImpl<>(bank, currency, rates); CACHE.add(list); } return list; } @Override protected void onFinish() { LOADERS.remove(this); } @Override public RateListImpl<R> sync() { return Exceptions.execute(super::sync); } } private static <R extends Rate> void createLastUpdatesTable() { try { getDatabase().queryUpdate("CREATE TABLE LAST_UPDATES " + "(TABLE VARCHAR PRIMARY KEY NOT NULL, LAST_UPDATE TIMESTAMP NOT NULL)"); } catch (RuntimeSQLException ex) { } } public static Cursor getLastUpdatesTable(Cursor.Type type, Cursor.Concurrency concurrency) { Cursor cursor = getDatabase().query("SELECT * FROM LAST_UPDATES", type, concurrency); try { cursor.init(); return cursor; } catch (RuntimeSQLException ex) { createLastUpdatesTable(); return getLastUpdatesTable(type, concurrency); } } public static Map<String, LocalDateTime> getLastUpdates() { Cursor c = getLastUpdatesTable(Cursor.Type.FORWARD_ONLY, Cursor.Concurrency.READ_ONLY); Map<String, LocalDateTime> map = new HashMap<>(); for (Row row : c) { map.put(row.get("TABLE").toString(), row.get("LAST_UPDATE").toLocalDateTime()); } c.close(); return map; } public static LocalDateTime getLastUpdate(Bank<?> bank, Currency currency) { return getLastUpdates().get(getTableName(bank, currency)); } public static void writeLastUpdate(Bank<?> bank, Currency currency, LocalDateTime dateTime) { String table = getTableName(bank, currency); String lastUpdate = dateTime.format(of("yyyy-MM-dd hh:mm:ss.nnnnnnnnn")); try { try { getDatabase().queryUpdate(String .format("INSERT INTO LAST_UPDATES (TABLE, LAST_UPDATE) VALUES(%s, %s)", table, lastUpdate)); } catch (RuntimeSQLException ignored) { } } catch (RuntimeSQLException ex) { try { getDatabase().queryUpdate(String.format( "UPDATE LAST_UPDATES SET LAST_UPDATE=TIMESTAMP(%s) WHERE TABLE='%s'", lastUpdate, table)); } catch (RuntimeSQLException ignored) { } } } }