Merge pull request #1035 from Owain94/http-api-rx-calls

http-api: rx calls
This commit is contained in:
Ganom
2019-07-18 23:23:02 -04:00
committed by GitHub
10 changed files with 262 additions and 230 deletions

View File

@@ -26,6 +26,7 @@ package net.runelite.http.api.item;
import com.google.gson.JsonParseException; import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken; import com.google.gson.reflect.TypeToken;
import io.reactivex.Observable;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@@ -112,7 +113,7 @@ public class ItemClient
} }
} }
public BufferedImage getIcon(int itemId) throws IOException public Observable<BufferedImage> getIcon(int itemId)
{ {
HttpUrl url = RuneLiteAPI.getApiBase().newBuilder() HttpUrl url = RuneLiteAPI.getApiBase().newBuilder()
.addPathSegment("item") .addPathSegment("item")
@@ -126,23 +127,26 @@ public class ItemClient
.url(url) .url(url)
.build(); .build();
try (Response response = RuneLiteAPI.CLIENT.newCall(request).execute()) return Observable.defer(() ->
{ {
if (!response.isSuccessful()) try (Response response = RuneLiteAPI.CLIENT.newCall(request).execute())
{ {
logger.debug("Error grabbing icon {}: {}", itemId, response); if (!response.isSuccessful())
return null; {
} logger.debug("Error grabbing icon {}: {}", itemId, response);
return Observable.just(null);
}
InputStream in = response.body().byteStream(); InputStream in = response.body().byteStream();
synchronized (ImageIO.class) synchronized (ImageIO.class)
{ {
return ImageIO.read(in); return Observable.just(ImageIO.read(in));
}
} }
} });
} }
public SearchResult search(String itemName) throws IOException public Observable<SearchResult> search(String itemName)
{ {
HttpUrl url = RuneLiteAPI.getApiBase().newBuilder() HttpUrl url = RuneLiteAPI.getApiBase().newBuilder()
.addPathSegment("item") .addPathSegment("item")
@@ -152,28 +156,31 @@ public class ItemClient
logger.debug("Built URI: {}", url); logger.debug("Built URI: {}", url);
Request request = new Request.Builder() return Observable.defer(() ->
.url(url)
.build();
try (Response response = RuneLiteAPI.CLIENT.newCall(request).execute())
{ {
if (!response.isSuccessful()) Request request = new Request.Builder()
.url(url)
.build();
try (Response response = RuneLiteAPI.CLIENT.newCall(request).execute())
{ {
logger.debug("Error looking up item {}: {}", itemName, response); if (!response.isSuccessful())
return null; {
} logger.debug("Error looking up item {}: {}", itemName, response);
return Observable.just(null);
}
InputStream in = response.body().byteStream(); InputStream in = response.body().byteStream();
return RuneLiteAPI.GSON.fromJson(new InputStreamReader(in), SearchResult.class); return Observable.just(RuneLiteAPI.GSON.fromJson(new InputStreamReader(in), SearchResult.class));
} }
catch (JsonParseException ex) catch (JsonParseException ex)
{ {
throw new IOException(ex); return Observable.error(ex);
} }
});
} }
public ItemPrice[] getPrices() throws IOException public Observable<ItemPrice[]> getPrices()
{ {
HttpUrl.Builder urlBuilder = RuneLiteAPI.getApiBase().newBuilder() HttpUrl.Builder urlBuilder = RuneLiteAPI.getApiBase().newBuilder()
.addPathSegment("item") .addPathSegment("item")
@@ -183,28 +190,32 @@ public class ItemClient
logger.debug("Built URI: {}", url); logger.debug("Built URI: {}", url);
Request request = new Request.Builder()
.url(url)
.build();
try (Response response = RuneLiteAPI.CLIENT.newCall(request).execute()) return Observable.defer(() ->
{ {
if (!response.isSuccessful()) Request request = new Request.Builder()
.url(url)
.build();
try (Response response = RuneLiteAPI.CLIENT.newCall(request).execute())
{ {
logger.warn("Error looking up prices: {}", response); if (!response.isSuccessful())
return null; {
} logger.warn("Error looking up prices: {}", response);
return Observable.just(null);
}
InputStream in = response.body().byteStream(); InputStream in = response.body().byteStream();
return RuneLiteAPI.GSON.fromJson(new InputStreamReader(in), ItemPrice[].class); return Observable.just(RuneLiteAPI.GSON.fromJson(new InputStreamReader(in), ItemPrice[].class));
} }
catch (JsonParseException ex) catch (JsonParseException ex)
{ {
throw new IOException(ex); return Observable.error(ex);
} }
});
} }
public Map<Integer, ItemStats> getStats() throws IOException public Observable<Map<Integer, ItemStats>> getStats()
{ {
HttpUrl.Builder urlBuilder = RuneLiteAPI.getStaticBase().newBuilder() HttpUrl.Builder urlBuilder = RuneLiteAPI.getStaticBase().newBuilder()
.addPathSegment("item") .addPathSegment("item")
@@ -215,27 +226,31 @@ public class ItemClient
logger.debug("Built URI: {}", url); logger.debug("Built URI: {}", url);
Request request = new Request.Builder()
.url(url)
.build();
try (Response response = RuneLiteAPI.CLIENT.newCall(request).execute()) return Observable.defer(() ->
{ {
if (!response.isSuccessful()) Request request = new Request.Builder()
.url(url)
.build();
try (Response response = RuneLiteAPI.CLIENT.newCall(request).execute())
{ {
logger.warn("Error looking up item stats: {}", response); if (!response.isSuccessful())
return null; {
logger.warn("Error looking up item stats: {}", response);
return Observable.just(null);
}
InputStream in = response.body().byteStream();
final Type typeToken = new TypeToken<Map<Integer, ItemStats>>()
{
}.getType();
return Observable.just(RuneLiteAPI.GSON.fromJson(new InputStreamReader(in), typeToken));
} }
catch (JsonParseException ex)
InputStream in = response.body().byteStream();
final Type typeToken = new TypeToken<Map<Integer, ItemStats>>()
{ {
}.getType(); return Observable.error(ex);
return RuneLiteAPI.GSON.fromJson(new InputStreamReader(in), typeToken); }
} });
catch (JsonParseException ex)
{
throw new IOException(ex);
}
} }
} }

