diff --git a/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/trade/TradeRequest.java b/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/trade/TradeRequest.java new file mode 100644 index 000000000..67157e3f9 --- /dev/null +++ b/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/trade/TradeRequest.java @@ -0,0 +1,25 @@ +package com.gempukku.lotro.trade; + +public class TradeRequest { + private String _from; + private String _to; + private long _createdDate; + + public TradeRequest(String from, String to) { + _from = from; + _to = to; + _createdDate = System.currentTimeMillis(); + } + + public String getFrom() { + return _from; + } + + public String getTo() { + return _to; + } + + public long getCreatedDate() { + return _createdDate; + } +} diff --git a/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/trade/TradeResult.java b/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/trade/TradeResult.java new file mode 100644 index 000000000..2a87bb9a6 --- /dev/null +++ b/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/trade/TradeResult.java @@ -0,0 +1,33 @@ +package com.gempukku.lotro.trade; + +import com.gempukku.lotro.game.CardCollection; + +public class TradeResult { + private String _initiatingPlayer; + private String _joiningPlayer; + private CardCollection _initiatingPlayerItems; + private CardCollection _joiningPlayerItems; + + public TradeResult(String initiatingPlayer, CardCollection initiatingPlayerItems, String joiningPlayer, CardCollection joiningPlayerItems) { + _initiatingPlayer = initiatingPlayer; + _initiatingPlayerItems = initiatingPlayerItems; + _joiningPlayer = joiningPlayer; + _joiningPlayerItems = joiningPlayerItems; + } + + public String getInitiatingPlayer() { + return _initiatingPlayer; + } + + public CardCollection getInitiatingPlayerItems() { + return _initiatingPlayerItems; + } + + public String getJoiningPlayer() { + return _joiningPlayer; + } + + public CardCollection getJoiningPlayerItems() { + return _joiningPlayerItems; + } +} diff --git a/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/trade/TradeServer.java b/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/trade/TradeServer.java new file mode 100644 index 000000000..65edfaafe --- /dev/null +++ b/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/trade/TradeServer.java @@ -0,0 +1,169 @@ +package com.gempukku.lotro.trade; + +import com.gempukku.lotro.AbstractServer; +import com.gempukku.lotro.chat.ChatServer; +import com.gempukku.lotro.game.Player; + +import java.util.*; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +public class TradeServer extends AbstractServer { + private Map _tradeRequestsFrom = new LinkedHashMap(); + private Map> _tradeRequestsTo = new HashMap>(); + + private Set _ongoingTrades = new HashSet(); + private Map _playerOngoingTrade = new HashMap(); + + private ReadWriteLock _tradeLock = new ReentrantReadWriteLock(); + + private static final long TRADE_REQUEST_TIMEOUT_MS = 1000 * 60 * 5; // 5 minutes + private static final long TRADE_TIMEOUT_MS = 1000 * 30; // 30 seconds + + private ChatServer _chatServer; + + public TradeServer(ChatServer chatServer) { + _chatServer = chatServer; + } + + public void sendTradeRequest(Player playerFrom, String playerNameTo) { + _tradeLock.writeLock().lock(); + try { + if (noOngoingActivity(playerFrom)) { + createTradeRequest(playerFrom, playerNameTo); + + } + } finally { + _tradeLock.writeLock().unlock(); + } + } + + public void cancelTradeRequest(Player playerFrom) { + _tradeLock.writeLock().lock(); + try { + final TradeRequest tradeRequest = _tradeRequestsFrom.get(playerFrom.getName()); + if (tradeRequest != null) { + destroyTradeRequest(tradeRequest); + } + } finally { + _tradeLock.writeLock().unlock(); + } + } + + public void acceptTradeRequest(Player player, String requestFrom) { + _tradeLock.writeLock().lock(); + try { + if (noOngoingActivity(player)) { + final TradeRequest tradeRequest = _tradeRequestsFrom.get(requestFrom); + if (tradeRequest != null + && tradeRequest.getTo().equals(player.getName())) { + destroyTradeRequest(tradeRequest); + + createNewOngoingTrade(tradeRequest.getFrom(), tradeRequest.getTo()); + } + } + } finally { + _tradeLock.writeLock().unlock(); + } + } + + public TradeState getOngoingTrade(Player player) { + _tradeLock.readLock().lock(); + try { + return _playerOngoingTrade.get(player.getName()); + } finally { + _tradeLock.readLock().unlock(); + } + } + + public void processTradeVisitor(Player player, TradesVisitor tradesVisitor) { + _tradeLock.readLock().lock(); + try { + final TradeRequest tradeRequest = _tradeRequestsFrom.get(player.getName()); + if (tradeRequest != null) + tradesVisitor.processPendingTradeRequestSent(tradeRequest.getTo()); + + final List incomingTradeRequests = _tradeRequestsTo.get(player.getName()); + if (incomingTradeRequests != null) { + for (TradeRequest incomingTradeRequest : incomingTradeRequests) + tradesVisitor.processPendingTradeRequestIncoming(incomingTradeRequest.getFrom()); + } + + final TradeState ongoingTrade = _playerOngoingTrade.get(player.getName()); + if (ongoingTrade != null) + tradesVisitor.processOngoingTrade(ongoingTrade, getChatRoomName(ongoingTrade.getInitiatingPlayer(), ongoingTrade.getJoiningPlayer())); + } finally { + _tradeLock.readLock().unlock(); + } + } + + private boolean noOngoingActivity(Player player) { + return (!_tradeRequestsFrom.containsKey(player.getName()) + && !_playerOngoingTrade.containsKey(player.getName())); + } + + private void createTradeRequest(Player playerFrom, String playerNameTo) { + // Create new trade request + final TradeRequest newTradeRequest = new TradeRequest(playerFrom.getName(), playerNameTo); + // Add it to outgoing + _tradeRequestsFrom.put(playerFrom.getName(), newTradeRequest); + // Add it to incoming + List incomingTradeRequests = _tradeRequestsTo.get(playerNameTo); + if (incomingTradeRequests == null) { + incomingTradeRequests = new LinkedList(); + _tradeRequestsTo.put(playerNameTo, incomingTradeRequests); + } + incomingTradeRequests.add(newTradeRequest); + } + + private void destroyTradeRequest(TradeRequest tradeRequest) { + final String tradeFrom = tradeRequest.getFrom(); + final String tradeTo = tradeRequest.getTo(); + + _tradeRequestsFrom.remove(tradeFrom); + _tradeRequestsTo.get(tradeTo).remove(tradeRequest); + if (_tradeRequestsTo.get(tradeTo).size() == 0) + _tradeRequestsTo.remove(tradeTo); + } + + private void createNewOngoingTrade(String from, String to) { + TradeState tradeState = new TradeState(from, to); + _ongoingTrades.add(tradeState); + _playerOngoingTrade.put(from, tradeState); + _playerOngoingTrade.put(to, tradeState); + + _chatServer.createChatRoom(getChatRoomName(from, to), 30); + } + + private void destroyOngoingTrade(TradeState tradeState) { + _ongoingTrades.remove(tradeState); + _playerOngoingTrade.remove(tradeState.getInitiatingPlayer()); + _playerOngoingTrade.remove(tradeState.getJoiningPlayer()); + + _chatServer.destroyChatRoom(getChatRoomName(tradeState.getInitiatingPlayer(), tradeState.getJoiningPlayer())); + } + + private String getChatRoomName(String from, String to) { + return "Trade - " + from + " and " + to; + } + + @Override + protected void cleanup() { + _tradeLock.writeLock().lock(); + try { + long now = System.currentTimeMillis(); + for (TradeRequest tradeRequest : new ArrayList(_tradeRequestsFrom.values())) { + if (tradeRequest.getCreatedDate() < now + TRADE_REQUEST_TIMEOUT_MS) + destroyTradeRequest(tradeRequest); + } + + for (TradeState tradeState : new HashSet(_ongoingTrades)) { + if (tradeState.isTradeFinished() || tradeState.getLastActivity() < now + TRADE_TIMEOUT_MS) + destroyOngoingTrade(tradeState); + } + + } finally { + _tradeLock.writeLock().unlock(); + } + } +} diff --git a/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/trade/TradeState.java b/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/trade/TradeState.java new file mode 100644 index 000000000..6e6d0b955 --- /dev/null +++ b/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/trade/TradeState.java @@ -0,0 +1,144 @@ +package com.gempukku.lotro.trade; + +import com.gempukku.lotro.game.DefaultCardCollection; +import com.gempukku.lotro.game.MutableCardCollection; + +public class TradeState { + private String _initiatingPlayer; + private String _joiningPlayer; + + private MutableCardCollection _initiatingPlayerItems = new DefaultCardCollection(); + private MutableCardCollection _joiningPlayerItems = new DefaultCardCollection(); + + private boolean _initiatingPlayerAgreed; + private boolean _joiningPlayerAgreed; + + private int _tradeState; + + private int _initiatingPlayerConfirmedState = -1; + private int _joiningPlayerConfirmedState = -1; + + private boolean _tradeFinished; + + private long _lastActivity; + + public TradeState(String initiatingPlayer, String joiningPlayer) { + _initiatingPlayer = initiatingPlayer; + _joiningPlayer = joiningPlayer; + } + + public synchronized void addItemsWanted(String player, String item, int count) { + if (player.equals(_initiatingPlayer)) + adjustJoiningItems(item, count); + else if (player.equals(_joiningPlayer)) + adjustInitiatingItems(item, count); + } + + public synchronized void removeItemsWanted(String player, String item, int count) { + if (player.equals(_initiatingPlayer)) + adjustJoiningItems(item, -count); + else if (player.equals(_joiningPlayer)) + adjustInitiatingItems(item, -count); + } + + public synchronized void addItemsOffered(String player, String item, int count) { + if (player.equals(_initiatingPlayer)) + adjustInitiatingItems(item, count); + else if (player.equals(_joiningPlayer)) + adjustJoiningItems(item, count); + } + + public synchronized void removeItemsOffered(String player, String item, int count) { + if (player.equals(_initiatingPlayer)) + adjustInitiatingItems(item, -count); + else if (player.equals(_joiningPlayer)) + adjustJoiningItems(item, -count); + } + + public synchronized void playerAgreed(String player) { + if (!_tradeFinished) { + if (player.equals(_initiatingPlayer)) + _initiatingPlayerAgreed = true; + else if (player.equals(_joiningPlayer)) + _joiningPlayerAgreed = true; + + if (_initiatingPlayerAgreed && _joiningPlayerAgreed) + agreementStateReached(); + } + } + + public synchronized TradeResult playerConfirmed(String player, int tradeState) { + if (!_tradeFinished) { + if (player.equals(_initiatingPlayer)) + _initiatingPlayerConfirmedState = tradeState; + else if (player.equals(_joiningPlayer)) + _joiningPlayerConfirmedState = tradeState; + + if (_initiatingPlayerConfirmedState == _joiningPlayerConfirmedState + && _initiatingPlayerConfirmedState != -1) { + _tradeFinished = true; + return new TradeResult(_initiatingPlayer, _initiatingPlayerItems, + _joiningPlayer, _joiningPlayerItems); + } + } + + return null; + } + + public synchronized void processTradeStateVisitor(String player, TradeStateVisitor tradeStateVisitor) { + if (player.equals(_initiatingPlayer)) + tradeStateVisitor.processTradeState( + _joiningPlayer, + new DefaultCardCollection(_initiatingPlayerItems), new DefaultCardCollection(_joiningPlayerItems), + _initiatingPlayerAgreed, _joiningPlayerAgreed, _tradeState, + _initiatingPlayerConfirmedState == _tradeState, _joiningPlayerConfirmedState == _tradeState); + else if (player.equals(_joiningPlayer)) + tradeStateVisitor.processTradeState( + _initiatingPlayer, + new DefaultCardCollection(_joiningPlayerItems), new DefaultCardCollection(_initiatingPlayerItems), + _joiningPlayerAgreed, _initiatingPlayerAgreed, _tradeState, + _joiningPlayerConfirmedState == _tradeState, _initiatingPlayerConfirmedState == _tradeState); + + _lastActivity = System.currentTimeMillis(); + } + + public String getInitiatingPlayer() { + return _initiatingPlayer; + } + + public String getJoiningPlayer() { + return _joiningPlayer; + } + + public boolean isTradeFinished() { + return _tradeFinished; + } + + public long getLastActivity() { + return _lastActivity; + } + + private void agreementStateReached() { + _tradeState++; + } + + private void adjustJoiningItems(String item, int diff) { + adjustItems(_joiningPlayerItems, item, diff); + } + + private void adjustInitiatingItems(String item, int diff) { + adjustItems(_initiatingPlayerItems, item, diff); + } + + private void adjustItems(MutableCardCollection collection, String item, int diff) { + if (!_tradeFinished) { + if (diff < 0) + collection.removeItem(item, -diff); + else + collection.addItem(item, diff); + + _initiatingPlayerAgreed = false; + _joiningPlayerAgreed = false; + } + } +} diff --git a/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/trade/TradeStateVisitor.java b/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/trade/TradeStateVisitor.java new file mode 100644 index 000000000..388b229d1 --- /dev/null +++ b/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/trade/TradeStateVisitor.java @@ -0,0 +1,9 @@ +package com.gempukku.lotro.trade; + +import com.gempukku.lotro.game.CardCollection; + +public interface TradeStateVisitor { + public void processTradeState(String otherParty, CardCollection offering, CardCollection getting, + boolean selfAccepted, boolean otherAccepted, int tradeState, + boolean selfConfirmed, boolean otherConfirmed); +} diff --git a/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/trade/TradesVisitor.java b/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/trade/TradesVisitor.java new file mode 100644 index 000000000..3f5924288 --- /dev/null +++ b/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/trade/TradesVisitor.java @@ -0,0 +1,9 @@ +package com.gempukku.lotro.trade; + +public interface TradesVisitor { + public void processPendingTradeRequestSent(String playerTo); + + public void processPendingTradeRequestIncoming(String playerFrom); + + public void processOngoingTrade(TradeState tradeState, String chatRoomName); +} diff --git a/gemp-lotr/gemp-lotr-web/src/main/java/com/gempukku/lotro/server/ServerProvider.java b/gemp-lotr/gemp-lotr-web/src/main/java/com/gempukku/lotro/server/ServerProvider.java index 9b4da8ccb..46de688d6 100644 --- a/gemp-lotr/gemp-lotr-web/src/main/java/com/gempukku/lotro/server/ServerProvider.java +++ b/gemp-lotr/gemp-lotr-web/src/main/java/com/gempukku/lotro/server/ServerProvider.java @@ -7,6 +7,7 @@ import com.gempukku.lotro.game.LotroCardBlueprintLibrary; import com.gempukku.lotro.game.LotroServer; import com.gempukku.lotro.hall.HallServer; import com.gempukku.lotro.league.LeagueService; +import com.gempukku.lotro.trade.TradeServer; import com.sun.jersey.core.spi.component.ComponentContext; import com.sun.jersey.core.spi.component.ComponentScope; import com.sun.jersey.spi.inject.Injectable; @@ -22,6 +23,7 @@ public class ServerProvider implements InjectableProvider { private Injectable _leagueServerInjectable; private Injectable _hallServerInjectable; private Injectable _lotroServerInjectable; + private Injectable _tradeServerInjectable; private Injectable _collectionsManagerInjectable; @Context @@ -55,6 +57,8 @@ public class ServerProvider implements InjectableProvider { return getLeagueServiceInjectable(); if (type.equals(CollectionsManager.class)) return getCollectionsManagerInjectable(); + if (type.equals(TradeServer.class)) + return getTradeServerInjectable(); return null; } @@ -85,6 +89,20 @@ public class ServerProvider implements InjectableProvider { return _hallServerInjectable; } + private synchronized Injectable getTradeServerInjectable() { + if (_tradeServerInjectable == null) { + final TradeServer tradeServer = new TradeServer(getChatServerInjectable().getValue()); + tradeServer.startServer(); + _tradeServerInjectable = new Injectable() { + @Override + public TradeServer getValue() { + return tradeServer; + } + }; + } + return _tradeServerInjectable; + } + private synchronized Injectable getLotroServerInjectable() { if (_lotroServerInjectable == null) { final LotroServer lotroServer = new LotroServer(_deckDao, _gameHistoryDao, _library, getChatServerInjectable().getValue(), false); diff --git a/gemp-lotr/gemp-lotr-web/src/main/java/com/gempukku/lotro/server/TradeResource.java b/gemp-lotr/gemp-lotr-web/src/main/java/com/gempukku/lotro/server/TradeResource.java new file mode 100644 index 000000000..8417b9563 --- /dev/null +++ b/gemp-lotr/gemp-lotr-web/src/main/java/com/gempukku/lotro/server/TradeResource.java @@ -0,0 +1,110 @@ +package com.gempukku.lotro.server; + +import com.gempukku.lotro.game.CardCollection; +import com.gempukku.lotro.game.LotroCardBlueprintLibrary; +import com.gempukku.lotro.game.Player; +import com.gempukku.lotro.trade.TradeServer; +import com.gempukku.lotro.trade.TradeState; +import com.gempukku.lotro.trade.TradeStateVisitor; +import com.sun.jersey.spi.resource.Singleton; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import java.util.Map; + +@Singleton +@Path("/trade") +public class TradeResource extends AbstractResource { + @Context + private TradeServer _tradeServer; + @Context + private LotroCardBlueprintLibrary _library; + + @GET + @Produces(MediaType.APPLICATION_XML) + public Document getOngoingTrade(@QueryParam("participantId") String participantId, + @Context HttpServletRequest request, + @Context HttpServletResponse response) throws ParserConfigurationException { + Player resourceOwner = getResourceOwnerSafely(request, participantId); + + DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); + DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder(); + + Document doc = documentBuilder.newDocument(); + + Element trade = doc.createElement("trade"); + + final TradeState ongoingTrade = _tradeServer.getOngoingTrade(resourceOwner); + + if (ongoingTrade != null) + ongoingTrade.processTradeStateVisitor(resourceOwner.getName(), new OngoingTradeVisitor(doc, trade)); + + doc.appendChild(trade); + + processDeliveryServiceNotification(request, response); + + return doc; + } + + + private class OngoingTradeVisitor implements TradeStateVisitor { + private Document _doc; + private Element _root; + + private OngoingTradeVisitor(Document doc, Element root) { + _doc = doc; + _root = root; + } + + @Override + public void processTradeState(String otherParty, CardCollection offering, CardCollection getting, boolean selfAccepted, boolean otherAccepted, int tradeState, boolean selfConfirmed, boolean otherConfirmed) { + _root.setAttribute("otherParty", otherParty); + if (selfAccepted) + _root.setAttribute("selfAccepted", "true"); + if (otherAccepted) + _root.setAttribute("otherAccepted", "true"); + if (selfAccepted && otherAccepted) + _root.setAttribute("tradeState", String.valueOf(tradeState)); + if (selfConfirmed) + _root.setAttribute("selfConfirmed", "true"); + if (otherConfirmed) + _root.setAttribute("otherConfirmed", "true"); + + final Element offer = _doc.createElement("offer"); + appendItems(_doc, offer, offering); + _root.appendChild(offer); + Element get = _doc.createElement("get"); + appendItems(_doc, get, getting); + _root.appendChild(get); + } + + private void appendItems(Document doc, Element elem, CardCollection collection) { + final Map items = collection.getAll(); + for (Map.Entry itemCount : items.entrySet()) { + String blueprintId = itemCount.getKey(); + if (blueprintId.contains("_")) { + Element card = doc.createElement("card"); + card.setAttribute("count", String.valueOf(itemCount.getValue())); + card.setAttribute("blueprintId", blueprintId); + elem.appendChild(card); + } else { + Element pack = doc.createElement("pack"); + pack.setAttribute("count", String.valueOf(itemCount.getValue())); + pack.setAttribute("blueprintId", blueprintId); + elem.appendChild(pack); + } + } + } + } +}