/** * Copyright (C) 2013 - present by OpenGamma Inc. and the OpenGamma group of companies * * Please see distribution for license. */ package com.opengamma.web.analytics.blotter; import java.math.BigDecimal; import java.text.DecimalFormat; import java.text.ParseException; import java.util.Map; import java.util.Set; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.WordUtils; import org.joda.beans.BeanBuilder; import org.joda.beans.MetaBean; import org.joda.beans.MetaProperty; import org.joda.convert.StringConvert; import org.joda.convert.StringConverter; import org.threeten.bp.LocalDate; import org.threeten.bp.LocalTime; import org.threeten.bp.OffsetTime; import org.threeten.bp.ZoneOffset; import org.threeten.bp.ZonedDateTime; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.opengamma.analytics.financial.credit.DebtSeniority; import com.opengamma.core.id.ExternalSchemes; import com.opengamma.financial.convention.StubType; import com.opengamma.financial.conversion.JodaBeanConverters; import com.opengamma.financial.security.FinancialSecurity; import com.opengamma.financial.security.LongShort; import com.opengamma.financial.security.capfloor.CapFloorCMSSpreadSecurity; import com.opengamma.financial.security.capfloor.CapFloorSecurity; import com.opengamma.financial.security.cash.CashSecurity; import com.opengamma.financial.security.cds.CreditDefaultSwapIndexSecurity; import com.opengamma.financial.security.cds.CreditDefaultSwapSecurity; import com.opengamma.financial.security.cds.LegacyFixedRecoveryCDSSecurity; import com.opengamma.financial.security.cds.LegacyRecoveryLockCDSSecurity; import com.opengamma.financial.security.cds.LegacyVanillaCDSSecurity; import com.opengamma.financial.security.cds.StandardFixedRecoveryCDSSecurity; import com.opengamma.financial.security.cds.StandardRecoveryLockCDSSecurity; import com.opengamma.financial.security.cds.StandardVanillaCDSSecurity; import com.opengamma.financial.security.equity.EquityVarianceSwapSecurity; import com.opengamma.financial.security.fra.FRASecurity; import com.opengamma.financial.security.fx.FXForwardSecurity; import com.opengamma.financial.security.fx.NonDeliverableFXForwardSecurity; import com.opengamma.financial.security.option.BarrierDirection; import com.opengamma.financial.security.option.BarrierType; import com.opengamma.financial.security.option.CreditDefaultSwapOptionSecurity; import com.opengamma.financial.security.option.FXBarrierOptionSecurity; import com.opengamma.financial.security.option.FXDigitalOptionSecurity; import com.opengamma.financial.security.option.FXOptionSecurity; import com.opengamma.financial.security.option.MonitoringType; import com.opengamma.financial.security.option.NonDeliverableFXOptionSecurity; import com.opengamma.financial.security.option.OptionType; import com.opengamma.financial.security.option.SamplingFrequency; import com.opengamma.financial.security.option.SwaptionSecurity; import com.opengamma.financial.security.swap.FixedInflationSwapLeg; import com.opengamma.financial.security.swap.FixedInterestRateLeg; import com.opengamma.financial.security.swap.FloatingGearingIRLeg; import com.opengamma.financial.security.swap.FloatingInterestRateLeg; import com.opengamma.financial.security.swap.FloatingSpreadIRLeg; import com.opengamma.financial.security.swap.InflationIndexSwapLeg; import com.opengamma.financial.security.swap.InterestRateNotional; import com.opengamma.financial.security.swap.SwapLeg; import com.opengamma.financial.security.swap.SwapSecurity; import com.opengamma.financial.security.swap.YearOnYearInflationSwapSecurity; import com.opengamma.financial.security.swap.ZeroCouponInflationSwapSecurity; import com.opengamma.id.ExternalId; import com.opengamma.id.ExternalIdBundle; import com.opengamma.master.security.ManageableSecurity; import com.opengamma.util.ArgumentChecker; import com.opengamma.util.time.Expiry; /** * */ /* package */ class BlotterUtils { // TODO this should be configurable, should be able to add from client projects /** All the securities and related types supported by the blotter. */ private static final Set<MetaBean> s_metaBeans = Sets.<MetaBean>newHashSet( FXForwardSecurity.meta(), SwapSecurity.meta(), SwaptionSecurity.meta(), CapFloorCMSSpreadSecurity.meta(), NonDeliverableFXOptionSecurity.meta(), FXOptionSecurity.meta(), FRASecurity.meta(), CapFloorSecurity.meta(), EquityVarianceSwapSecurity.meta(), FXBarrierOptionSecurity.meta(), FXDigitalOptionSecurity.meta(), FixedInterestRateLeg.meta(), FloatingInterestRateLeg.meta(), FloatingSpreadIRLeg.meta(), FloatingGearingIRLeg.meta(), FixedInflationSwapLeg.meta(), InflationIndexSwapLeg.meta(), InterestRateNotional.meta(), LegacyVanillaCDSSecurity.meta(), LegacyRecoveryLockCDSSecurity.meta(), LegacyFixedRecoveryCDSSecurity.meta(), StandardVanillaCDSSecurity.meta(), StandardRecoveryLockCDSSecurity.meta(), StandardFixedRecoveryCDSSecurity.meta(), CreditDefaultSwapIndexSecurity.meta(), CreditDefaultSwapOptionSecurity.meta(), YearOnYearInflationSwapSecurity.meta(), ZeroCouponInflationSwapSecurity.meta(), CashSecurity.meta()); /** Meta bean factory for looking up meta beans by type name. */ private static final MetaBeanFactory s_metaBeanFactory = new MapMetaBeanFactory(s_metaBeans); /** Formatter for decimal numbers, DecimalFormat isn't thread safe. */ private static final ThreadLocal<DecimalFormat> s_decimalFormat = new ThreadLocal<DecimalFormat>() { @Override protected DecimalFormat initialValue() { DecimalFormat decimalFormat = new DecimalFormat("#,###.#####"); decimalFormat.setParseBigDecimal(true); return decimalFormat; } }; /** * For traversing trade and security {@link MetaBean}s and building instances from the data sent from the blotter. * The security type name is filtered out because it is a read-only property. The external ID bundle is filtered * out because it is always empty for trades and securities entered via the blotter but isn't nullable. Therefore * it has to be explicitly set to an empty bundle after the client data is processed but before the object is built. */ private static final BeanTraverser s_beanBuildingTraverser = new BeanTraverser( new PropertyFilter(FinancialSecurity.meta().externalIdBundle()), new PropertyFilter(ManageableSecurity.meta().securityType())); /** For converting between strings values used by the UI and real objects. */ private static final StringConvert s_stringConvert; /** For converting property values when creating trades and securities from JSON. */ private static final Converters s_beanBuildingConverters; /** For converting property values when creating JSON objects from trades and securities. */ private static final Converters s_jsonBuildingConverters; static { StringToRegionIdConverter stringToRegionIdConverter = new StringToRegionIdConverter(); // for building beans from JSON Map<MetaProperty<?>, Converter<?, ?>> beanRegionConverters = Maps.newHashMap(); beanRegionConverters.putAll( ImmutableMap.<MetaProperty<?>, Converter<?, ?>>of( CashSecurity.meta().regionId(), stringToRegionIdConverter, CreditDefaultSwapSecurity.meta().regionId(), stringToRegionIdConverter, EquityVarianceSwapSecurity.meta().regionId(), stringToRegionIdConverter, FRASecurity.meta().regionId(), stringToRegionIdConverter, SwapLeg.meta().regionId(), stringToRegionIdConverter)); beanRegionConverters.putAll( ImmutableMap.<MetaProperty<?>, Converter<?, ?>>of( FXForwardSecurity.meta().regionId(), new FXRegionConverter(), NonDeliverableFXForwardSecurity.meta().regionId(), new FXRegionConverter())); // for building JSON from beans RegionIdToStringConverter regionIdToStringConverter = new RegionIdToStringConverter(); Map<MetaProperty<?>, Converter<?, ?>> jsonRegionConverters = ImmutableMap.<MetaProperty<?>, Converter<?, ?>>of( CashSecurity.meta().regionId(), regionIdToStringConverter, CreditDefaultSwapSecurity.meta().regionId(), regionIdToStringConverter, EquityVarianceSwapSecurity.meta().regionId(), regionIdToStringConverter, FRASecurity.meta().regionId(), regionIdToStringConverter, SwapLeg.meta().regionId(), regionIdToStringConverter); s_stringConvert = new StringConvert(); s_stringConvert.register(BigDecimal.class, new BigDecimalConverter()); s_stringConvert.register(Double.class, new DoubleConverter()); s_stringConvert.register(Double.TYPE, new DoubleConverter()); s_stringConvert.register(ExternalIdBundle.class, new JodaBeanConverters.ExternalIdBundleConverter()); s_stringConvert.register(Expiry.class, new ExpiryConverter()); s_stringConvert.register(MonitoringType.class, new EnumConverter<MonitoringType>()); s_stringConvert.register(BarrierType.class, new EnumConverter<BarrierType>()); s_stringConvert.register(BarrierDirection.class, new EnumConverter<BarrierDirection>()); s_stringConvert.register(SamplingFrequency.class, new EnumConverter<SamplingFrequency>()); s_stringConvert.register(LongShort.class, new EnumConverter<LongShort>()); s_stringConvert.register(OptionType.class, new EnumConverter<OptionType>()); s_stringConvert.register(ZonedDateTime.class, new ZonedDateTimeConverter()); s_stringConvert.register(OffsetTime.class, new OffsetTimeConverter()); s_stringConvert.register(DebtSeniority.class, new EnumConverter<DebtSeniority>()); s_stringConvert.register(StubType.class, new EnumConverter<StubType>()); s_jsonBuildingConverters = new Converters(jsonRegionConverters, s_stringConvert); s_beanBuildingConverters = new Converters(beanRegionConverters, s_stringConvert); } /** * Filters out region ID for FX forwards when building JSON for the security and HTML screens showing the structure. * The property value is hard-coded to {@code FINANCIAL_REGION~GB} for FX forwards so its value is of no interest * to the client and it can't be updated. */ private static final PropertyFilter s_fxRegionFilter = new PropertyFilter(FXForwardSecurity.meta().regionId(), NonDeliverableFXForwardSecurity.meta().regionId()); /** * Filters out the {@code externalIdBundle} property from OTC securities when building the HTML showing the security * structure. OTC security details are passed to the blotter back end which generates the ID so this * info is irrelevant to the client. */ private static final BeanVisitorDecorator s_externalIdBundleFilter = new PropertyNameFilter("externalIdBundle"); /** * Filters out the underlying ID field of {@link SwaptionSecurity} when building the HTML showing the security * structure. The back end creates the underlying security and fills this field in so it's of no interest * to the client. */ private static final PropertyFilter s_swaptionUnderlyingFilter = new PropertyFilter(SwaptionSecurity.meta().underlyingId()); /** * Filters out the underlying ID field of {@link CreditDefaultSwapOptionSecurity} when building the HTML showing the security * structure. The back end creates the underlying security and fills this field in so it's of no interest * to the client. */ private static final PropertyFilter s_cdsOptionUnderlyingFilter = new PropertyFilter(CreditDefaultSwapOptionSecurity.meta().underlyingId()); /** * Filters out the {@code securityType} field for all securities when building the HTML showing the security * structure. This value is read-only in each security type and is of no interest to the client. */ private static final PropertyFilter s_securityTypeFilter = new PropertyFilter(ManageableSecurity.meta().securityType()); /** * @return A thread-local formatter instance set to parse numbers into BigDecimals. */ /* package */ static DecimalFormat getDecimalFormat() { return s_decimalFormat.get(); } /* package */ static FinancialSecurity buildSecurity(BeanDataSource data) { return buildSecurity(data, ExternalIdBundle.EMPTY); } @SuppressWarnings("unchecked") /* package */ static FinancialSecurity buildSecurity(BeanDataSource data, ExternalIdBundle idBundle) { BeanVisitor<BeanBuilder<FinancialSecurity>> visitor = new BeanBuildingVisitor<>(data, s_metaBeanFactory, s_beanBuildingConverters); MetaBean metaBean = s_metaBeanFactory.beanFor(data); // TODO check it's a FinancialSecurity metaBean if (!(metaBean instanceof FinancialSecurity.Meta)) { throw new IllegalArgumentException("MetaBean " + metaBean + " isn't for a FinancialSecurity"); } BeanBuilder<FinancialSecurity> builder = (BeanBuilder<FinancialSecurity>) s_beanBuildingTraverser.traverse(metaBean, visitor); // externalIdBundle needs to be specified or building fails because it's not nullable builder.set(FinancialSecurity.meta().externalIdBundle(), idBundle); return builder.build(); } // TODO move to BlotterUtils /* package */ static StringConvert getStringConvert() { return s_stringConvert; } /* package */ static Converters getJsonBuildingConverters() { return s_jsonBuildingConverters; } /* package */ static Converters getBeanBuildingConverters() { return s_beanBuildingConverters; } /* package */ static Set<MetaBean> getMetaBeans() { return s_metaBeans; } /* package */ static BeanTraverser structureBuildingTraverser() { return new BeanTraverser(s_externalIdBundleFilter, s_securityTypeFilter, s_swaptionUnderlyingFilter, s_cdsOptionUnderlyingFilter, s_fxRegionFilter); } /* package */ static BeanTraverser securityJsonBuildingTraverser() { return new BeanTraverser(s_securityTypeFilter, s_fxRegionFilter); } } // ---------------------------------------------------------------------------------- /** * For converting between enum instances and strings. The enum value names are made more readable by downcasing * and capitalizing them and replacing underscores with spaces. * @param <T> Type of the enum */ @SuppressWarnings({"rawtypes", "unchecked" }) /* package */ class EnumConverter<T extends Enum> implements StringConverter<T> { @Override public T convertFromString(Class<? extends T> type, String str) { // IntelliJ says this cast is redundant but javac disagrees //noinspection RedundantCast return (T) Enum.valueOf(type, str.toUpperCase().replace(' ', '_')); } @Override public String convertToString(T e) { return WordUtils.capitalize(e.name().toLowerCase().replace('_', ' ')); } } /** * Converts {@link ZonedDateTime} to a local date string (e.g. 2012-12-21) and creates a {@link ZonedDateTime} from * a local date string with a time of 11:00 and a zone of UTC. */ /* package */ class ZonedDateTimeConverter implements StringConverter<ZonedDateTime> { @Override public ZonedDateTime convertFromString(Class<? extends ZonedDateTime> cls, String localDateString) { LocalDate localDate = LocalDate.parse(localDateString); return localDate.atTime(11, 0).atZone(ZoneOffset.UTC); } @Override public String convertToString(ZonedDateTime dateTime) { return dateTime.toLocalDate().toString(); } } /** * Converts an {@link OffsetTime} to a time string (e.g. 11:35) and discards the offset. Creates * an {@link OffsetTime} instance by parsing a local date string and using UTC as the offset. */ /* package */ class OffsetTimeConverter implements StringConverter<OffsetTime> { @Override public OffsetTime convertFromString(Class<? extends OffsetTime> cls, String timeString) { if (!StringUtils.isEmpty(timeString)) { return OffsetTime.of(LocalTime.parse(timeString.trim()), ZoneOffset.UTC); } else { return null; } } @Override public String convertToString(OffsetTime time) { return time.toLocalTime().toString(); } } /** * Converts between an {@link Expiry} and a local date string (e.g. 2011-03-08). */ /* package */ class ExpiryConverter implements StringConverter<Expiry> { @Override public Expiry convertFromString(Class<? extends Expiry> cls, String localDateString) { LocalDate localDate = LocalDate.parse(localDateString); return new Expiry(localDate.atTime(11, 0).atZone(ZoneOffset.UTC)); } @Override public String convertToString(Expiry expiry) { return expiry.getExpiry().toLocalDate().toString(); } } /** * Converts doubles to strings in simple format (i.e. no scientific notation). Limits to 5DP. */ /* package */ class DoubleConverter implements StringConverter<Double> { @Override public Double convertFromString(Class<? extends Double> cls, String str) { try { return BlotterUtils.getDecimalFormat().parse(str).doubleValue(); } catch (ParseException e) { throw new IllegalArgumentException("Failed to parse number", e); } } @Override public String convertToString(Double value) { return BlotterUtils.getDecimalFormat().format(value); } } /** * Converts big decimals to strings in simple format (i.e. no scientific notation). Limits to 5DP. */ /* package */ class BigDecimalConverter implements StringConverter<BigDecimal> { @Override public BigDecimal convertFromString(Class<? extends BigDecimal> cls, String str) { try { Number number = BlotterUtils.getDecimalFormat().parse(str); // bizarrely if you call setParseBigDecimal(true) on a DecimalFormat it returns a BigDecimal unless the number // is NaN or +/- infinity in which case it returns a Double if (number instanceof BigDecimal) { return (BigDecimal) number; } else { throw new IllegalArgumentException("Failed to parse number as BigDecimal: " + number); } } catch (ParseException e) { throw new IllegalArgumentException("Failed to parse number", e); } } @Override public String convertToString(BigDecimal value) { return BlotterUtils.getDecimalFormat().format(value); } } /** * Converts a string to an {@link ExternalId} with a scheme of {@link ExternalSchemes#FINANCIAL}. */ /* package */ class StringToRegionIdConverter implements Converter<String, ExternalId> { /** * Converts a string to an {@link ExternalId} with a scheme of {@link ExternalSchemes#FINANCIAL}. * @param region The region name, not empty * @return An {@link ExternalId} with a scheme of {@link ExternalSchemes#FINANCIAL} and a value of {@code region}. */ @Override public ExternalId convert(String region) { if (StringUtils.isEmpty(region)) { throw new IllegalArgumentException("Region must not be empty"); } return ExternalId.of(ExternalSchemes.FINANCIAL, region); } } /** * Converts an {@link ExternalId} to a string. */ /* package */ class RegionIdToStringConverter implements Converter<ExternalId, String> { /** * Converts an {@link ExternalId} to a string * @param regionId The region ID, not null * @return {@code regionId}'s value */ @Override public String convert(ExternalId regionId) { ArgumentChecker.notNull(regionId, "regionId"); return regionId.getValue(); } }