потоковая нетривиальная структура данных JSON в модель данных Java POJO с использованием привязки johnzon и jakarta - PullRequest
0 голосов
/ 05 октября 2019

Сводка

В настоящее время я учусь потоковой передаче данных JSON в модели данных POJO. Теперь я застрял в обработке реального примера. Пожалуйста, помогите мне понять, где моя (-и) ошибка (-и) мысли, и укажите мне правильное направление.

Текущая задача: мне нужно прочитать более сложную (для моего уровня опыта) конструкцию JSONиз поставщика данных о запасах в пользовательскую модель данных POJO. Я не имею никакого влияния на формат JSON, но модель данных и код обработки полностью зависят от меня.

Givens

  1. Просто так, что вызнаю, какой у меня уровень знаний: я успешно преобразовал простые списки JSON [{"keyA":"valueA1","keyB":"ValueB1"},{"keyA":"valueA2","keyB":"ValueB2"}] в экземпляры пользовательских классов, хранящихся в списке.

  2. Мне нужно использовать Java 1.8

  3. Ниже приведены исходные данные JSON, которые я должен обработать, просто с сокращенным списком во «Временном ряду» до трех записей, поэтому его можно публиковать здесь.

    / example_short.json

    {
        "Meta Data": {
            "1. Information": "Intraday (1min) open, high, low, close prices and volume",
            "2. Symbol": "AAPL",
            "3. Last Refreshed": "2019-10-04 16:00:00",
            "4. Interval": "1min",
            "5. Output Size": "Compact",
            "6. Time Zone": "US/Eastern"
        },
        "Time Series (1min)": {
            "2019-10-04 16:00:00": {
                "1. open": "227.0200",
                "2. high": "227.1100",
                "3. low": "226.6500",
                "4. close": "227.0100",
                "5. volume": "670411"
            },
            "2019-10-04 15:59:00": {
                "1. open": "227.0295",
                "2. high": "227.2985",
                "3. low": "227.0182",
                "4. close": "227.0495",
                "5. volume": "273103"
            },
            "2019-10-04 15:58:00": {
                "1. open": "226.9600",
                "2. high": "227.0400",
                "3. low": "226.9200",
                "4. close": "227.0126",
                "5. volume": "207653"
            }
        }
    }

Что я сделал до сих пор

Я настроил свой проект с некоторымизависимости от библиотек, которые помогли мне раньше:

/ pom.xml (отрывок)

    <dependencies>
        <dependency>
            <groupId>javax.json</groupId>
            <artifactId>javax.json-api</artifactId>
            <version>1.1.4</version>
        </dependency>
        <dependency>
            <groupId>jakarta.json.bind</groupId>
            <artifactId>jakarta.json.bind-api</artifactId>
            <version>1.0.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.johnzon</groupId>
            <artifactId>johnzon-jsonb</artifactId>
            <version>1.1.12</version>
        </dependency>
    </dependencies>

Хотя я не отвергаю подсказки другим заслуживающим доверия библиотекам (например, изПроект Apache, GSON не пойдет), я бы предпочел остаться с теми, о которых я уже немного узнал, и Натиve Java ofc.

Я создал следующую модель данных POJO:

Одна для «конверта», содержащая поля для экземпляра MetaData и коллекцию экземпляров DataPoint временного ряда.

/ example_short.json


    package avc.datamodel;

    import java.util.Collections;
    import java.util.Map;
    import java.util.TreeMap;

    import org.apache.johnzon.mapper.JohnzonProperty;

    public class TimeSeriesIntradayResponse
    {
        @JohnzonProperty("Meta Data")
        private MetaData               metaData;

        @JohnzonProperty("Time Series (1min)")
        private Map<String, DataPoint> timeSeries = new TreeMap<>();

        public MetaData getMetaData()
        {
            return metaData;
        }

        public Map<String, DataPoint> getTimeSeries()
        {
            return Collections.unmodifiableMap(timeSeries);
        }

        public void setMetaData(MetaData metaData)
        {
            this.metaData = metaData;
        }

        public void setTimeSeries(Map<String, DataPoint> timeSeries)
        {
            this.timeSeries = timeSeries;
        }
    }