View File

@@ -58,7 +58,7 @@ public class OSBGrandExchangeClient
{ {
if (!response.isSuccessful()) if (!response.isSuccessful())
{ {
throw new IOException("Error looking up item id: " + response); return Observable.error(new IOException("Error looking up item id: " + response));
} }
final InputStream in = response.body().byteStream(); final InputStream in = response.body().byteStream();

View File

@@ -26,6 +26,9 @@
package net.runelite.http.api.worlds; package net.runelite.http.api.worlds;
import com.google.gson.JsonParseException; import com.google.gson.JsonParseException;
import io.reactivex.Observable;
import java.io.InputStream;
import java.io.InputStreamReader;
import net.runelite.http.api.RuneLiteAPI; import net.runelite.http.api.RuneLiteAPI;
import okhttp3.HttpUrl; import okhttp3.HttpUrl;
import okhttp3.Request; import okhttp3.Request;
@@ -33,15 +36,11 @@ import okhttp3.Response;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
public class WorldClient public class WorldClient
{ {
private static final Logger logger = LoggerFactory.getLogger(WorldClient.class); private static final Logger logger = LoggerFactory.getLogger(WorldClient.class);
public WorldResult lookupWorlds() throws IOException public Observable<WorldResult> lookupWorlds()
{ {
HttpUrl url = RuneLiteAPI.getApiBase().newBuilder() HttpUrl url = RuneLiteAPI.getApiBase().newBuilder()
.addPathSegment("worlds.js") .addPathSegment("worlds.js")
@@ -49,24 +48,27 @@ public class WorldClient
logger.debug("Built URI: {}", url); logger.debug("Built URI: {}", url);
Request request = new Request.Builder() return Observable.defer(() ->
.url(url)
.build();
try (Response response = RuneLiteAPI.CLIENT.newCall(request).execute())
{ {
if (!response.isSuccessful()) Request request = new Request.Builder()
.url(url)
.build();
try (Response response = RuneLiteAPI.CLIENT.newCall(request).execute())
{ {
logger.debug("Error looking up worlds: {}", response); if (!response.isSuccessful())
return null; {
} logger.debug("Error looking up worlds: {}", response);
return Observable.just(null);
}
InputStream in = response.body().byteStream(); InputStream in = response.body().byteStream();
return RuneLiteAPI.GSON.fromJson(new InputStreamReader(in), WorldResult.class); return Observable.just(RuneLiteAPI.GSON.fromJson(new InputStreamReader(in), WorldResult.class));
} }
catch (JsonParseException ex) catch (JsonParseException ex)
{ {
throw new IOException(ex); return Observable.error(ex);
} }
});
} }
} }

View File

@@ -27,14 +27,16 @@ package net.runelite.client.callback;
import com.google.inject.Inject; import com.google.inject.Inject;
import java.util.Iterator; import java.util.Iterator;
import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Executor;
import java.util.function.BooleanSupplier; import java.util.function.BooleanSupplier;
import javax.inject.Singleton; import javax.inject.Singleton;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import net.runelite.api.Client; import net.runelite.api.Client;
import org.jetbrains.annotations.NotNull;
@Singleton @Singleton
@Slf4j @Slf4j
public class ClientThread public class ClientThread implements Executor
{ {
private final ConcurrentLinkedQueue<BooleanSupplier> invokes = new ConcurrentLinkedQueue<>(); private final ConcurrentLinkedQueue<BooleanSupplier> invokes = new ConcurrentLinkedQueue<>();
@@ -112,4 +114,14 @@ public class ClientThread
} }
} }
} }
@Override
public void execute(@NotNull Runnable r)
{
invoke(() ->
{
r.run();
return true;
});
}
} }

