GsonAwareParser.java
/**
 * Copyright 2012 the original author or authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.googlecode.phisix.api.parser;
import java.io.Reader;
import java.lang.reflect.Type;
import java.text.ParseException;
import java.util.Calendar;
import java.util.Date;
import java.util.TimeZone;
import org.apache.commons.lang3.time.DateParser;
import org.apache.commons.lang3.time.FastDateFormat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonNull;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.Strictness;
import com.googlecode.phisix.api.model.Stock;
import com.googlecode.phisix.api.model.Stocks;
import com.googlecode.phisix.api.parser.Parser;
/**
 * {@link Parser} that delegates to a {@link JsonParser}.
 * 
 * @author Edge Dalmacio
 * 
 */
public class GsonAwareParser implements Parser<Reader, Stocks> {
	private static final Logger LOGGER = LoggerFactory.getLogger(GsonAwareParser.class);
	private static final TimeZone ASIA_MANILA = TimeZone.getTimeZone("Asia/Manila");
	private static final DateParser dateParser = FastDateFormat.getInstance("MM/dd/yyyy hh:mm a", ASIA_MANILA);
	private final Gson gson;
	
	public GsonAwareParser() {
		Type type = new TypeToken<Stock>() {}.getType();
		// Gson 2.11.0+: strictness is controlled at JsonReader level, not GsonBuilder
		gson = new GsonBuilder()
			.registerTypeAdapter(type, new PhisixDeserializer())
			.create();
	}
	@Override
	public Stocks parse(Reader source) {
		Stocks stocks = new Stocks();
		
		JsonElement rootElement = parseJsonElement(source);
		if (rootElement == null) {
			setDefaultAsOf(stocks);
			return stocks;
		}
		
		JsonArray jsonArray = extractStockArray(rootElement, stocks);
		parseStocksFromArray(jsonArray, stocks);
		setDefaultAsOfIfMissing(stocks);
		
		return stocks;
	}
	
	private JsonElement parseJsonElement(Reader source) {
		try {
			JsonReader jsonReader = new JsonReader(source);
			// Strictness enum is in com.google.gson package starting from Gson 2.11.0
			jsonReader.setStrictness(Strictness.LENIENT);
			JsonElement parse = JsonParser.parseReader(jsonReader);
			if (parse == null || JsonNull.INSTANCE.equals(parse)) {
				return null;
			}
			return parse;
		} catch (Exception e) {
			LOGGER.error("Failed to parse JSON", e);
			return null;
		}
	}
	
	private JsonArray extractStockArray(JsonElement rootElement, Stocks stocks) {
		if (rootElement.isJsonObject()) {
			return extractStockArrayFromObject(rootElement.getAsJsonObject(), stocks);
		}
		return rootElement.getAsJsonArray();
	}
	
	private JsonArray extractStockArrayFromObject(JsonObject rootObject, Stocks stocks) {
		// Handle both "stock" (singular, our format) and "stocks" (plural, external API format)
		if (rootObject.has("stock") && rootObject.get("stock").isJsonArray()) {
			parseAsOfFromObject(rootObject, stocks);
			return rootObject.get("stock").getAsJsonArray();
		}
		if (rootObject.has("stocks") && rootObject.get("stocks").isJsonArray()) {
			parseAsOfFromObject(rootObject, stocks);
			return rootObject.get("stocks").getAsJsonArray();
		}
		// Fallback: treat object as array with single element
		JsonArray jsonArray = new JsonArray();
		jsonArray.add(rootObject);
		return jsonArray;
	}
	
	private void parseAsOfFromObject(JsonObject rootObject, Stocks stocks) {
		JsonElement asOfElement = rootObject.get("as_of");
		if (asOfElement != null && !asOfElement.isJsonNull()) {
			Calendar asOfCalendar = parseAsOfDateFromString(asOfElement.getAsString());
			if (asOfCalendar != null) {
				stocks.setAsOf(asOfCalendar);
			}
		}
	}
	
	private void parseStocksFromArray(JsonArray jsonArray, Stocks stocks) {
		Type type = new TypeToken<Stock>() {}.getType();
		for (JsonElement jsonElement : jsonArray) {
			Stock stock = gson.fromJson(jsonElement, type);
			if (stock != null) {
				stocks.getStocks().add(stock);
			}
		}
	}
	
	private void setDefaultAsOfIfMissing(Stocks stocks) {
		if (stocks.getAsOf() == null) {
			setDefaultAsOf(stocks);
		}
	}
	
	private void setDefaultAsOf(Stocks stocks) {
		Calendar calendar = Calendar.getInstance(ASIA_MANILA);
		calendar.set(Calendar.HOUR_OF_DAY, 0);
		calendar.set(Calendar.MINUTE, 0);
		calendar.set(Calendar.SECOND, 0);
		calendar.set(Calendar.MILLISECOND, 0);
		stocks.setAsOf(calendar);
	}
	
	protected Calendar parseAsOfDate(JsonObject jsonObject) {
		String asOfDate = jsonObject.get("securityAlias").getAsString();
		Calendar calendar = null;
		try {
			Date date = dateParser.parse(asOfDate);
			calendar = Calendar.getInstance(ASIA_MANILA);
			calendar.setTime(date);
		} catch (ParseException e) {
			if (LOGGER.isWarnEnabled()) {
				LOGGER.warn(e.getMessage(), e);
			}
		}
		return calendar;
	}
	
	protected Calendar parseAsOfDateFromString(String asOfDateString) {
		Calendar calendar = null;
		try {
			// Handle ISO 8601 format: "2025-10-31T00:00:00+08:00"
			java.text.SimpleDateFormat isoFormat = new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX");
			Date date = isoFormat.parse(asOfDateString);
			calendar = Calendar.getInstance(ASIA_MANILA);
			calendar.setTime(date);
		} catch (ParseException e) {
			try {
				// Try without timezone offset
				java.text.SimpleDateFormat isoFormatNoTz = new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
				Date date = isoFormatNoTz.parse(asOfDateString);
				calendar = Calendar.getInstance(ASIA_MANILA);
				calendar.setTime(date);
			} catch (ParseException e2) {
				if (LOGGER.isWarnEnabled()) {
					LOGGER.warn("Failed to parse as_of date: " + asOfDateString, e2);
				}
			}
		}
		return calendar;
	}
}