Если вы инстинктивно хотите указать, что использование ключа DateTime в качестве ключа для отсортированной карты timeSeries может быть лучше реализовано с LocalTimeDateзатем со строкой. Да, я знаю, но мне все еще нужно выяснить, как выполнить преобразование ключей JSON. Но это не вопрос для этого поста, и, возможно, я смогу выяснить это самостоятельно после того, как моя текущая проблема решена.

/ avc / datamodel / MetaData.java

    package avc.datamodel;

    import java.io.Serializable;
    import java.time.Duration;
    import java.time.LocalDateTime;
    import java.time.ZoneId;

    import org.apache.johnzon.mapper.JohnzonConverter;
    import org.apache.johnzon.mapper.JohnzonIgnore;
    import org.apache.johnzon.mapper.JohnzonProperty;

    public class MetaData
    {
        @JohnzonProperty("1. Information")
        private String        information;

        @JohnzonProperty("2. Symbol")
        private String        symbol;

        @JohnzonProperty("3. Last Refreshed")
        @JohnzonConverter(LocalDateTimeConverter.class)
        private LocalDateTime lastRefresh;

        @JohnzonProperty("4. Interval")
        private Duration      interval;

        @JohnzonProperty("5. Output Size")
        private String        outputSize;

        @JohnzonProperty("6. Time Zone")
        @JohnzonConverter(ZoneConverter.class)
        private ZoneId        timeZone;

        public String getInformation()
        {
            return information;
        }

        public Duration getInterval()
        {
            return interval;
        }

        public LocalDateTime getLastRefresh()
        {
            return lastRefresh;
        }

        public String getOutputSize()
        {
            return outputSize;
        }

        public String getSymbol()
        {
            return symbol;
        }

        public ZoneId getTimeZone()
        {
            return timeZone;
        }

        public void setInformation(String information)
        {
            this.information = information;
        }

        public void setInterval(Duration interval)
        {
            this.interval = interval;
        }

        public void setLastRefresh(LocalDateTime lastRefresh)
        {
            this.lastRefresh = lastRefresh;
        }

        public void setOutputSize(String outputSize)
        {
            this.outputSize = outputSize;
        }

        public void setSymbol(String symbol)
        {
            this.symbol = symbol;
        }

        public void setTimeZone(ZoneId timeZone)
        {
            this.timeZone = timeZone;
        }
    }

/ avc / datamodel / DataPoint.java

    package avc.datamodel;

    import java.io.Serializable;

    import org.apache.johnzon.mapper.JohnzonConverter;
    import org.apache.johnzon.mapper.JohnzonIgnore;
    import org.apache.johnzon.mapper.JohnzonProperty;

    public class DataPoint implements Serializable
    {
        @JohnzonIgnore
        private static final long serialVersionUID = -864318740406225178L;

        public static long getSerialversionuid()
        {
            return serialVersionUID;
        }

        @JohnzonProperty("1. open")
        @JohnzonConverter(DoubleConverter.class)
        private double open;

        @JohnzonProperty("2. high")
        @JohnzonConverter(DoubleConverter.class)
        private double high;

        @JohnzonProperty("3. low")
        @JohnzonConverter(DoubleConverter.class)
        private double low;

        @JohnzonProperty("4. close")
        @JohnzonConverter(DoubleConverter.class)
        private double close;

        @JohnzonProperty("5. volume")
        @JohnzonConverter(LongConverter.class)
        private long   volume;

        public double getClose()
        {
            return close;
        }

        public double getHigh()
        {
            return high;
        }

        public double getLow()
        {
            return low;
        }

        public double getOpen()
        {
            return open;
        }

        public long getVolume()
        {
            return volume;
        }

        public void setClose(double close)
        {
            this.close = close;
        }

        public void setHigh(double high)
        {
            this.high = high;
        }

        public void setLow(double low)
        {
            this.low = low;
        }

        public void setOpen(double open)
        {
            this.open = open;
        }

        public void setVolume(long volume)
        {
            this.volume = volume;
        }
    }

