Сводка
В настоящее время я учусь потоковой передаче данных JSON в модели данных POJO. Теперь я застрял в обработке реального примера. Пожалуйста, помогите мне понять, где моя (-и) ошибка (-и) мысли, и укажите мне правильное направление.
Текущая задача: мне нужно прочитать более сложную (для моего уровня опыта) конструкцию JSONиз поставщика данных о запасах в пользовательскую модель данных POJO. Я не имею никакого влияния на формат JSON, но модель данных и код обработки полностью зависят от меня.
Givens
Просто так, что вызнаю, какой у меня уровень знаний: я успешно преобразовал простые списки JSON [{"keyA":"valueA1","keyB":"ValueB1"},{"keyA":"valueA2","keyB":"ValueB2"}]
в экземпляры пользовательских классов, хранящихся в списке.
Мне нужно использовать Java 1.8
Ниже приведены исходные данные 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 часа поиска и чтения либо не привели к решению, либо я не смог распознать его как таковой.