View File

@@ -30,9 +30,9 @@ import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken; import com.google.gson.reflect.TypeToken;
import io.reactivex.schedulers.Schedulers;
import java.awt.Color; import java.awt.Color;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.lang.reflect.Type; import java.lang.reflect.Type;
@@ -142,6 +142,7 @@ public class ItemManager
private final LoadingCache<OutlineKey, BufferedImage> itemOutlines; private final LoadingCache<OutlineKey, BufferedImage> itemOutlines;
private Map<Integer, ItemPrice> itemPrices = Collections.emptyMap(); private Map<Integer, ItemPrice> itemPrices = Collections.emptyMap();
private Map<Integer, ItemStats> itemStats = Collections.emptyMap(); private Map<Integer, ItemStats> itemStats = Collections.emptyMap();
@Inject @Inject
public ItemManager( public ItemManager(
Client client, Client client,
@@ -209,43 +210,43 @@ public class ItemManager
private void loadPrices() private void loadPrices()
{ {
try itemClient.getPrices()
{ .subscribeOn(Schedulers.io())
ItemPrice[] prices = itemClient.getPrices(); .subscribe(
if (prices != null) (prices) ->
{
ImmutableMap.Builder<Integer, ItemPrice> map = ImmutableMap.builderWithExpectedSize(prices.length);
for (ItemPrice price : prices)
{ {
map.put(price.getId(), price); if (prices != null)
} {
itemPrices = map.build(); ImmutableMap.Builder<Integer, ItemPrice> map = ImmutableMap.builderWithExpectedSize(prices.length);
} for (ItemPrice price : prices)
{
map.put(price.getId(), price);
}
itemPrices = map.build();
}
log.debug("Loaded {} prices", itemPrices.size()); log.debug("Loaded {} prices", itemPrices.size());
} },
catch (IOException e) (e) -> log.warn("error loading prices!", e)
{ );
log.warn("error loading prices!", e);
}
} }
private void loadStats() private void loadStats()
{ {
try itemClient.getStats()
{ .subscribeOn(Schedulers.io())
final Map<Integer, ItemStats> stats = itemClient.getStats(); .subscribe(
if (stats != null) (stats) ->
{ {
itemStats = ImmutableMap.copyOf(stats); if (stats != null)
} {
itemStats = ImmutableMap.copyOf(stats);
}
log.debug("Loaded {} stats", itemStats.size()); log.debug("Loaded {} stats", itemStats.size());
} },
catch (IOException e) (e) -> log.warn("error loading stats!", e)
{ );
log.warn("error loading stats!", e);
}
} }
private void onGameStateChanged(final GameStateChanged event) private void onGameStateChanged(final GameStateChanged event)
@@ -285,7 +286,7 @@ public class ItemManager
/** /**
* Look up an item's price * Look up an item's price
* *
* @param itemID item id * @param itemID item id
* @param ignoreUntradeableMap should the price returned ignore the {@link UntradeableItemMapping} * @param ignoreUntradeableMap should the price returned ignore the {@link UntradeableItemMapping}
* @return item price * @return item price
*/ */

View File

@@ -827,45 +827,45 @@ public class ChatCommandsPlugin extends Plugin
{ {
ItemPrice item = retrieveFromList(results, search); ItemPrice item = retrieveFromList(results, search);
CLIENT.lookupItem(item.getId()) CLIENT.lookupItem(item.getId())
.subscribeOn(Schedulers.single()) .subscribeOn(Schedulers.io())
.observeOn(Schedulers.from(clientThread))
.subscribe( .subscribe(
(osbresult) -> (osbresult) ->
clientThread.invoke(() -> {
int itemId = item.getId();
int itemPrice = itemManager.getItemPrice(itemId);
final ChatMessageBuilder builder = new ChatMessageBuilder();
builder.append(ChatColorType.NORMAL);
builder.append(ChatColorType.HIGHLIGHT);
builder.append(item.getName());
builder.append(ChatColorType.NORMAL);
builder.append(": GE ");
builder.append(ChatColorType.HIGHLIGHT);
builder.append(StackFormatter.formatNumber(itemPrice));
builder.append(ChatColorType.NORMAL);
builder.append(": OSB ");
builder.append(ChatColorType.HIGHLIGHT);
builder.append(StackFormatter.formatNumber(osbresult.getOverall_average()));
ItemDefinition itemComposition = itemManager.getItemDefinition(itemId);
if (itemComposition != null)
{ {
int itemId = item.getId(); int alchPrice = itemManager.getAlchValue(itemId);
int itemPrice = itemManager.getItemPrice(itemId); builder
.append(ChatColorType.NORMAL)
.append(" HA value ")
.append(ChatColorType.HIGHLIGHT)
.append(StackFormatter.formatNumber(alchPrice));
}
final ChatMessageBuilder builder = new ChatMessageBuilder(); String response = builder.build();
builder.append(ChatColorType.NORMAL);
builder.append(ChatColorType.HIGHLIGHT);
builder.append(item.getName());
builder.append(ChatColorType.NORMAL);
builder.append(": GE ");
builder.append(ChatColorType.HIGHLIGHT);
builder.append(StackFormatter.formatNumber(itemPrice));
builder.append(ChatColorType.NORMAL);
builder.append(": OSB ");
builder.append(ChatColorType.HIGHLIGHT);
builder.append(StackFormatter.formatNumber(osbresult.getOverall_average()));
ItemDefinition itemComposition = itemManager.getItemDefinition(itemId); log.debug("Setting response {}", response);
if (itemComposition != null) messageNode.setRuneLiteFormatMessage(response);
{ chatMessageManager.update(messageNode);
int alchPrice = itemManager.getAlchValue(itemId); client.refreshChat();
builder }
.append(ChatColorType.NORMAL)
.append(" HA value ")
.append(ChatColorType.HIGHLIGHT)
.append(StackFormatter.formatNumber(alchPrice));
}
String response = builder.build();
log.debug("Setting response {}", response);
messageNode.setRuneLiteFormatMessage(response);
chatMessageManager.update(messageNode);
client.refreshChat();
})
); );
} }
} }