и необходимые преобразователи

/ avc / datamodel / DoubleConverter.java

    package avc.datamodel;

    import org.apache.johnzon.mapper.Converter;

    public class DoubleConverter implements Converter<Double>
    {
        @Override
        public Double fromString(String text)
        {
            return Double.parseDouble(text);
        }

        @Override
        public String toString(Double instance)
        {
            return instance.toString();
        }
    }

/ avc / datamodel / DurationConverter.java

    package avc.datamodel;

    import java.time.Duration;
    import java.util.regex.Matcher;
    import java.util.regex.Pattern;

    import org.apache.johnzon.mapper.Converter;

    public class DurationConverter implements Converter<Duration>
    {
        @Override
        public Duration fromString(String text)
        {
            Duration duration = null;
            Pattern p = Pattern.compile("([0-9]+)([a-zA-Z]+)");
            Matcher m = p.matcher(text);
            if (m.matches())
            {
                int value = Integer.parseInt(m.group(1));
                String unit = m.group(2);

                switch (unit)
                {
                    case "min":
                        duration = Duration.ofMinutes(value);
                        break;
                    // TODO: implement different duration units
                }
            }
            return duration;
        }

        @Override
        public String toString(Duration instance)
        {
            Duration calcCopy = instance;
            StringBuilder result = new StringBuilder();

            long days = calcCopy.toDays();
            if (days > 0)
            {
                result.append(days).append("day ");
                calcCopy = calcCopy.minusDays(days);
            }

            long hours = calcCopy.toHours();
            if (hours > 0)
            {
                result.append(hours).append("h ");
                calcCopy = calcCopy.minusHours(hours);
            }

            long minutes = calcCopy.toMinutes();
            if (hours > 0)
            {
                result.append(minutes).append("min ");
                calcCopy = calcCopy.minusMinutes(minutes);
            }

            long seconds = calcCopy.getSeconds();
            if (seconds > 0)
            {
                result.append(seconds).append("sec ");
            }

            return result.toString().trim();
        }
    }

/ avc / datamodel / LocalDateTimeConverter.java

    package avc.datamodel;

    import java.time.LocalDateTime;

    import org.apache.johnzon.mapper.Converter;

    public class LocalDateTimeConverter implements Converter<LocalDateTime>
    {
        @Override
        public LocalDateTime fromString(String text)
        {
            return LocalDateTime.parse(text);
        }

        @Override
        public String toString(LocalDateTime instance)
        {
            return instance.toString();
        }

    }

/ avc / datamodel / LongConverter.java

    package avc.datamodel;

    import org.apache.johnzon.mapper.Converter;

    public class LongConverter implements Converter<Long>
    {
        @Override
        public Long fromString(String text)
        {
            return Long.parseLong(text);
        }

        @Override
        public String toString(Long instance)
        {
            return instance.toString();
        }

    }

/ avc / datamodel / ZoneConverter.java

    package avc.datamodel;

    import java.time.ZoneId;

    import org.apache.johnzon.mapper.Converter;

    public class ZoneConverter implements Converter<ZoneId>
    {
        @Override
        public ZoneId fromString(String text)
        {
            return ZoneId.of(text);
        }

        @Override
        public String toString(ZoneId instance)
        {
            return instance.toString();
        }
    }

Наконец, я создал макет для клиента, чтобы написать его, который просто фокусируется на той части, с которой у меня сейчас проблема.

/ avc / ClientMockup.java

    package avc;

    import java.io.IOException;
    import java.io.InputStreamReader;
    import java.util.Optional;

    import javax.json.bind.Jsonb;

    import org.apache.johnzon.jsonb.JohnzonBuilder;

    import avc.datamodel.TimeSeriesIntradayResponse;

    public class ClientMockup
    {
        public static Optional<TimeSeriesIntradayResponse> query(String resourceName) throws IOException
        {
            Optional<TimeSeriesIntradayResponse> responseObject = Optional.empty();
            Jsonb jsonb = new JohnzonBuilder().build();
            try (InputStreamReader reader = new InputStreamReader(ClientMockup.class.getClassLoader().getResourceAsStream(resourceName)))
            {
                responseObject = Optional.of(jsonb.fromJson(reader, new TimeSeriesIntradayResponse()
                {
                }.getClass().getGenericSuperclass()));
            }

            return responseObject;
        }

    }

