From 9734ded39055580b699857494e9ac8ee462e934d Mon Sep 17 00:00:00 2001 From: Adam Date: Fri, 14 Jul 2017 18:34:34 -0400 Subject: [PATCH] http-service: add item search --- .../net/runelite/http/api/item/ItemType.java | 20 ++- .../runelite/http/api/item/SearchResult.java | 42 +++++ .../net/runelite/http/service/Service.java | 2 + .../http/service/item/ItemService.java | 145 ++++++++++++++---- .../runelite/http/service/item/RSItem.java | 13 ++ .../runelite/http/service/item/RSSearch.java | 42 +++++ 6 files changed, 235 insertions(+), 29 deletions(-) create mode 100644 http-api/src/main/java/net/runelite/http/api/item/SearchResult.java create mode 100644 http-service/src/main/java/net/runelite/http/service/item/RSSearch.java diff --git a/http-api/src/main/java/net/runelite/http/api/item/ItemType.java b/http-api/src/main/java/net/runelite/http/api/item/ItemType.java index b1d38a06a2..9221f096b2 100644 --- a/http-api/src/main/java/net/runelite/http/api/item/ItemType.java +++ b/http-api/src/main/java/net/runelite/http/api/item/ItemType.java @@ -24,7 +24,25 @@ */ package net.runelite.http.api.item; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + public enum ItemType { - DEFAULT + DEFAULT; + + private static final Logger logger = LoggerFactory.getLogger(ItemType.class); + + public static ItemType of(String type) + { + try + { + return ItemType.valueOf(type.toUpperCase()); + } + catch (IllegalArgumentException ex) + { + logger.warn("unable to convert type", ex); + return DEFAULT; + } + } } diff --git a/http-api/src/main/java/net/runelite/http/api/item/SearchResult.java b/http-api/src/main/java/net/runelite/http/api/item/SearchResult.java new file mode 100644 index 0000000000..90ae1ec783 --- /dev/null +++ b/http-api/src/main/java/net/runelite/http/api/item/SearchResult.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2017, Adam + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.http.api.item; + +import java.util.List; + +public class SearchResult +{ + private List items; + + public List getItems() + { + return items; + } + + public void setItems(List items) + { + this.items = items; + } +} diff --git a/http-service/src/main/java/net/runelite/http/service/Service.java b/http-service/src/main/java/net/runelite/http/service/Service.java index fb5131dd36..6d30f5b64d 100644 --- a/http-service/src/main/java/net/runelite/http/service/Service.java +++ b/http-service/src/main/java/net/runelite/http/service/Service.java @@ -120,6 +120,8 @@ public class Service implements SparkApplication }); path("/item", () -> { + get("/search", item::search, transformer); + get("/:id", item::getItem, transformer); get("/:id/icon", item::getIcon); get("/:id/icon/large", item::getIconLarge); diff --git a/http-service/src/main/java/net/runelite/http/service/item/ItemService.java b/http-service/src/main/java/net/runelite/http/service/item/ItemService.java index e8571706f8..80bd60e314 100644 --- a/http-service/src/main/java/net/runelite/http/service/item/ItemService.java +++ b/http-service/src/main/java/net/runelite/http/service/item/ItemService.java @@ -24,6 +24,8 @@ */ package net.runelite.http.service.item; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; import com.google.gson.JsonParseException; import com.google.inject.Inject; import com.google.inject.name.Named; @@ -35,10 +37,12 @@ import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.List; import java.util.Map.Entry; +import java.util.stream.Collectors; import net.runelite.http.api.RuneliteAPI; import net.runelite.http.api.item.Item; import net.runelite.http.api.item.ItemPrice; import net.runelite.http.api.item.ItemType; +import net.runelite.http.api.item.SearchResult; import okhttp3.HttpUrl; import okhttp3.ResponseBody; import org.slf4j.Logger; @@ -54,16 +58,18 @@ public class ItemService { private static final Logger logger = LoggerFactory.getLogger(ItemService.class); - private static final HttpUrl RS_ITEM_URL = HttpUrl.parse("http://services.runescape.com/m=itemdb_oldschool/api/catalogue/detail.json"); - private static final HttpUrl RS_PRICE_URL = HttpUrl.parse("http://services.runescape.com/m=itemdb_oldschool/api/graph"); + private static final String BASE = "https://services.runescape.com/m=itemdb_oldschool"; + private static final HttpUrl RS_ITEM_URL = HttpUrl.parse(BASE + "/api/catalogue/detail.json"); + private static final HttpUrl RS_PRICE_URL = HttpUrl.parse(BASE + "/api/graph"); + private static final HttpUrl RS_SEARCH_URL = HttpUrl.parse(BASE + "/api/catalogue/items.json?category=1"); private static final String CREATE_ITEMS = "CREATE TABLE IF NOT EXISTS `items` (\n" + " `id` int(11) NOT NULL,\n" + " `name` tinytext NOT NULL,\n" + " `description` tinytext NOT NULL,\n" + " `type` enum('DEFAULT') NOT NULL,\n" - + " `icon` blob NOT NULL,\n" - + " `icon_large` blob NOT NULL,\n" + + " `icon` blob,\n" + + " `icon_large` blob,\n" + " `timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,\n" + " PRIMARY KEY (`id`)\n" + ") ENGINE=InnoDB"; @@ -81,9 +87,12 @@ public class ItemService private static final String CREATE_PRICES_IDX = "ALTER TABLE `prices`\n" + " ADD UNIQUE KEY `item` (`item`,`time`);"; - private static final String RUNELITE_ITEM_CACHE = "Runelite-Item-Cache"; + private static final String RUNELITE_CACHE = "Runelite-Cache"; private final Sql2o sql2o; + private final Cache cachedSearches = CacheBuilder.newBuilder() + .maximumSize(1024L) + .build(); @Inject public ItemService(@Named("Runelite SQL2O") Sql2o sql2o) @@ -131,7 +140,7 @@ public class ItemService if (item != null) { response.type("application/json"); - response.header(RUNELITE_ITEM_CACHE, "HIT"); + response.header(RUNELITE_CACHE, "HIT"); return item.toItem(); } @@ -139,7 +148,7 @@ public class ItemService if (item != null) { response.type("application/json"); - response.header(RUNELITE_ITEM_CACHE, "MISS"); + response.header(RUNELITE_CACHE, "MISS"); return item.toItem(); } @@ -151,10 +160,10 @@ public class ItemService int itemId = Integer.parseInt(request.params("id")); ItemEntry item = getItem(itemId); - if (item != null) + if (item != null && item.getIcon() != null) { response.type("image/gif"); - response.header(RUNELITE_ITEM_CACHE, "HIT"); + response.header(RUNELITE_CACHE, "HIT"); return item.getIcon(); } @@ -162,7 +171,7 @@ public class ItemService if (item != null) { response.type("image/gif"); - response.header(RUNELITE_ITEM_CACHE, "MISS"); + response.header(RUNELITE_CACHE, "MISS"); return item.getIcon(); } @@ -174,10 +183,10 @@ public class ItemService int itemId = Integer.parseInt(request.params("id")); ItemEntry item = getItem(itemId); - if (item != null) + if (item != null && item.getIcon_large() != null) { response.type("image/gif"); - response.header(RUNELITE_ITEM_CACHE, "HIT"); + response.header(RUNELITE_CACHE, "HIT"); return item.getIcon_large(); } @@ -185,7 +194,7 @@ public class ItemService if (item != null) { response.type("image/gif"); - response.header(RUNELITE_ITEM_CACHE, "MISS"); + response.header(RUNELITE_CACHE, "MISS"); return item.getIcon_large(); } @@ -255,10 +264,67 @@ public class ItemService itemPrice.setTime(priceEntry.getTime()); response.type("application/json"); - response.header(RUNELITE_ITEM_CACHE, hit ? "HIT" : "MISS"); + response.header(RUNELITE_CACHE, hit ? "HIT" : "MISS"); return itemPrice; } + public SearchResult search(Request request, Response response) + { + String query = request.queryParams("query"); + + // rs api seems to require lowercase + query = query.toLowerCase(); + + SearchResult searchResult = cachedSearches.getIfPresent(query); + if (searchResult != null) + { + response.type("application/json"); + response.header(RUNELITE_CACHE, "HIT"); + return searchResult; + } + + try + { + RSSearch search = fetchRSSearch(query); + + searchResult = new SearchResult(); + List items = search.getItems().stream() + .map(rsi -> rsi.toItem()) + .collect(Collectors.toList()); + searchResult.setItems(items); + + cachedSearches.put(query, searchResult); + + try (Connection con = sql2o.beginTransaction()) + { + Query q = con.createQuery("insert into items (id, name, description, type) values (:id," + + " :name, :description, :type) ON DUPLICATE KEY UPDATE name = :name," + + " description = :description, type = :type"); + + for (RSItem rsItem : search.getItems()) + { + q.addParameter("id", rsItem.getId()) + .addParameter("name", rsItem.getName()) + .addParameter("description", rsItem.getDescription()) + .addParameter("type", rsItem.getType()) + .addToBatch(); + } + + q.executeBatch(); + con.commit(); + } + + response.type("application/json"); + response.header(RUNELITE_CACHE, "MISS"); + return searchResult; + } + catch (IOException ex) + { + logger.warn("error while searching items", ex); + return null; + } + } + private ItemEntry getItem(int itemId) { try (Connection con = sql2o.open()) @@ -296,14 +362,32 @@ public class ItemService try { RSItem rsItem = fetchRSItem(itemId); - byte[] icon = fetchImage(rsItem.getIcon()); - byte[] iconLarge = fetchImage(rsItem.getIcon_large()); + byte[] icon = null, iconLarge = null; + + try + { + icon = fetchImage(rsItem.getIcon()); + } + catch (IOException ex) + { + logger.warn("error fetching image", ex); + } + + try + { + iconLarge = fetchImage(rsItem.getIcon_large()); + } + catch (IOException ex) + { + logger.warn("error fetching image", ex); + } try (Connection con = sql2o.open()) { con.createQuery("insert into items (id, name, description, type, icon, icon_large) values (:id," - + " :name, :description, :type, :icon, :icon_large)") - .addParameter("id", itemId) + + " :name, :description, :type, :icon, :icon_large) ON DUPLICATE KEY UPDATE name = :name," + + " description = :description, type = :type, icon = :icon, icon_large = :icon_large") + .addParameter("id", rsItem.getId()) .addParameter("name", rsItem.getName()) .addParameter("description", rsItem.getDescription()) .addParameter("type", rsItem.getType()) @@ -316,16 +400,7 @@ public class ItemService item.setId(itemId); item.setName(rsItem.getName()); item.setDescription(rsItem.getDescription()); - - try - { - item.setType(ItemType.valueOf(rsItem.getType().toUpperCase())); - } - catch (IllegalArgumentException ex) - { - logger.warn(null, ex); - } - + item.setType(ItemType.of(rsItem.getType())); item.setIcon(icon); item.setIcon_large(iconLarge); return item; @@ -408,6 +483,20 @@ public class ItemService return fetchJson(request, RSPrices.class); } + private RSSearch fetchRSSearch(String query) throws IOException + { + HttpUrl searchUrl = RS_SEARCH_URL + .newBuilder() + .addQueryParameter("alpha", query) + .build(); + + okhttp3.Request request = new okhttp3.Request.Builder() + .url(searchUrl) + .build(); + + return fetchJson(request, RSSearch.class); + } + private T fetchJson(okhttp3.Request request, Class clazz) throws IOException { okhttp3.Response response = RuneliteAPI.CLIENT.newCall(request).execute(); diff --git a/http-service/src/main/java/net/runelite/http/service/item/RSItem.java b/http-service/src/main/java/net/runelite/http/service/item/RSItem.java index 90ba053786..dcd1e1d490 100644 --- a/http-service/src/main/java/net/runelite/http/service/item/RSItem.java +++ b/http-service/src/main/java/net/runelite/http/service/item/RSItem.java @@ -24,6 +24,9 @@ */ package net.runelite.http.service.item; +import net.runelite.http.api.item.Item; +import net.runelite.http.api.item.ItemType; + public class RSItem { private int id; @@ -92,4 +95,14 @@ public class RSItem { this.icon_large = icon_large; } + + public Item toItem() + { + Item item = new Item(); + item.setId(id); + item.setName(name); + item.setType(ItemType.of(type)); + item.setDescription(description); + return item; + } } diff --git a/http-service/src/main/java/net/runelite/http/service/item/RSSearch.java b/http-service/src/main/java/net/runelite/http/service/item/RSSearch.java new file mode 100644 index 0000000000..ba758daa23 --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/item/RSSearch.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2017, Adam + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.http.service.item; + +import java.util.List; + +public class RSSearch +{ + private List items; + + public List getItems() + { + return items; + } + + public void setItems(List items) + { + this.items = items; + } +}