View File

@@ -25,13 +25,14 @@
package net.runelite.client.plugins.defaultworld; package net.runelite.client.plugins.defaultworld;
import com.google.inject.Provides; import com.google.inject.Provides;
import java.io.IOException; import io.reactivex.schedulers.Schedulers;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import net.runelite.api.Client; import net.runelite.api.Client;
import net.runelite.api.GameState; import net.runelite.api.GameState;
import net.runelite.api.events.GameStateChanged; import net.runelite.api.events.GameStateChanged;
import net.runelite.client.callback.ClientThread;
import net.runelite.client.config.ConfigManager; import net.runelite.client.config.ConfigManager;
import net.runelite.client.eventbus.EventBus; import net.runelite.client.eventbus.EventBus;
import net.runelite.client.events.SessionOpen; import net.runelite.client.events.SessionOpen;
@@ -40,7 +41,6 @@ import net.runelite.client.plugins.PluginDescriptor;
import net.runelite.client.util.WorldUtil; import net.runelite.client.util.WorldUtil;
import net.runelite.http.api.worlds.World; import net.runelite.http.api.worlds.World;
import net.runelite.http.api.worlds.WorldClient; import net.runelite.http.api.worlds.WorldClient;
import net.runelite.http.api.worlds.WorldResult;
@PluginDescriptor( @PluginDescriptor(
name = "Default World", name = "Default World",
@@ -60,6 +60,9 @@ public class DefaultWorldPlugin extends Plugin
@Inject @Inject
private EventBus eventBus; private EventBus eventBus;
@Inject
private ClientThread clientThread;
private final WorldClient worldClient = new WorldClient(); private final WorldClient worldClient = new WorldClient();
private int worldCache; private int worldCache;
private boolean worldChangeRequired; private boolean worldChangeRequired;
@@ -122,39 +125,39 @@ public class DefaultWorldPlugin extends Plugin
return; return;
} }
try worldClient.lookupWorlds()
{ .subscribeOn(Schedulers.io())
final WorldResult worldResult = worldClient.lookupWorlds(); .observeOn(Schedulers.from(clientThread))
.subscribe(
(worldResult) ->
{
if (worldResult == null)
{
return;
}
if (worldResult == null) final World world = worldResult.findWorld(correctedWorld);
{
return;
}
final World world = worldResult.findWorld(correctedWorld); if (world != null)
{
final net.runelite.api.World rsWorld = client.createWorld();
rsWorld.setActivity(world.getActivity());
rsWorld.setAddress(world.getAddress());
rsWorld.setId(world.getId());
rsWorld.setPlayerCount(world.getPlayers());
rsWorld.setLocation(world.getLocation());
rsWorld.setTypes(WorldUtil.toWorldTypes(world.getTypes()));
if (world != null) client.changeWorld(rsWorld);
{ log.debug("Applied new world {}", correctedWorld);
final net.runelite.api.World rsWorld = client.createWorld(); }
rsWorld.setActivity(world.getActivity()); else
rsWorld.setAddress(world.getAddress()); {
rsWorld.setId(world.getId()); log.warn("World {} not found.", correctedWorld);
rsWorld.setPlayerCount(world.getPlayers()); }
rsWorld.setLocation(world.getLocation()); },
rsWorld.setTypes(WorldUtil.toWorldTypes(world.getTypes())); (e) -> log.warn("Error looking up world {}. Error: {}", correctedWorld, e)
);
client.changeWorld(rsWorld);
log.debug("Applied new world {}", correctedWorld);
}
else
{
log.warn("World {} not found.", correctedWorld);
}
}
catch (IOException e)
{
log.warn("Error looking up world {}. Error: {}", correctedWorld, e);
}
} }
private void applyWorld() private void applyWorld()

