/** * Copyright (C) 2012 - present by OpenGamma Inc. and the OpenGamma group of companies * * Please see distribution for license. */ package com.opengamma.integration.copier.portfolio.web; import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import javax.ws.rs.Consumes; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.StreamingOutput; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.opengamma.bbg.referencedata.ReferenceDataProvider; import com.opengamma.integration.copier.portfolio.ResolvingPortfolioCopier; import com.opengamma.integration.copier.portfolio.SimplePortfolioCopier; import com.opengamma.integration.copier.portfolio.reader.PositionReader; import com.opengamma.integration.copier.portfolio.reader.SingleSheetSimplePositionReader; import com.opengamma.integration.copier.portfolio.rowparser.ExchangeTradedRowParser; import com.opengamma.integration.copier.portfolio.rowparser.RowParser; import com.opengamma.integration.copier.portfolio.writer.MasterPositionWriter; import com.opengamma.integration.copier.portfolio.writer.PositionWriter; import com.opengamma.integration.copier.sheet.SheetFormat; import com.opengamma.integration.tool.portfolio.xml.SchemaRegister; import com.opengamma.integration.tool.portfolio.xml.XmlFileReader; import com.opengamma.master.historicaltimeseries.HistoricalTimeSeriesMaster; import com.opengamma.master.portfolio.PortfolioMaster; import com.opengamma.master.position.PositionMaster; import com.opengamma.master.security.SecurityMaster; import com.opengamma.provider.historicaltimeseries.HistoricalTimeSeriesProvider; import com.opengamma.provider.security.SecurityProvider; import com.opengamma.util.ArgumentChecker; import com.sun.jersey.multipart.BodyPartEntity; import com.sun.jersey.multipart.FormDataBodyPart; import com.sun.jersey.multipart.FormDataMultiPart; /** * REST resource that uploads a CSV file containing a portfolio definition and passes it to a portfolio loader. */ @Path("portfolioupload") public class PortfolioLoaderResource { private static final Logger s_logger = LoggerFactory.getLogger(PortfolioLoaderResource.class); private final PortfolioMaster _portfolioMaster; private final PositionMaster _positionMaster; private final SecurityMaster _securityMaster; private final HistoricalTimeSeriesMaster _historicalTimeSeriesMaster; private final SecurityProvider _securityProvider; private final HistoricalTimeSeriesProvider _historicalTimeSeriesProvider; private final ReferenceDataProvider _referenceDataProvider; /** * Creates an instance. * * @param portfolioMaster the master, not null * @param positionMaster the master, not null * @param securityMaster the master, not null * @param historicalTimeSeriesMaster the master, not null * @param securityProvider the provider, not null * @param historicalTimeSeriesProvider the provider, not null * @param bloombergReferenceDataProvider the provider, not null */ public PortfolioLoaderResource(PortfolioMaster portfolioMaster, PositionMaster positionMaster, SecurityMaster securityMaster, HistoricalTimeSeriesMaster historicalTimeSeriesMaster, SecurityProvider securityProvider, HistoricalTimeSeriesProvider historicalTimeSeriesProvider, ReferenceDataProvider bloombergReferenceDataProvider) { ArgumentChecker.notNull(positionMaster, "positionMaster"); ArgumentChecker.notNull(portfolioMaster, "portfolioMaster"); ArgumentChecker.notNull(securityMaster, "securityMaster"); ArgumentChecker.notNull(historicalTimeSeriesMaster, "historicalTimeSeriesMaster"); ArgumentChecker.notNull(securityProvider, "securityProvider"); ArgumentChecker.notNull(historicalTimeSeriesProvider, "historicalTimeSeriesProvider"); ArgumentChecker.notNull(bloombergReferenceDataProvider, "referenceDataProvider"); _portfolioMaster = portfolioMaster; _positionMaster = positionMaster; _securityMaster = securityMaster; _historicalTimeSeriesMaster = historicalTimeSeriesMaster; _securityProvider = securityProvider; _historicalTimeSeriesProvider = historicalTimeSeriesProvider; _referenceDataProvider = bloombergReferenceDataProvider; } @POST @Consumes(MediaType.MULTIPART_FORM_DATA) @Produces(MediaType.TEXT_PLAIN) @SuppressWarnings("resource") public Response uploadPortfolio(FormDataMultiPart formData) throws IOException { FormDataBodyPart fileBodyPart = getBodyPart(formData, "file"); FormDataBodyPart filexmlBodyPart = getBodyPart(formData, "filexml"); if (filexmlBodyPart.getFormDataContentDisposition().getFileName().toLowerCase().endsWith("xml")) { // xml can contain multiple portfolios Object filexmlEntity = filexmlBodyPart.getEntity(); InputStream filexmlStream = new WorkaroundInputStream(((BodyPartEntity) filexmlEntity).getInputStream()); for (PositionReader positionReader : returnPorfolioReader(filexmlStream)) { xmlPortfolioCopy(positionReader); } return Response.ok("Upload complete").build(); } else { Object fileEntity = fileBodyPart.getEntity(); String fileName = fileBodyPart.getFormDataContentDisposition().getFileName(); InputStream fileStream = new WorkaroundInputStream(((BodyPartEntity) fileEntity).getInputStream()); String dataField = getString(formData, "dataField"); String dataProvider = getString(formData, "dataProvider"); String portfolioName = getString(formData, "portfolioName"); String dateFormatName = getString(formData, "dateFormat"); // fields can be separated by whitespace or a comma with whitespace String[] dataFields = dataField.split("(\\s*,\\s*|\\s+)"); s_logger.info("Portfolio uploaded. fileName: {}, portfolioName: {}, dataField: {}, dataProvider: {}", fileName, portfolioName, dataField, dataProvider); if (fileEntity == null) { throw new WebApplicationException(Response.Status.BAD_REQUEST); } final ResolvingPortfolioCopier copier = new ResolvingPortfolioCopier(_historicalTimeSeriesMaster, _historicalTimeSeriesProvider, _referenceDataProvider, dataProvider, dataFields); final PositionWriter positionWriter = new MasterPositionWriter(portfolioName, _portfolioMaster, _positionMaster, _securityMaster, false, false, true); SheetFormat format = getFormatForFileName(fileName); ExchangeTradedRowParser.DateFormat dateFormat = Enum.valueOf(ExchangeTradedRowParser.DateFormat.class, dateFormatName); RowParser rowParser = new ExchangeTradedRowParser(_securityProvider, dateFormat); final PositionReader positionReader = new SingleSheetSimplePositionReader(format, fileStream, rowParser); StreamingOutput streamingOutput = new StreamingOutput() { @Override public void write(OutputStream output) throws IOException, WebApplicationException { // TODO callback for progress updates as portoflio is copied copier.copy(positionReader, positionWriter); output.write("Upload complete".getBytes()); } }; return Response.ok(streamingOutput).build(); } } private void xmlPortfolioCopy(PositionReader positionReader) { SimplePortfolioCopier copier = new SimplePortfolioCopier(null); final PositionWriter positionWriter = new MasterPositionWriter(positionReader.getPortfolioName(), _portfolioMaster, _positionMaster, _securityMaster, false, false, true); // Call the portfolio loader with the supplied arguments copier.copy(positionReader, positionWriter); // close stuff positionReader.close(); positionWriter.close(); } private Iterable<? extends PositionReader> returnPorfolioReader(InputStream fileStream) { return new XmlFileReader(fileStream, new SchemaRegister()); } private static FormDataBodyPart getBodyPart(FormDataMultiPart formData, String fieldName) { FormDataBodyPart bodyPart = formData.getField(fieldName); if (bodyPart == null) { Response response = Response.status(Response.Status.BAD_REQUEST).entity("Missing form field: " + fieldName).build(); throw new WebApplicationException(response); } return bodyPart; } private static String getString(FormDataMultiPart formData, String fieldName) { FormDataBodyPart bodyPart = getBodyPart(formData, fieldName); String value = bodyPart.getValue(); if (StringUtils.isEmpty(value)) { Response response = Response.status(Response.Status.BAD_REQUEST).entity("Missing form value: " + fieldName).build(); throw new WebApplicationException(response); } return value; } // TODO this belongs somewhere else private static SheetFormat getFormatForFileName(String fileName) { if (fileName.toLowerCase().endsWith("csv")) { return SheetFormat.CSV; } else if (fileName.toLowerCase().endsWith("xls")) { return SheetFormat.XLS; } Response response = Response.status(Response.Status.BAD_REQUEST).entity("Portfolio upload only supports CSV/XLS" + "files and Excel worksheets").build(); throw new WebApplicationException(response); } /** * This wraps the file upload input stream to work around a bug in {@code org.jvnet.mimepull} which is used by Jersey * Multipart. The bug causes the {@code read()} method of the file upload stream to throw an exception if it is * called twice at the end of the stream which violates the contract of {@link InputStream}. It ought to * keep returning {@code -1} indefinitely. This class restores that behaviour. * TODO Check if this can be removed when we upgrade Jersey. It is a problem when the CSV file doesn't end with a new line * @see <a href="http://java.net/jira/browse/JAX_WS-965">The bug report</a> */ private static class WorkaroundInputStream extends FilterInputStream { private boolean _ended; public WorkaroundInputStream(InputStream in) { super(in); } @Override public int read() throws IOException { if (_ended) { return -1; } int i = super.read(); if (i == -1) { _ended = true; } return i; } @Override public int read(byte[] b) throws IOException { if (_ended) { return -1; } int i = super.read(b); if (i == -1) { _ended = true; } return i; } @Override public int read(byte[] b, int off, int len) throws IOException { if (_ended) { return -1; } int i = super.read(b, off, len); if (i == -1) { _ended = true; } return i; } } }