Чтение из файла только для макета. Данные в реальном приложении будут поступать из службы веб-службы провайдера, доступ к которой осуществляется через url.openStream (). Одна из сдерживающих частей здесь заключается в том, что я хотел бы напрямую обработать поток, не читая поток сначала в какой-либо последовательности символов, а затем обрабатывать ее, если только этого нельзя избежать разумно.

Я использую свой тест JUnitкласс для его выполнения:

/ avc / ClientMockupTests.java

    package avc;

    import java.io.IOException;
    import java.time.Duration;
    import java.time.LocalDateTime;
    import java.util.Map;
    import java.util.Optional;
    import java.util.TimeZone;

    import org.junit.jupiter.api.Assertions;
    import org.junit.jupiter.api.Test;

    import avc.datamodel.DataPoint;
    import avc.datamodel.MetaData;
    import avc.datamodel.TimeSeriesIntradayResponse;

    public class ClientMockupTests
    {
        @Test
        public void testTimeSeriesIntraday() throws IOException
        {
            Optional<TimeSeriesIntradayResponse> wrappedResponse = ClientMockup.query("example_short.json");
            Assertions.assertTrue(wrappedResponse.isPresent());

            TimeSeriesIntradayResponse response = wrappedResponse.get();

            // test Meta Data
            MetaData metaData = response.getMetaData();
            Assertions.assertNotNull(metaData, "Meta Data is <null>.");
            Assertions.assertEquals("Intraday (1min) open, high, low, close prices and volume", metaData.getInformation());
            Assertions.assertEquals("AAPL", metaData.getSymbol());
            Assertions.assertEquals(LocalDateTime.parse("2019-10-04 16:00:00"), metaData.getLastRefresh());
            Assertions.assertEquals(Duration.ofMinutes(1), metaData.getInterval());
            Assertions.assertEquals("Compact", metaData.getOutputSize());
            Assertions.assertEquals(TimeZone.getTimeZone("US/Eastern"), metaData.getTimeZone());

            // test Time Series
            Map<String, DataPoint> timeSeries = response.getTimeSeries();
            Assertions.assertNotNull(timeSeries, "Time Series Map is <null>.");
            Assertions.assertEquals(3, timeSeries.size());

            LocalDateTime testKey = LocalDateTime.parse("2019-10-04 15:59:00");
            Assertions.assertTrue(timeSeries.containsKey(testKey));

            DataPoint dataPoint = timeSeries.get(testKey);
            Assertions.assertNotNull(dataPoint, "Data Point Map is <null>.");
            Assertions.assertEquals(2270200L, Math.round(dataPoint.getOpen() * 10000));
            Assertions.assertEquals(2271100L, Math.round(dataPoint.getHigh() * 10000));
            Assertions.assertEquals(2266500L, Math.round(dataPoint.getLow() * 10000));
            Assertions.assertEquals(2270100L, Math.round(dataPoint.getClose() * 10000));
            Assertions.assertEquals(670411L, dataPoint.getVolume());
        }
    }

Возвращенный TimeSeriesIntradayResponse поставляется с полем metaData, равным нулю, в соответствии с ошибочным утверждением проверки нуля,Коллекция timeSeries также содержит только 0 записей. Я знаю, что из анализа и попыток решения я уже сделал сам.

Завершение

Так что, похоже, моя установка не подходит для чтения в MetaData и TimeSeriesданные, и я не понимаю, что я должен делать по-другому. Если ответ на этот вопрос есть где-то в Интернете, 4 часа поиска и чтения либо не привели к решению, либо я не смог распознать его как таковой.

...