View File

@@ -370,37 +370,37 @@ public class ExaminePlugin extends Plugin
{ {
int finalQuantity = quantity; int finalQuantity = quantity;
CLIENT.lookupItem(id) CLIENT.lookupItem(id)
.subscribeOn(Schedulers.single()) .subscribeOn(Schedulers.io())
.observeOn(Schedulers.from(clientThread))
.subscribe( .subscribe(
(osbresult) -> (osbresult) ->
clientThread.invoke(() -> {
message
.append(ChatColorType.NORMAL)
.append(" GE ")
.append(ChatColorType.HIGHLIGHT)
.append(StackFormatter.formatNumber(gePrice * finalQuantity));
if (osbresult != null)
{ {
message message
.append(ChatColorType.NORMAL) .append(ChatColorType.NORMAL)
.append(" GE ") .append(" OSB ")
.append(ChatColorType.HIGHLIGHT) .append(ChatColorType.HIGHLIGHT)
.append(StackFormatter.formatNumber(gePrice * finalQuantity)); .append(StackFormatter.formatNumber(osbresult.getOverall_average() * finalQuantity));
}
if (osbresult != null) if (finalQuantity > 1)
{ {
message message
.append(ChatColorType.NORMAL) .append(ChatColorType.NORMAL)
.append(" OSB ") .append(" (")
.append(ChatColorType.HIGHLIGHT) .append(ChatColorType.HIGHLIGHT)
.append(StackFormatter.formatNumber(osbresult.getOverall_average() * finalQuantity)); .append(StackFormatter.formatNumber(gePrice))
} .append(ChatColorType.NORMAL)
.append("ea)");
if (finalQuantity > 1) }
{ },
message
.append(ChatColorType.NORMAL)
.append(" (")
.append(ChatColorType.HIGHLIGHT)
.append(StackFormatter.formatNumber(gePrice))
.append(ChatColorType.NORMAL)
.append("ea)");
}
}),
(e) -> log.error(e.toString()) (e) -> log.error(e.toString())
); );
} }

