package com.intuit.benten.hackernews.utils; import com.intuit.benten.common.http.HttpHelper; import com.intuit.benten.hackernews.exceptions.BentenHackernewsException; import com.intuit.benten.hackernews.model.HackernewsItem; import com.intuit.benten.hackernews.model.HackernewsSetRange; import com.intuit.benten.hackernews.properties.HackernewsProperties; import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.util.EntityUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.io.IOException; import java.util.Arrays; import java.util.LinkedList; import java.util.List; import java.util.stream.Collectors; /** * Created by jleveroni on 10/09/2019 */ @Component public class HackernewsService { private static final Logger logger = LoggerFactory.getLogger(HackernewsService.class); @Autowired private HttpHelper httpHelper; @Autowired private HackernewsProperties hackernewsProperties; @Autowired private HackernewsExecutorService hackernewsExecutorService; public List<HackernewsItem> fetchHackernewsCollectionContent(String actionName, Integer resultSetSize, Integer offset, Integer startIndex) throws BentenHackernewsException { return requestItemIds(HackernewsConstants.ApiEndpoints.fromActionName(actionName), resultSetSize, offset, startIndex); } private List<HackernewsItem> requestItemIds(String endpoint, Integer resultSetSize, Integer offset, Integer startIndex) throws BentenHackernewsException { String uri = buildHackernewsRequestUrl(endpoint); HttpGet req = new HttpGet(uri); try { HttpResponse res = httpHelper.getClient().execute(req); if (res.getStatusLine().getStatusCode() != 200) { String message = "Response code " + res.getStatusLine().getStatusCode(); throw new BentenHackernewsException(message); } String json = EntityUtils.toString(res.getEntity()); List<Integer> hackerNewsItemIds = parseListOfIds(json, resultSetSize, offset, startIndex); return fetchHackerNewsItems(hackerNewsItemIds); } catch (IOException e) { throw new BentenHackernewsException("requestItemIds result could not be handled", e.getMessage()); } } private String buildHackernewsRequestUrl(String ... pathSegments) { StringBuilder sb = new StringBuilder(hackernewsProperties.getUrlWithApiVersion()); if (pathSegments != null) { for (String segment : pathSegments) { sb.append("/").append(segment); } } sb.append(HackernewsConstants.ApiEndpoints.PRETTY_PRINT); return sb.toString(); } // Normally the json response from an api call would be deserialized with an object mapper // however the hacker news api returns invalid json. its raw response is literally [id1, id2, ...] // so we need to do some string manipulation to work with it private List<Integer> parseListOfIds(String idListJson, Integer resultSetSize, Integer offset, Integer startIndex) { boolean isOffsetBased = true; if (resultSetSize == null || 0 == resultSetSize || 0 > resultSetSize) { resultSetSize = HackernewsConstants.HACKERNEWS_DEFAULT_ITEM_LIMIT; } else if (resultSetSize > HackernewsConstants.HACKERNEWS_MAX_ITEM_LIMIT) { logger.info(HackernewsConstants.ErrorMessages.LIMIT_EXCEEDS_MAX_LIMIT + resultSetSize); resultSetSize = HackernewsConstants.HACKERNEWS_MAX_ITEM_LIMIT; } // default to offset if it is set, or if both offset and startIndex are set if (offset == null && startIndex != null) { isOffsetBased = false; } else { if (offset == null || 0 > offset) { offset = HackernewsConstants.HACKERNEWS_DEFAULT_OFFSET; } } String trimmedIdList = idListJson.substring(2, idListJson.length() - 2); String[] stringIds = trimmedIdList.split(","); HackernewsSetRange floorAndCeilingRange; if (isOffsetBased) { floorAndCeilingRange = calculateCollectionRangeByOffset(resultSetSize, offset); } else { if ((startIndex - 1) < 0) { throw new BentenHackernewsException("Start index cannot be less than 1."); } else { floorAndCeilingRange = calculateCollectionRangeByStartIndex(resultSetSize, startIndex); } } Integer floor = floorAndCeilingRange.getMin(); Integer ceiling = floorAndCeilingRange.getMax(); if (ceiling > stringIds.length) { String sb = HackernewsConstants.ErrorMessages.INVALID_LIMIT_AND_OFFSET_CONFIGURATION + "Hackernews Ids length: " + stringIds.length + "StartIndex: " + floor + "Offset: " + offset; throw new BentenHackernewsException(sb); } String[] trimmedIdsSubset = Arrays.copyOfRange(stringIds, floor, ceiling); return Arrays.stream(trimmedIdsSubset) .map(String::trim) .map(Integer::parseInt) .collect(Collectors.toCollection(LinkedList::new)); } private HackernewsSetRange calculateCollectionRangeByOffset(Integer size, Integer offset) { Integer startIndex = (size * offset); int stopIndex = (startIndex + size); return new HackernewsSetRange(startIndex, stopIndex); } private HackernewsSetRange calculateCollectionRangeByStartIndex(Integer size, Integer startIndex) { int stopIndex = (startIndex + size); return new HackernewsSetRange(startIndex, stopIndex); } private List<HackernewsItem> fetchHackerNewsItems(List<Integer> itemIds) { List<FetchHackernewsItemTask> tasks = itemIds.stream() .map(x -> new FetchHackernewsItemTask(hackernewsProperties.getUrlWithApiVersion(), x)) .collect(Collectors.toList()); return hackernewsExecutorService.submitFetchHackernewsItemTasks(tasks); } }