View File

@@ -551,14 +551,14 @@ public class GrandExchangePlugin extends Plugin
} }
CLIENT.lookupItem(itemId) CLIENT.lookupItem(itemId)
.subscribeOn(Schedulers.single()) .subscribeOn(Schedulers.io())
.observeOn(Schedulers.from(clientThread))
.subscribe( .subscribe(
(osbresult) -> (osbresult) ->
clientThread.invoke(() -> {
{ final String text = geText.getText() + OSB_GE_TEXT + StackFormatter.formatNumber(osbresult.getOverall_average());
final String text = geText.getText() + OSB_GE_TEXT + StackFormatter.formatNumber(osbresult.getOverall_average()); geText.setText(text);
geText.setText(text); },
}),
(e) -> log.debug("Error getting price of item {}", itemId, e) (e) -> log.debug("Error getting price of item {}", itemId, e)
); );
}); });

View File

@@ -29,8 +29,8 @@ import com.google.common.base.Stopwatch;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.ObjectArrays; import com.google.common.collect.ObjectArrays;
import com.google.inject.Provides; import com.google.inject.Provides;
import io.reactivex.schedulers.Schedulers;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.IOException;
import java.time.Duration; import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import java.util.Comparator; import java.util.Comparator;
@@ -502,22 +502,21 @@ public class WorldHopperPlugin extends Plugin
{ {
log.debug("Fetching worlds"); log.debug("Fetching worlds");
try new WorldClient().lookupWorlds()
{ .subscribeOn(Schedulers.io())
WorldResult worldResult = new WorldClient().lookupWorlds(); .subscribe(
(worldResult) ->
if (worldResult != null) {
{ if (worldResult != null)
worldResult.getWorlds().sort(Comparator.comparingInt(World::getId)); {
this.worldResult = worldResult; worldResult.getWorlds().sort(Comparator.comparingInt(World::getId));
this.lastFetch = Instant.now(); this.worldResult = worldResult;
updateList(); this.lastFetch = Instant.now();
} updateList();
} }
catch (IOException ex) },
{ (ex) -> log.warn("Error looking up worlds", ex)
log.warn("Error looking up worlds", ex); );
}
} }
/** /**