From e1cffbde4069f7c94c43d32c6113aa83c0aebddc Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Wed, 31 Jul 2024 16:24:59 +0400 Subject: [PATCH] download: reworked connection: - added shareable code with default proxy, headers and other settings for download tasks like images, symbols, mtgjson, etc; - use XmageURLConnection.downloadText for text resources - use XmageURLConnection.downloadBinary for any file resources - added user agent with app version for all requests; - added http logs and improved error messages; --- .../src/main/java/mage/client/MageFrame.java | 4 +- .../client/deckeditor/DeckEditorPanel.java | 4 +- .../java/mage/client/game/FeedbackPanel.java | 4 +- .../client/remote/XmageURLConnection.java | 294 ++++++++++++++++++ .../org/mage/card/arcane/ManaSymbols.java | 50 +-- .../org/mage/plugins/card/CardPluginImpl.java | 36 +-- .../org/mage/plugins/card/dl/DownloadJob.java | 107 +------ .../plugins/card/dl/DownloadServiceInfo.java | 2 - .../org/mage/plugins/card/dl/Downloader.java | 117 +++---- .../card/dl/sources/CardImageSource.java | 7 +- .../dl/sources/DirectLinksForDownload.java | 5 +- .../plugins/card/dl/sources/GathererSets.java | 15 +- .../card/dl/sources/GathererSymbols.java | 11 +- .../card/dl/sources/ScryfallImageSource.java | 48 ++- .../dl/sources/ScryfallSymbolsSource.java | 26 +- .../dl/sources/WizardCardsImageSource.java | 15 +- .../card/images/DownloadPicturesService.java | 194 +++++------- .../plugins/card/utils/CardImageUtils.java | 28 +- .../java/mage/client/util/DownloaderTest.java | 48 +++ .../src/main/java/mage/utils/MageVersion.java | 4 + .../mage/server/console/ConsoleFrame.java | 4 +- .../src/mage/player/ai/ComputerPlayer6.java | 4 +- .../mage/player/ai/ComputerPlayerMCTS.java | 4 +- .../java/mage/server/UserManagerImpl.java | 6 +- .../java/mage/server/game/GameController.java | 4 +- .../java/mage/server/game/GamesRoomImpl.java | 4 +- .../mage/server/util/ServerMessagesUtil.java | 4 +- .../mage/server/util/ThreadExecutorImpl.java | 14 +- .../java/org/mage/test/load/LoadTest.java | 6 +- .../java/mage/verify/mtgjson/MtgJsonCard.java | 15 +- .../mage/verify/mtgjson/MtgJsonMetadata.java | 12 +- .../mage/verify/mtgjson/MtgJsonService.java | 58 ++-- .../java/mage/verify/mtgjson/MtgJsonSet.java | 13 +- .../main/java/mage/game/draft/DraftImpl.java | 4 +- ...adFactory.java => XmageThreadFactory.java} | 6 +- 35 files changed, 713 insertions(+), 464 deletions(-) create mode 100644 Mage.Client/src/main/java/mage/client/remote/XmageURLConnection.java create mode 100644 Mage.Client/src/test/java/mage/client/util/DownloaderTest.java rename Mage/src/main/java/mage/util/{XMageThreadFactory.java => XmageThreadFactory.java} (86%) diff --git a/Mage.Client/src/main/java/mage/client/MageFrame.java b/Mage.Client/src/main/java/mage/client/MageFrame.java index f6be85deefd..a92f5f70253 100644 --- a/Mage.Client/src/main/java/mage/client/MageFrame.java +++ b/Mage.Client/src/main/java/mage/client/MageFrame.java @@ -46,7 +46,7 @@ import mage.remote.Connection; import mage.remote.Connection.ProxyType; import mage.util.DebugUtil; import mage.util.ThreadUtils; -import mage.util.XMageThreadFactory; +import mage.util.XmageThreadFactory; import mage.utils.MageVersion; import mage.view.GameEndView; import mage.view.UserRequestMessage; @@ -134,7 +134,7 @@ public class MageFrame extends javax.swing.JFrame implements MageClient { private static final MageUI UI = new MageUI(); private static final ScheduledExecutorService PING_SENDER_EXECUTOR = Executors.newSingleThreadScheduledExecutor( - new XMageThreadFactory(ThreadUtils.THREAD_PREFIX_CLIENT_PING_SENDER) + new XmageThreadFactory(ThreadUtils.THREAD_PREFIX_CLIENT_PING_SENDER) ); private static UpdateMemUsageTask updateMemUsageTask; diff --git a/Mage.Client/src/main/java/mage/client/deckeditor/DeckEditorPanel.java b/Mage.Client/src/main/java/mage/client/deckeditor/DeckEditorPanel.java index ed27da771e3..90b74859bf3 100644 --- a/Mage.Client/src/main/java/mage/client/deckeditor/DeckEditorPanel.java +++ b/Mage.Client/src/main/java/mage/client/deckeditor/DeckEditorPanel.java @@ -26,7 +26,7 @@ import mage.game.GameException; import mage.remote.Session; import mage.util.DeckUtil; import mage.util.ThreadUtils; -import mage.util.XMageThreadFactory; +import mage.util.XmageThreadFactory; import mage.view.CardView; import mage.view.SimpleCardView; import org.apache.log4j.Logger; @@ -1496,7 +1496,7 @@ public class DeckEditorPanel extends javax.swing.JPanel { private void btnSubmitTimerActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_btnSubmitTimerActionPerformed ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor( - new XMageThreadFactory(ThreadUtils.THREAD_PREFIX_CLIENT_SUBMIT_TIMER) + new XmageThreadFactory(ThreadUtils.THREAD_PREFIX_CLIENT_SUBMIT_TIMER) ); timeToSubmit = 60; this.btnSubmitTimer.setEnabled(false); diff --git a/Mage.Client/src/main/java/mage/client/game/FeedbackPanel.java b/Mage.Client/src/main/java/mage/client/game/FeedbackPanel.java index a562a947a0b..82a2f87bf33 100644 --- a/Mage.Client/src/main/java/mage/client/game/FeedbackPanel.java +++ b/Mage.Client/src/main/java/mage/client/game/FeedbackPanel.java @@ -9,7 +9,7 @@ import mage.client.util.gui.ArrowBuilder; import mage.constants.PlayerAction; import mage.constants.TurnPhase; import mage.util.ThreadUtils; -import mage.util.XMageThreadFactory; +import mage.util.XmageThreadFactory; import org.apache.log4j.Logger; import javax.swing.*; @@ -45,7 +45,7 @@ public class FeedbackPanel extends javax.swing.JPanel { private static final int AUTO_CLOSE_END_DIALOG_TIMEOUT_SECS = 8; private static final ScheduledExecutorService AUTO_CLOSE_EXECUTOR = Executors.newSingleThreadScheduledExecutor( - new XMageThreadFactory(ThreadUtils.THREAD_PREFIX_CLIENT_AUTO_CLOSE_TIMER) + new XmageThreadFactory(ThreadUtils.THREAD_PREFIX_CLIENT_AUTO_CLOSE_TIMER) ); /** diff --git a/Mage.Client/src/main/java/mage/client/remote/XmageURLConnection.java b/Mage.Client/src/main/java/mage/client/remote/XmageURLConnection.java new file mode 100644 index 00000000000..37e115d1f60 --- /dev/null +++ b/Mage.Client/src/main/java/mage/client/remote/XmageURLConnection.java @@ -0,0 +1,294 @@ +package mage.client.remote; + +import mage.client.dialog.PreferencesDialog; +import mage.remote.Connection; +import mage.utils.MageVersion; +import org.apache.log4j.Logger; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.URL; +import java.util.Map; + +/** + * Network: proxy class to set up and use network connections like URLConnection + *

+ * It used stream logic for data access + *

+ * For text: + * - download text data by XmageURLConnection.downloadText + *

+ * For binary data (e.g. file download): + * - get stream by XmageURLConnection.downloadBinary + * - save stream to file or process + *

+ * TODO: no needs in POST requests (support only GET), but can be added later for another third party APIs + * + * @author JayDi85 + */ +public class XmageURLConnection { + + private static final MageVersion version = new MageVersion(XmageURLConnection.class); + private static final Logger logger = Logger.getLogger(XmageURLConnection.class); + + private static final int CONNECTION_STARTING_TIMEOUT_MS = 10000; + private static final int CONNECTION_READING_TIMEOUT_MS = 60000; + + final String url; + Proxy proxy = null; + HttpURLConnection connection = null; + HttpLoggingType loggingType = HttpLoggingType.ERRORS; + + public XmageURLConnection(String url) { + this.url = url; + } + + // example: 404 Not Found xxx + enum HttpLoggingType { + NONE, + ERRORS, + ALL + } + + /** + * Add additional headers like non standard user agent, etc + */ + public void setRequestHeaders(Map additionalHeaders) { + makeSureConnectionStarted(); + + for (String key : additionalHeaders.keySet()) { + this.connection.setRequestProperty(key, additionalHeaders.get(key)); + } + } + + /** + * Connect to server + */ + public void startConnection() { + initDefaultProxy(); + + try { + URL url = new URL(this.url); + + // proxy settings + if (this.proxy != null) { + this.connection = (HttpURLConnection) url.openConnection(this.proxy); + } else { + this.connection = (HttpURLConnection) url.openConnection(); + } + + // additional settings + this.connection.setConnectTimeout(CONNECTION_STARTING_TIMEOUT_MS); + this.connection.setReadTimeout(CONNECTION_READING_TIMEOUT_MS); + + initDefaultHeaders(); + } catch (IOException e) { + this.connection = null; + } + } + + public void initDefaultProxy() { + Connection.ProxyType configProxyType = Connection.ProxyType.valueByText(PreferencesDialog.getCachedValue(PreferencesDialog.KEY_PROXY_TYPE, "None")); + Proxy.Type type; + switch (configProxyType) { + case HTTP: + type = Proxy.Type.HTTP; + break; + case SOCKS: + type = Proxy.Type.SOCKS; + break; + case NONE: + default: + type = Proxy.Type.DIRECT; + break; + } + + this.proxy = Proxy.NO_PROXY; + if (type != Proxy.Type.DIRECT) { + try { + String address = PreferencesDialog.getCachedValue(PreferencesDialog.KEY_PROXY_ADDRESS, ""); + int port = Integer.parseInt(PreferencesDialog.getCachedValue(PreferencesDialog.KEY_PROXY_PORT, "80")); + this.proxy = new Proxy(type, new InetSocketAddress(address, port)); + } catch (Exception e) { + throw new RuntimeException("Network: can't create proxy, check your settings or reset it - " + e, e); + } + } + } + + private void initDefaultHeaders() { + // warning, do not add Accept-Encoding - it processing inside URLConnection for http/https links (trying to use gzip by default) + + // user agent due standard notation User-Agent: / + // warning, dot not add os, language and other details + this.connection.setRequestProperty("User-Agent", String.format("XMage/%s build: %s", + version.toString(false), version.getBuildTime())); + } + + /** + * Connect to server's resource + */ + public void connect() throws IOException { + makeSureConnectionStarted(); + + this.connection.connect(); + printHttpResult(); + } + + public boolean isConnected() { + return this.connection != null; + } + + /** + * Get http status code from a web server (200 for ok, -1 for not connect, 400 and other for errors) + */ + public int getResponseCode() { + makeSureConnectionStarted(); + + try { + return this.connection.getResponseCode(); + } catch (IOException ignore) { + return -1; + } + } + + /** + * Get total file size to download (call it after start connection) + * -1 for unknown size + * 0 for text or small files + *

+ * Warning, result depends on Accept-Encoding, so use it for information only + */ + public int getContentLength() { + makeSureConnectionStarted(); + + return this.connection.getContentLength(); + } + + /** + * Get http status message from a web server like Not Found + */ + public String getResponseMessage() { + makeSureConnectionStarted(); + + try { + return this.connection.getResponseMessage(); + } catch (IOException ignore) { + return ""; + } + } + + /** + * Get returned html from a web server + */ + public String getErrorResponseAsString() { + makeSureConnectionStarted(); + + java.util.Scanner s = new java.util.Scanner(this.connection.getErrorStream()).useDelimiter("\\A"); + return s.hasNext() ? s.next() : ""; + } + + /** + * Get non-text data as stream, e.g. binary file content + */ + public InputStream getGoodResponseAsStream() throws IOException { + makeSureConnectionStarted(); + + return this.connection.getInputStream(); + } + + /** + * Get text data as string, e.g. html document + */ + public String getGoodResponseAsString() { + makeSureConnectionStarted(); + StringBuffer tmp = new StringBuffer(); + BufferedReader in = null; + try { + in = new BufferedReader(new InputStreamReader(this.getGoodResponseAsStream())); + String line; + while ((line = in.readLine()) != null) { + tmp.append(line); + } + } catch (IOException e) { + throw new RuntimeException("Network: can't get text data from " + this.url + " - " + e, e); + } + + return String.valueOf(tmp); + } + + private void makeSureConnectionStarted() { + if (!isConnected()) { + throw new IllegalArgumentException("Wrong code usage: must call startConnection first", new Throwable()); + } + } + + /** + * Fast download of text data + * + * @return downloaded text on OK 200 response or empty on any other errors + */ + public static String downloadText(String resourceUrl) { + XmageURLConnection con = new XmageURLConnection(resourceUrl); + con.startConnection(); + if (con.isConnected()) { + try { + con.connect(); + if (con.getResponseCode() == 200) { + return con.getGoodResponseAsString(); + } + } catch (IOException e) { + logger.error(e, e); + } + } + return ""; + } + + /** + * Fast download of binary data + * + * @return stream on OK 200 response or null on any other errors + */ + public static InputStream downloadBinary(String resourceUrl) { + XmageURLConnection con = new XmageURLConnection(resourceUrl); + con.startConnection(); + if (con.isConnected()) { + try { + con.connect(); + if (con.getResponseCode() == 200) { + return con.getGoodResponseAsStream(); + } + } catch (IOException e) { + logger.error(e, e); + } + } + return null; + } + + private void printHttpResult() { + if (this.connection == null) { + return; + } + + boolean needPrint; + switch (this.loggingType) { + case NONE: + needPrint = false; + break; + case ERRORS: + needPrint = getResponseCode() != HttpURLConnection.HTTP_OK; + break; + case ALL: + default: + needPrint = true; + } + + if (needPrint) { + logger.info(String.format("http request %d %s %s", this.getResponseCode(), this.getResponseMessage(), this.url)); + } + } +} \ No newline at end of file diff --git a/Mage.Client/src/main/java/org/mage/card/arcane/ManaSymbols.java b/Mage.Client/src/main/java/org/mage/card/arcane/ManaSymbols.java index d23a5e3d7d4..6f0fae9c542 100644 --- a/Mage.Client/src/main/java/org/mage/card/arcane/ManaSymbols.java +++ b/Mage.Client/src/main/java/org/mage/card/arcane/ManaSymbols.java @@ -22,13 +22,15 @@ import java.awt.image.BufferedImage; import java.io.*; import java.nio.file.*; import java.nio.file.attribute.BasicFileAttributes; +import java.util.List; import java.util.*; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicIntegerArray; import java.util.regex.Pattern; import java.util.stream.IntStream; +/** + * Mana symbol resources + */ public final class ManaSymbols { private static final Logger logger = Logger.getLogger(ManaSymbols.class); @@ -120,18 +122,13 @@ public final class ManaSymbols { ImageIO.write(image, "png", newFile); } } catch (Exception e) { - logger.warn("Can't generate png image for symbol:" + symbol); + logger.warn("Symbols: can't generate png image for symbol:" + symbol); } } } // preload set images java.util.List setCodes = ExpansionRepository.instance.getSetCodes(); - if (setCodes == null) { - // the cards db file is probaly not included in the client. It will be created after the first connect to a server. - logger.warn("No db information for sets found. Connect to a server to create database file on client side. Then try to restart the client."); - return; - } for (String set : setCodes) { if (withoutSymbols.contains(set)) { @@ -289,9 +286,9 @@ public final class ManaSymbols { // priority: SVG -> GIF // gif remain for backward compatibility - AtomicIntegerArray iconErrors = new AtomicIntegerArray(2); // 0 - svg, 1 - gif + final List svgFails = new ArrayList<>(); // scryfall + final List otherFails = new ArrayList<>(); // gatherer - AtomicBoolean fileErrors = new AtomicBoolean(false); Map sizedSymbols = new ConcurrentHashMap<>(); IntStream.range(0, symbols.length).parallel().forEach(i -> { String symbol = symbols[i]; @@ -305,50 +302,53 @@ public final class ManaSymbols { try { InputStream fileStream = new FileInputStream(file); image = loadSymbolAsSVG(fileStream, file.getPath(), size, size); - } catch (FileNotFoundException e) { - // it's ok to hide error + } catch (FileNotFoundException ignore) { } } } - - // gif if (image == null) { + synchronized (svgFails) { + svgFails.add(symbol); + } + } - iconErrors.incrementAndGet(0); // svg fail - + // gif (if svg fails) + if (image == null) { file = getSymbolFileNameAsGIF(symbol, size); if (file.exists()) { image = loadSymbolAsGIF(file, size, size); } } + if (image == null) { + synchronized (otherFails) { + otherFails.add(symbol); + } + } // save if (image != null) { sizedSymbols.put(symbol, image); - } else { - iconErrors.incrementAndGet(1); // gif fail - fileErrors.set(true); } }); // total errors String errorInfo = ""; - if (iconErrors.get(0) > 0) { - errorInfo += "SVG miss - " + iconErrors.get(0); + if (!svgFails.isEmpty()) { + errorInfo += String.format("SVG miss - %s and %d others", svgFails.get(0), svgFails.size() - 1); } - if (iconErrors.get(1) > 0) { + if (!otherFails.isEmpty()) { if (!errorInfo.isEmpty()) { errorInfo += ", "; } - errorInfo += "GIF miss - " + iconErrors.get(1); + errorInfo += String.format("GIF miss - %s and %d others", otherFails.get(0), otherFails.size() - 1); } if (!errorInfo.isEmpty()) { - logger.warn("Symbols can't be loaded, make sure you download it by main menu - size " + size + ", " + errorInfo); + logger.warn("Symbols: can't load, make sure you download it by main menu - size " + size + ", " + errorInfo); } manaImages.put(size, sizedSymbols); - return !fileErrors.get(); + return errorInfo.isEmpty(); } private static void renameSymbols(String path) { diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/CardPluginImpl.java b/Mage.Client/src/main/java/org/mage/plugins/card/CardPluginImpl.java index 58f5e26e941..c5fc47f5b51 100644 --- a/Mage.Client/src/main/java/org/mage/plugins/card/CardPluginImpl.java +++ b/Mage.Client/src/main/java/org/mage/plugins/card/CardPluginImpl.java @@ -4,7 +4,6 @@ import mage.cards.MageCard; import mage.cards.MagePermanent; import mage.cards.action.ActionCallback; import mage.client.util.GUISizeHelper; -import mage.client.util.ImageCaches; import mage.interfaces.plugin.CardPlugin; import mage.view.CardView; import mage.view.CounterView; @@ -103,7 +102,7 @@ public class CardPluginImpl implements CardPlugin { * yet, so use old component based rendering for the split cards. */ private CardPanel makeCardPanel(CardView view, UUID gameId, boolean loadImage, ActionCallback callback, - boolean isFoil, Dimension dimension, int renderMode, boolean needFullPermanentRender) { + boolean isFoil, Dimension dimension, int renderMode, boolean needFullPermanentRender) { switch (renderMode) { case 0: return new CardPanelRenderModeMTGO(view, gameId, loadImage, callback, isFoil, dimension, @@ -118,7 +117,7 @@ public class CardPluginImpl implements CardPlugin { @Override public MageCard getMagePermanent(PermanentView permanent, Dimension dimension, UUID gameId, ActionCallback callback, - boolean canBeFoil, boolean loadImage, int renderMode, boolean needFullPermanentRender) { + boolean canBeFoil, boolean loadImage, int renderMode, boolean needFullPermanentRender) { CardPanel cardPanel = makeCardPanel(permanent, gameId, loadImage, callback, false, dimension, renderMode, needFullPermanentRender); cardPanel.setShowCastingCost(true); @@ -127,7 +126,7 @@ public class CardPluginImpl implements CardPlugin { @Override public MageCard getMageCard(CardView cardView, Dimension dimension, UUID gameId, ActionCallback callback, - boolean canBeFoil, boolean loadImage, int renderMode, boolean needFullPermanentRender) { + boolean canBeFoil, boolean loadImage, int renderMode, boolean needFullPermanentRender) { CardPanel cardPanel = makeCardPanel(cardView, gameId, loadImage, callback, false, dimension, renderMode, needFullPermanentRender); cardPanel.setShowCastingCost(true); @@ -191,7 +190,7 @@ public class CardPluginImpl implements CardPlugin { && stackPower == cardPower && stackToughness == cardToughness && stackAbilities.equals(cardAbilities) && stackCounters.equals(cardCounters) && (!perm.isCreature() || firstPanelPerm.getOriginalPermanent().hasSummoningSickness() == perm - .getOriginalPermanent().hasSummoningSickness())) { + .getOriginalPermanent().hasSummoningSickness())) { if (!empty(firstPanelPerm.getOriginalPermanent().getAttachments())) { // Put this land to the left of lands with the same name and attachments. @@ -235,7 +234,7 @@ public class CardPluginImpl implements CardPlugin { @Override public int sortPermanents(Map ui, Map cards, boolean nonPermanentsOwnRow, - boolean topPanel) { + boolean topPanel) { // requires to find out is position have been changed that includes: // adding/removing permanents, type change @@ -279,7 +278,7 @@ public class CardPluginImpl implements CardPlugin { cardHeight = Math.round(cardWidth * CardPanel.ASPECT_RATIO); extraCardSpacingX = Math.round(cardWidth * EXTRA_CARD_SPACING_X); cardSpacingX = cardHeight - cardWidth + extraCardSpacingX; // need space for tap animation (horizontal - // position) + // position) cardSpacingY = Math.round(cardHeight * CARD_SPACING_Y); stackSpacingX = stackVertical ? 0 : Math.round(cardWidth * STACK_SPACING_X); stackSpacingY = Math.round(cardHeight * STACK_SPACING_Y); @@ -363,7 +362,7 @@ public class CardPluginImpl implements CardPlugin { } for (int panelIndex = 0, panelCount = stack.size(); panelIndex < panelCount; panelIndex++) { MagePermanent panelPerm = stack.get(panelIndex); // it's original card panel, but you must change - // top layer + // top layer int stackPosition = panelCount - panelIndex - 1; if (cardsPanel != null) { cardsPanel.setComponentZOrder(panelPerm.getTopPanelRef(), panelIndex); @@ -470,7 +469,7 @@ public class CardPluginImpl implements CardPlugin { } private AttachmentLayoutInfos calculateNeededNumberOfVerticalColumns(int currentCol, Map cards, - MageCard cardWithAttachments) { + MageCard cardWithAttachments) { int maxCol = ++currentCol; int attachments = 0; MagePermanent permWithAttachments = (MagePermanent) cardWithAttachments.getMainPanel(); @@ -653,38 +652,34 @@ public class CardPluginImpl implements CardPlugin { final Downloader downloader = new Downloader(); final DownloadGui downloadGui = new DownloadGui(downloader); - LOGGER.info("Symbols download prepare..."); + LOGGER.info("Download: prepare symbols to download..."); Iterable jobs; + // mana symbols (low quality) jobs = new GathererSymbols(); for (DownloadJob job : jobs) { downloader.add(job); } + // set code symbols (low quality) jobs = new GathererSets(); for (DownloadJob job : jobs) { downloader.add(job); } + // mana symbols (high quality) jobs = new ScryfallSymbolsSource(); for (DownloadJob job : jobs) { downloader.add(job); } - /* - * it = new CardFrames(imagesDir); // TODO: delete frames download (not need - * now) - * for (DownloadJob job : it) { - * g.getDownloader().add(job); - * } - */ - + // additional resources jobs = new DirectLinksForDownload(); for (DownloadJob job : jobs) { downloader.add(job); } - LOGGER.info("Symbols download needs " + downloader.getJobs().size() + " files"); + LOGGER.info("Download: app used " + downloader.getJobs().size() + " symbol files"); // download GUI dialog JDialog dialog = new JDialog((Frame) null, "Download symbols", false); @@ -711,6 +706,7 @@ public class CardPluginImpl implements CardPlugin { return null; } }; + // downloader finisher worker.addPropertyChangeListener(new PropertyChangeListener() { @Override @@ -718,7 +714,7 @@ public class CardPluginImpl implements CardPlugin { if (evt.getPropertyName().equals("state")) { if (evt.getNewValue() == SwingWorker.StateValue.DONE) { // all done, can close dialog and refresh symbols for UI - LOGGER.info("Symbols download finished"); + LOGGER.info("Download: symbols download finished"); dialog.dispose(); ManaSymbols.loadImages(); GUISizeHelper.refreshGUIAndCards(false); diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/dl/DownloadJob.java b/Mage.Client/src/main/java/org/mage/plugins/card/dl/DownloadJob.java index a0221c4e100..cbbbdb52680 100644 --- a/Mage.Client/src/main/java/org/mage/plugins/card/dl/DownloadJob.java +++ b/Mage.Client/src/main/java/org/mage/plugins/card/dl/DownloadJob.java @@ -2,16 +2,15 @@ package org.mage.plugins.card.dl; import org.mage.plugins.card.dl.beans.properties.Property; import org.mage.plugins.card.dl.lm.AbstractLaternaBean; -import org.mage.plugins.card.utils.CardImageUtils; import javax.swing.*; -import java.io.*; -import java.net.Proxy; -import java.net.URL; -import java.net.URLConnection; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; /** - * Downloader job to download one resource + * Download: download job to load one resource, used for symbols * * @author Clemens Koza, JayDi85 */ @@ -22,7 +21,7 @@ public class DownloadJob extends AbstractLaternaBean { } private final String name; - private Source source; + private String url; private final Destination destination; private final boolean forceToDownload; // download image everytime, do not keep old image private final Property state = properties.property("state", State.NEW); @@ -30,13 +29,21 @@ public class DownloadJob extends AbstractLaternaBean { private final Property error = properties.property("error"); private final BoundedRangeModel progress = new DefaultBoundedRangeModel(); - public DownloadJob(String name, Source source, Destination destination, boolean forceToDownload) { + public DownloadJob(String name, String url, Destination destination, boolean forceToDownload) { this.name = name; - this.source = source; + this.url = url; this.destination = destination; this.forceToDownload = forceToDownload; } + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + /** * Sets the job's state. If the state is {@link State#ABORTED}, it instead * sets the error to "ABORTED" @@ -80,7 +87,7 @@ public class DownloadJob extends AbstractLaternaBean { */ public void setError(String message, Exception error) { if (message == null) { - message = "Download of " + name + " from " + source.toString() + " caused error: " + error.toString(); + message = "Download: " + name + " from " + url + " caused error - " + error; } this.state.setValue(State.ABORTED); this.error.setValue(error); @@ -145,14 +152,6 @@ public class DownloadJob extends AbstractLaternaBean { return name; } - public Source getSource() { - return source; - } - - public void setSource(Source source) { - this.source = source; - } - public Destination getDestination() { return destination; } @@ -161,71 +160,6 @@ public class DownloadJob extends AbstractLaternaBean { return forceToDownload; } - public static Source fromURL(final String url) { - return fromURL(CardImageUtils.getProxyFromPreferences(), url); - } - - public static Source fromURL(final URL url) { - return fromURL(CardImageUtils.getProxyFromPreferences(), url); - } - - public static Source fromURL(final Proxy proxy, final String url) { - return new Source() { - private URLConnection c; - - public URLConnection getConnection() throws IOException { - if (c == null) { - c = proxy == null ? new URL(url).openConnection() : new URL(url).openConnection(proxy); - } - return c; - } - - @Override - public InputStream open() throws IOException { - return getConnection().getInputStream(); - } - - @Override - public int length() throws IOException { - return getConnection().getContentLength(); - } - - @Override - public String toString() { - return proxy != null ? proxy.type().toString() + ' ' : url; - } - - }; - } - - public static Source fromURL(final Proxy proxy, final URL url) { - return new Source() { - private URLConnection c; - - public URLConnection getConnection() throws IOException { - if (c == null) { - c = proxy == null ? url.openConnection() : url.openConnection(proxy); - } - return c; - } - - @Override - public InputStream open() throws IOException { - return getConnection().getInputStream(); - } - - @Override - public int length() throws IOException { - return getConnection().getContentLength(); - } - - @Override - public String toString() { - return proxy != null ? proxy.type().toString() + ' ' : String.valueOf(url); - } - }; - } - public static Destination toFile(final String file) { return toFile(new File(file)); } @@ -264,13 +198,6 @@ public class DownloadJob extends AbstractLaternaBean { }; } - public interface Source { - - InputStream open() throws IOException; - - int length() throws IOException; - } - public interface Destination { OutputStream open() throws IOException; diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/dl/DownloadServiceInfo.java b/Mage.Client/src/main/java/org/mage/plugins/card/dl/DownloadServiceInfo.java index 0e5152b3157..72c816d3908 100644 --- a/Mage.Client/src/main/java/org/mage/plugins/card/dl/DownloadServiceInfo.java +++ b/Mage.Client/src/main/java/org/mage/plugins/card/dl/DownloadServiceInfo.java @@ -7,8 +7,6 @@ import java.net.Proxy; */ public interface DownloadServiceInfo { - Proxy getProxy(); - boolean isNeedCancel(); void incErrorCount(); diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/dl/Downloader.java b/Mage.Client/src/main/java/org/mage/plugins/card/dl/Downloader.java index 6281a8dc69a..ad44256a076 100644 --- a/Mage.Client/src/main/java/org/mage/plugins/card/dl/Downloader.java +++ b/Mage.Client/src/main/java/org/mage/plugins/card/dl/Downloader.java @@ -1,7 +1,8 @@ package org.mage.plugins.card.dl; +import mage.client.remote.XmageURLConnection; import mage.util.ThreadUtils; -import mage.util.XMageThreadFactory; +import mage.util.XmageThreadFactory; import org.apache.log4j.Logger; import org.jetlang.channels.Channel; import org.jetlang.channels.MemoryChannel; @@ -9,13 +10,15 @@ import org.jetlang.core.Callback; import org.jetlang.fibers.Fiber; import org.jetlang.fibers.PoolFiberFactory; import org.mage.plugins.card.dl.DownloadJob.Destination; -import org.mage.plugins.card.dl.DownloadJob.Source; import org.mage.plugins.card.dl.DownloadJob.State; import org.mage.plugins.card.dl.lm.AbstractLaternaBean; import javax.swing.*; -import java.io.*; -import java.net.ConnectException; +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; @@ -24,7 +27,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; /** - * Symbols downloader + * Download: symbols download service * * @author Clemens Koza, JayDi85 */ @@ -37,7 +40,7 @@ public class Downloader extends AbstractLaternaBean { private CountDownLatch worksCount = null; private final ExecutorService pool = Executors.newCachedThreadPool( - new XMageThreadFactory(ThreadUtils.THREAD_PREFIX_CLIENT_SYMBOLS_DOWNLOADER, false) + new XmageThreadFactory(ThreadUtils.THREAD_PREFIX_CLIENT_SYMBOLS_DOWNLOADER, false) ); private final List fibers = new ArrayList<>(); @@ -105,11 +108,11 @@ public class Downloader extends AbstractLaternaBean { worksCount.await(60, TimeUnit.SECONDS); if (worksCount.getCount() != 0) { - logger.warn("Symbols download too long..."); + logger.warn("Download: symbols downloading too long"); } } } catch (InterruptedException e) { - logger.error("Need to stop symbols download..."); + logger.error("Download: symbols downloading must be stopped"); } } @@ -118,8 +121,8 @@ public class Downloader extends AbstractLaternaBean { } /** - * Performs the download job: Transfers data from {@link Source} to - * {@link Destination} and updates the download job's state to reflect the + * Performs the download job: Transfers data from source to + * destination and updates the download job's state to reflect the * progress. */ private class DownloadCallback implements Callback { @@ -133,7 +136,7 @@ public class Downloader extends AbstractLaternaBean { // take new job job.doPrepareAndStartWork(); if (job.getState() != State.WORKING) { - logger.warn("Can't prepare symbols download job: " + job.getName()); + logger.warn("Download: can't prepare symbols download job: " + job.getName() + ", reason: " + job.getError()); worksCount.countDown(); return; } @@ -146,7 +149,6 @@ public class Downloader extends AbstractLaternaBean { // real work for new job // download and save data try { - Source src = job.getSource(); Destination dst = job.getDestination(); BoundedRangeModel progress = job.getProgress(); @@ -155,65 +157,66 @@ public class Downloader extends AbstractLaternaBean { progress.setMaximum(1); progress.setValue(1); } else { - // downloading + // need to download + + // clean local file if (dst.exists()) { try { dst.delete(); - } catch (IOException ex1) { - logger.warn("While deleting not valid file", ex1); + } catch (IOException e) { + logger.warn("Download: can't delete old file " + e, e); } } - progress.setMaximum(src.length()); - InputStream is = new BufferedInputStream(src.open()); - try { - OutputStream os = new BufferedOutputStream(dst.open()); - try { - byte[] buf = new byte[8 * 1024]; - int total = 0; - for (int len; (len = is.read(buf)) != -1; ) { - if (job.getState() == State.ABORTED) { - throw new IOException("Job was aborted"); + + // download + // start debug here with breakpoint like job.getName().contains("C/B") + XmageURLConnection connection = new XmageURLConnection(job.getUrl()); + connection.startConnection(); + if (connection.isConnected()) { + // start downloading + connection.connect(); + if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) { + // all fine, can continue and save + progress.setMaximum(connection.getContentLength()); + try (InputStream inputStream = connection.getGoodResponseAsStream(); + OutputStream outputStream = new BufferedOutputStream(dst.open())) { + byte[] buf = new byte[8 * 1024]; + int total = 0; + for (int len; (len = inputStream.read(buf)) != -1; ) { + // fast cancel + if (job.getState() == State.ABORTED) { + throw new IOException("job was aborted"); + } + progress.setValue(total += len); + outputStream.write(buf, 0, len); } - progress.setValue(total += len); - os.write(buf, 0, len); - } - } catch (IOException ex) { - try { - dst.delete(); - } catch (IOException ex1) { - logger.warn("While deleting", ex1); - } - throw ex; - } finally { - try { - os.close(); + } catch (IOException e) { + // something bad on downloading + logger.warn("Download: " + job.getName() + " - catch error on downloading network resource " + + job.getUrl() + " - " + e, e); + } finally { + // clean up if (!dst.isValid()) { + logger.warn("Download: " + job.getName() + " - downloaded invalid network resource (not exists?) " + job.getUrl()); dst.delete(); - logger.warn("Resource not found " + job.getName() + " from " + job.getSource().toString()); } - } catch (IOException ex) { - logger.warn("While closing", ex); } + } else { + // something bad with resource on server (example: wrong url) + logger.warn("Download: " + job.getName() + " - can't find network resource " + job.getUrl()); } - } finally { - try { - is.close(); - } catch (IOException ex) { - logger.warn("While closing", ex); - } + } else { + // something bad with network (example: can't connect due bad network) + logger.warn("Download: " + job.getName() + " - can't connect to network resource " + job.getUrl()); } } + + // all done job.setState(State.FINISHED); - } catch (ConnectException ex) { - String message; - if (ex.getMessage() != null) { - message = ex.getMessage(); - } else { - message = "Unknown error"; - } - logger.warn("Error resource download " + job.getName() + " from " + job.getSource().toString() + ": " + message); - } catch (IOException ex) { - job.setError(ex); + } catch (Exception e) { + // TODO: save error in other logger.warn? + job.setError(e); + logger.warn("Download: " + job.getName() + " - unknown error for network resource " + job.getUrl() + " - " + e, e); } finally { worksCount.countDown(); } diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/CardImageSource.java b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/CardImageSource.java index c353095b632..7de570322db 100644 --- a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/CardImageSource.java +++ b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/CardImageSource.java @@ -54,13 +54,10 @@ public interface CardImageSource { } /** - * Set additional http headers like user agent, referer, cookies, etc + * Set additional headers like user agent, referer, cookies, etc */ default Map getHttpRequestHeaders(String fullUrl) { - Map headers = new LinkedHashMap<>(); - // TODO: add xmage name and client version here - headers.put("User-Agent", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.4; en-US; rv:1.9.2.2) Gecko/20100316 Firefox/3.6.2"); - return headers; + return new LinkedHashMap<>(); } default List getSupportedSets() { diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/DirectLinksForDownload.java b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/DirectLinksForDownload.java index 0c3acc8068c..1f6baf0d6b3 100644 --- a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/DirectLinksForDownload.java +++ b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/DirectLinksForDownload.java @@ -6,12 +6,11 @@ import org.mage.plugins.card.dl.DownloadJob; import java.io.File; import java.util.*; -import static org.mage.plugins.card.dl.DownloadJob.fromURL; import static org.mage.plugins.card.dl.DownloadJob.toFile; import static org.mage.plugins.card.utils.CardImageUtils.getImagesDir; /** - * Additional images from a third party sources + * Download: additional images from a third party sources, used for symbols * * @author noxx */ @@ -44,7 +43,7 @@ public class DirectLinksForDownload implements Iterable { for (Map.Entry url : directLinks.entrySet()) { File dst = new File(outDir, url.getKey()); // download images every time (need to update low quality image) - jobs.add(new DownloadJob(url.getKey(), fromURL(url.getValue()), toFile(dst), true)); + jobs.add(new DownloadJob(url.getKey(), url.getValue(), toFile(dst), true)); } return jobs.iterator(); } diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/GathererSets.java b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/GathererSets.java index cde061108b7..aa9dbf2254d 100644 --- a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/GathererSets.java +++ b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/GathererSets.java @@ -6,17 +6,18 @@ import mage.client.constants.Constants; import mage.constants.Rarity; import org.apache.log4j.Logger; import org.mage.plugins.card.dl.DownloadJob; -import static org.mage.plugins.card.dl.DownloadJob.fromURL; -import static org.mage.plugins.card.dl.DownloadJob.toFile; -import static org.mage.plugins.card.utils.CardImageUtils.getImagesDir; import java.io.File; import java.util.*; -/** - * WARNING, unsupported images plugin, last updates from 2018 - */ +import static org.mage.plugins.card.dl.DownloadJob.toFile; +import static org.mage.plugins.card.utils.CardImageUtils.getImagesDir; +/** + * Download: set code symbols download from wizards web size + *

+ * Warning, it's outdated source with low quality images. TODO: must migrate to scryfall like mana icons + */ public class GathererSets implements Iterable { private class CheckResult { @@ -338,6 +339,6 @@ public class GathererSets implements Iterable { set = codeReplacements.get(set); } String url = "https://gatherer.wizards.com/Handlers/Image.ashx?type=symbol&set=" + set + "&size=small&rarity=" + urlRarity; - return new DownloadJob(set + '-' + rarity, fromURL(url), toFile(dst), false); + return new DownloadJob(set + '-' + rarity, url, toFile(dst), false); } } diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/GathererSymbols.java b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/GathererSymbols.java index f7c51da91b7..e629b612fd6 100644 --- a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/GathererSymbols.java +++ b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/GathererSymbols.java @@ -8,15 +8,15 @@ import java.io.File; import java.util.Iterator; import static java.lang.String.format; -import static org.mage.plugins.card.dl.DownloadJob.fromURL; import static org.mage.plugins.card.dl.DownloadJob.toFile; import static org.mage.plugins.card.utils.CardImageUtils.getImagesDir; /** - * The class GathererSymbols. + * Download: mana symbols download from wizards web size + *

+ * Warning, it's outdated source with low quality images. Use scryfall source as primary. * - * @author Clemens Koza - * @version V0.0 25.08.2010 + * @author Clemens Koza, JayDi85 */ public class GathererSymbols implements Iterable { @@ -125,8 +125,7 @@ public class GathererSymbols implements Iterable { } String url = format(urlFmt, sizes[modSizeIndex], symbol); - - return new DownloadJob(sym, fromURL(url), toFile(dst), false); + return new DownloadJob(sym, url, toFile(dst), false); } } }; diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSource.java b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSource.java index ed63f9b99c0..abcc7f9001f 100644 --- a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSource.java +++ b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSource.java @@ -4,18 +4,16 @@ import com.google.gson.JsonArray; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import mage.MageException; +import mage.client.remote.XmageURLConnection; import mage.client.util.CardLanguage; import mage.util.JsonUtil; import org.apache.log4j.Logger; import org.mage.plugins.card.dl.DownloadServiceInfo; import org.mage.plugins.card.images.CardDownloadData; -import java.io.FileNotFoundException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.Proxy; -import java.net.URL; -import java.net.URLConnection; import java.util.*; /** @@ -31,7 +29,7 @@ public class ScryfallImageSource implements CardImageSource { private CardLanguage currentLanguage = CardLanguage.ENGLISH; // working language private final Map preparedUrls = new HashMap<>(); private static final int DOWNLOAD_TIMEOUT_MS = 100; - + public static ScryfallImageSource getInstance() { return instance; } @@ -89,15 +87,15 @@ public class ScryfallImageSource implements CardImageSource { baseUrl = link + localizedCode + "?format=image"; // workaround to use cards without english images (some promos or special cards) if (link.endsWith("/")) { - alternativeUrl = link.substring(0,link.length()-1) + "?format=image"; + alternativeUrl = link.substring(0, link.length() - 1) + "?format=image"; } } else { // direct link to image baseUrl = link; // workaround to use localization in direct links - if (link.contains("/?format=image")){ - baseUrl = link.replaceFirst("\\?format=image" , localizedCode + "?format=image"); - alternativeUrl = link.replaceFirst("/\\?format=image" , "?format=image"); + if (link.contains("/?format=image")) { + baseUrl = link.replaceFirst("\\?format=image", localizedCode + "?format=image"); + alternativeUrl = link.replaceFirst("/\\?format=image", "?format=image"); } } } @@ -117,7 +115,7 @@ public class ScryfallImageSource implements CardImageSource { // basic cards by api call (redirect to img link) // example: https://api.scryfall.com/cards/xln/121/en?format=image if (baseUrl == null) { - String cn = ScryfallImageSupportCards.prepareCardNumber(card.getCollectorId()) ; + String cn = ScryfallImageSupportCards.prepareCardNumber(card.getCollectorId()); baseUrl = String.format("https://api.scryfall.com/cards/%s/%s/%s?format=image", formatSetName(card.getSet(), isToken), cn, @@ -135,10 +133,10 @@ public class ScryfallImageSource implements CardImageSource { } - return new CardImageUrls(baseUrl, alternativeUrl ); + return new CardImageUrls(baseUrl, alternativeUrl); } - private String getFaceImageUrl(Proxy proxy, CardDownloadData card, boolean isToken) throws Exception { + private String getFaceImageUrl(CardDownloadData card, boolean isToken) throws Exception { final String defaultCode = CardLanguage.ENGLISH.getCode(); final String localizedCode = languageAliases.getOrDefault(this.getCurrentLanguage(), defaultCode); @@ -148,11 +146,11 @@ public class ScryfallImageSource implements CardImageSource { String apiUrl = ScryfallImageSupportCards.findDirectDownloadLink(card.getSet(), card.getName(), card.getCollectorId()); if (apiUrl != null) { if (apiUrl.endsWith("*/")) { - apiUrl = apiUrl.substring(0 , apiUrl.length() - 2) + "★/" ; + apiUrl = apiUrl.substring(0, apiUrl.length() - 2) + "★/"; } else if (apiUrl.endsWith("+/")) { - apiUrl = apiUrl.substring(0 , apiUrl.length() - 2) + "†/" ; + apiUrl = apiUrl.substring(0, apiUrl.length() - 2) + "†/"; } else if (apiUrl.endsWith("Ph/")) { - apiUrl = apiUrl.substring(0 , apiUrl.length() - 3) + "Φ/" ; + apiUrl = apiUrl.substring(0, apiUrl.length() - 3) + "Φ/"; } // BY DIRECT URL // direct links via hardcoded API path. Used for cards with non-ASCII collector numbers @@ -168,7 +166,7 @@ public class ScryfallImageSource implements CardImageSource { } else { // BY CARD NUMBER // localized and default - String cn = ScryfallImageSupportCards.prepareCardNumber (card.getCollectorId()) ; + String cn = ScryfallImageSupportCards.prepareCardNumber(card.getCollectorId()); needUrls.add(String.format("https://api.scryfall.com/cards/%s/%s/%s", formatSetName(card.getSet(), isToken), cn, @@ -183,18 +181,16 @@ public class ScryfallImageSource implements CardImageSource { InputStream jsonStream = null; String jsonUrl = null; for (String currentUrl : needUrls) { - // connect to Scryfall API + // find first workable api endpoint waitBeforeRequest(); - URL cardUrl = new URL(currentUrl); - URLConnection request = (proxy == null ? cardUrl.openConnection() : cardUrl.openConnection(proxy)); - request.connect(); - try { - jsonStream = (InputStream) request.getContent(); - jsonUrl = currentUrl; + + jsonStream = XmageURLConnection.downloadBinary(currentUrl); + if (jsonStream != null) { // found good url, can stop + jsonUrl = currentUrl; break; - } catch (FileNotFoundException e) { - // localized image doesn't exists, try next url + } else { + // localized image doesn't exist, try next url } } @@ -224,8 +220,6 @@ public class ScryfallImageSource implements CardImageSource { @Override public boolean prepareDownloadList(DownloadServiceInfo downloadServiceInfo, List downloadList) { // prepare download list example ( - Proxy proxy = downloadServiceInfo.getProxy(); - preparedUrls.clear(); // prepare stats @@ -248,7 +242,7 @@ public class ScryfallImageSource implements CardImageSource { if (card.isSecondSide()) { currentPrepareCount++; try { - String url = getFaceImageUrl(proxy, card, card.isToken()); + String url = getFaceImageUrl(card, card.isToken()); preparedUrls.put(card, url); } catch (Exception e) { logger.warn("Failed to prepare image URL (back face) for " + card.getName() + " (" + card.getSet() + ") #" diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallSymbolsSource.java b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallSymbolsSource.java index ef943e8ae61..c72dd1cfaa8 100644 --- a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallSymbolsSource.java +++ b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallSymbolsSource.java @@ -1,7 +1,8 @@ package org.mage.plugins.card.dl.sources; +import mage.client.remote.XmageURLConnection; +import org.jsoup.Jsoup; import org.mage.plugins.card.dl.DownloadJob; -import org.mage.plugins.card.utils.CardImageUtils; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; @@ -65,7 +66,8 @@ public class ScryfallSymbolsSource implements Iterable { try { sourceData = new String(Files.readAllBytes(Paths.get(sourcePath))); } catch (IOException e) { - LOGGER.error("Can't open file to parse data: " + sourcePath + " , reason: " + e.getMessage()); + LOGGER.error("Can't open file to parse svg data: " + sourcePath + ", reason: " + e); + return; } // gen symbols list @@ -129,10 +131,11 @@ public class ScryfallSymbolsSource implements Iterable { // listener for data parse after download complete private class ScryfallDownloadOnFinishedListener implements PropertyChangeListener { + private String downloadedFile; - public ScryfallDownloadOnFinishedListener(String ADestFile) { - this.downloadedFile = ADestFile; + public ScryfallDownloadOnFinishedListener(String downloadedFile) { + this.downloadedFile = downloadedFile; } @Override @@ -152,10 +155,15 @@ public class ScryfallSymbolsSource implements Iterable { } @Override - public void onPreparing() throws Exception { + public void onPreparing() { + // parse help page and find real URL with svg icons on it this.cssUrl = ""; - org.jsoup.nodes.Document doc = CardImageUtils.downloadHtmlDocument(CSS_SOURCE_URL); + // download + String sourceData = XmageURLConnection.downloadText(CSS_SOURCE_URL); + org.jsoup.nodes.Document doc = Jsoup.parse(sourceData); + + // process org.jsoup.select.Elements cssList = doc.select(CSS_SOURCE_SELECTOR); if (cssList.size() == 1) { this.cssUrl = cssList.first().attr("href"); @@ -164,13 +172,13 @@ public class ScryfallSymbolsSource implements Iterable { if (this.cssUrl.isEmpty()) { throw new IllegalStateException("Can't find stylesheet url from scryfall colors page."); } else { - this.setSource(fromURL(this.cssUrl)); + this.setUrl(this.cssUrl); } } public ScryfallSymbolsDownloadJob() { - // download init - super("Scryfall symbols source", fromURL(""), toFile(DOWNLOAD_TEMP_FILE), true); // url setup on preparing stage + // download init (real url will be added on prepare) + super("Scryfall symbols source", "", toFile(DOWNLOAD_TEMP_FILE), true); // url setup on preparing stage String destFile = DOWNLOAD_TEMP_FILE; this.addPropertyChangeListener(STATE_PROP_NAME, new ScryfallDownloadOnFinishedListener(destFile)); diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/WizardCardsImageSource.java b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/WizardCardsImageSource.java index bacb41aa449..48034a726ba 100644 --- a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/WizardCardsImageSource.java +++ b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/WizardCardsImageSource.java @@ -4,8 +4,10 @@ import mage.cards.Sets; import mage.cards.repository.CardCriteria; import mage.cards.repository.CardInfo; import mage.cards.repository.CardRepository; +import mage.client.remote.XmageURLConnection; import mage.client.util.CardLanguage; import org.apache.log4j.Logger; +import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; @@ -522,7 +524,8 @@ public enum WizardCardsImageSource implements CardImageSource { while (page < 999) { String searchUrl = "https://gatherer.wizards.com/Pages/Search/Default.aspx?sort=cn+&page=" + page + "&action=advanced&output=spoiler&method=visual&set=+%5B%22" + URLSetName + "%22%5D"; logger.debug("URL: " + searchUrl); - Document doc = CardImageUtils.downloadHtmlDocument(searchUrl); + String sourceData = XmageURLConnection.downloadText(searchUrl); + Document doc = Jsoup.parse(sourceData); Elements cardsImages = doc.select("img[src^=../../Handlers/]"); if (cardsImages.isEmpty()) { break; @@ -565,14 +568,15 @@ public enum WizardCardsImageSource implements CardImageSource { return setLinks; } - private void getLandVariations(LinkedHashMap setLinks, String cardSet, int multiverseId, String cardName) throws IOException, NumberFormatException { + private void getLandVariations(LinkedHashMap setLinks, String cardSet, int multiverseId, String cardName) { CardCriteria criteria = new CardCriteria(); criteria.name(cardName); criteria.setCodes(cardSet); List cards = CardRepository.instance.findCards(criteria); String urlLandDocument = "https://gatherer.wizards.com/Pages/Card/Details.aspx?multiverseid=" + multiverseId; - Document landDoc = CardImageUtils.downloadHtmlDocument(urlLandDocument); + String sourceData = XmageURLConnection.downloadText(urlLandDocument); + Document landDoc = Jsoup.parse(sourceData); Elements variations = landDoc.select("a.variationlink"); if (!variations.isEmpty()) { if (variations.size() > cards.size()) { @@ -617,9 +621,10 @@ public enum WizardCardsImageSource implements CardImageSource { } } - private Map getlocalizedMultiverseIds(Integer englishMultiverseId) throws IOException { + private Map getlocalizedMultiverseIds(Integer englishMultiverseId) { String cardLanguagesUrl = "https://gatherer.wizards.com/Pages/Card/Languages.aspx?multiverseid=" + englishMultiverseId; - Document cardLanguagesDoc = CardImageUtils.downloadHtmlDocument(cardLanguagesUrl); + String sourceData = XmageURLConnection.downloadText(cardLanguagesUrl); + Document cardLanguagesDoc = Jsoup.parse(sourceData); Elements languageTableRows = cardLanguagesDoc.select("tr.cardItem"); Map localizedIds = new HashMap<>(); if (!languageTableRows.isEmpty()) { diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/images/DownloadPicturesService.java b/Mage.Client/src/main/java/org/mage/plugins/card/images/DownloadPicturesService.java index 492bdce5d4b..3e337f2d88a 100644 --- a/Mage.Client/src/main/java/org/mage/plugins/card/images/DownloadPicturesService.java +++ b/Mage.Client/src/main/java/org/mage/plugins/card/images/DownloadPicturesService.java @@ -9,12 +9,12 @@ import mage.cards.repository.TokenRepository; import mage.client.MageFrame; import mage.client.dialog.DownloadImagesDialog; import mage.client.dialog.PreferencesDialog; +import mage.client.remote.XmageURLConnection; import mage.client.util.CardLanguage; import mage.client.util.GUISizeHelper; import mage.client.util.sets.ConstructedFormats; -import mage.remote.Connection; import mage.util.ThreadUtils; -import mage.util.XMageThreadFactory; +import mage.util.XmageThreadFactory; import net.java.truevfs.access.TFile; import net.java.truevfs.access.TFileInputStream; import net.java.truevfs.access.TFileOutputStream; @@ -31,7 +31,8 @@ import java.awt.*; import java.awt.event.ItemEvent; import java.awt.image.BufferedImage; import java.io.*; -import java.net.*; +import java.net.SocketException; +import java.net.UnknownHostException; import java.nio.file.AccessDeniedException; import java.util.List; import java.util.*; @@ -83,7 +84,6 @@ public class DownloadPicturesService extends DefaultBoundedRangeModel implements private static CardImageSource selectedSource; private final Object sync = new Object(); - private Proxy proxy = Proxy.NO_PROXY; enum DownloadSources { WIZARDS("1. wizards.com - low quality, cards only", WizardCardsImageSource.instance), @@ -635,97 +635,72 @@ public class DownloadPicturesService extends DefaultBoundedRangeModel implements base.mkdir(); } - Connection.ProxyType configProxyType = Connection.ProxyType.valueByText(PreferencesDialog.getCachedValue(PreferencesDialog.KEY_PROXY_TYPE, "None")); - Proxy.Type type = Proxy.Type.DIRECT; - switch (configProxyType) { - case HTTP: - type = Proxy.Type.HTTP; - break; - case SOCKS: - type = Proxy.Type.SOCKS; - break; - case NONE: - default: - proxy = Proxy.NO_PROXY; - break; - } - - if (type != Proxy.Type.DIRECT) { - try { - String address = PreferencesDialog.getCachedValue(PreferencesDialog.KEY_PROXY_ADDRESS, ""); - Integer port = Integer.parseInt(PreferencesDialog.getCachedValue(PreferencesDialog.KEY_PROXY_PORT, "80")); - proxy = new Proxy(type, new InetSocketAddress(address, port)); - } catch (Exception ex) { - throw new RuntimeException("Gui_DownloadPicturesService : error 1 - " + ex); - } - } - int downloadThreadsAmount = Math.max(1, Integer.parseInt((String) uiDialog.getDownloadThreadsCombo().getSelectedItem())); - if (proxy != null) { - logger.info("Started download of " + cardsDownloadQueue.size() + " images" - + " from source: " + selectedSource.getSourceName() - + ", language: " + selectedSource.getCurrentLanguage().getCode() - + ", threads: " + downloadThreadsAmount); - updateProgressMessage("Preparing download list..."); - if (selectedSource.prepareDownloadList(this, cardsDownloadQueue)) { - update(0, cardsDownloadQueue.size()); - ExecutorService executor = Executors.newFixedThreadPool( - downloadThreadsAmount, - new XMageThreadFactory(ThreadUtils.THREAD_PREFIX_CLIENT_IMAGES_DOWNLOADER, false) - ); - for (int i = 0; i < cardsDownloadQueue.size() && !this.isNeedCancel(); i++) { - try { - CardDownloadData card = cardsDownloadQueue.get(i); - logger.debug("Downloading image: " + card.getName() + " (" + card.getSet() + ')'); + logger.info("Started download of " + cardsDownloadQueue.size() + " images" + + " from source: " + selectedSource.getSourceName() + + ", language: " + selectedSource.getCurrentLanguage().getCode() + + ", threads: " + downloadThreadsAmount); + updateProgressMessage("Preparing download list..."); + if (selectedSource.prepareDownloadList(this, cardsDownloadQueue)) { + update(0, cardsDownloadQueue.size()); + ExecutorService executor = Executors.newFixedThreadPool( + downloadThreadsAmount, + new XmageThreadFactory(ThreadUtils.THREAD_PREFIX_CLIENT_IMAGES_DOWNLOADER, false) + ); + for (int i = 0; i < cardsDownloadQueue.size() && !this.isNeedCancel(); i++) { + try { + CardDownloadData card = cardsDownloadQueue.get(i); - CardImageUrls urls; - if (card.isToken()) { - if (!"0".equals(card.getCollectorId())) { - continue; - } - urls = selectedSource.generateTokenUrl(card); - } else { - urls = selectedSource.generateCardUrl(card); + logger.debug("Downloading image: " + card.getName() + " (" + card.getSet() + ')'); + + CardImageUrls urls; + if (card.isToken()) { + if (!"0".equals(card.getCollectorId())) { + continue; } - - if (urls == null) { - String imageRef = selectedSource.getNextHttpImageUrl(); - String fileName = selectedSource.getFileForHttpImage(imageRef); - if (imageRef != null && fileName != null) { - imageRef = selectedSource.getSourceName() + imageRef; - try { - card.setToken(selectedSource.isTokenSource()); - Runnable task = new DownloadTask(card, imageRef, fileName, selectedSource.getTotalImages()); - executor.execute(task); - } catch (Exception ex) { - } - } else if (selectedSource.getTotalImages() == -1) { - logger.info("Image not available on " + selectedSource.getSourceName() + ": " + card.getName() + " (" + card.getSet() + ')'); - synchronized (sync) { - update(cardIndex + 1, cardsDownloadQueue.size()); - } - } - } else { - Runnable task = new DownloadTask(card, urls, cardsDownloadQueue.size()); - executor.execute(task); - } - } catch (Exception ex) { - logger.error(ex, ex); + urls = selectedSource.generateTokenUrl(card); + } else { + urls = selectedSource.generateCardUrl(card); } + + if (urls == null) { + String imageRef = selectedSource.getNextHttpImageUrl(); + String fileName = selectedSource.getFileForHttpImage(imageRef); + if (imageRef != null && fileName != null) { + imageRef = selectedSource.getSourceName() + imageRef; + try { + card.setToken(selectedSource.isTokenSource()); + Runnable task = new DownloadTask(card, imageRef, fileName, selectedSource.getTotalImages()); + executor.execute(task); + } catch (Exception ex) { + } + } else if (selectedSource.getTotalImages() == -1) { + logger.info("Image not available on " + selectedSource.getSourceName() + ": " + card.getName() + " (" + card.getSet() + ')'); + synchronized (sync) { + update(cardIndex + 1, cardsDownloadQueue.size()); + } + } + } else { + Runnable task = new DownloadTask(card, urls, cardsDownloadQueue.size()); + executor.execute(task); + } + } catch (Exception ex) { + logger.error(ex, ex); } + } - executor.shutdown(); - while (!executor.isTerminated()) { - try { - TimeUnit.SECONDS.sleep(1); - } catch (InterruptedException ignore) { - } + executor.shutdown(); + while (!executor.isTerminated()) { + try { + TimeUnit.SECONDS.sleep(1); + } catch (InterruptedException ignore) { } } } + try { TVFS.umount(); } catch (FsSyncException e) { @@ -743,11 +718,6 @@ public class DownloadPicturesService extends DefaultBoundedRangeModel implements GUISizeHelper.refreshGUIAndCards(false); } - static String convertStreamToString(InputStream is) { - java.util.Scanner s = new java.util.Scanner(is).useDelimiter("\\A"); - return s.hasNext() ? s.next() : ""; - } - private final class DownloadTask implements Runnable { private final CardDownloadData card; @@ -834,17 +804,17 @@ public class DownloadPicturesService extends DefaultBoundedRangeModel implements // can download images from many alternative urls List downloadUrls; + XmageURLConnection connection = null; if (this.urls != null) { downloadUrls = this.urls.getDownloadList(); } else { downloadUrls = new ArrayList<>(); } + // try to find first workable link boolean isDownloadOK = false; - URLConnection httpConn = null; List errorsList = new ArrayList<>(); for (String currentUrl : downloadUrls) { - URL url = new URL(currentUrl); // fast stop on cancel if (DownloadPicturesService.getInstance().isNeedCancel()) { @@ -852,29 +822,27 @@ public class DownloadPicturesService extends DefaultBoundedRangeModel implements } // timeout before each request - selectedSource.doPause(url.toString()); + selectedSource.doPause(currentUrl); - httpConn = url.openConnection(proxy); - if (httpConn != null) { + connection = new XmageURLConnection(currentUrl); + connection.startConnection(); + if (connection.isConnected()) { - // custom headers like user agent - Map headers = selectedSource.getHttpRequestHeaders(url.toString()); - for (String key : headers.keySet()) { - httpConn.setRequestProperty(key, headers.get(key)); - } + // custom headers (ues + connection.setRequestHeaders(selectedSource.getHttpRequestHeaders(currentUrl)); try { - httpConn.connect(); + connection.connect(); } catch (SocketException e) { incErrorCount(); - errorsList.add("Wrong image URL or java app is not allowed to use network. Check your firewall or proxy settings. Error: " + e.getMessage() + ". Image URL: " + url.toString()); + errorsList.add("Wrong image URL or java app is not allowed to use network. Check your firewall or proxy settings. Error: " + e.getMessage() + ". Image URL: " + currentUrl); break; } catch (UnknownHostException e) { incErrorCount(); - errorsList.add("Unknown site. Check your DNS settings. Error: " + e.getMessage() + ". Image URL: " + url.toString()); + errorsList.add("Unknown site. Check your DNS settings. Error: " + e.getMessage() + ". Image URL: " + currentUrl); break; } - int responseCode = ((HttpURLConnection) httpConn).getResponseCode(); + int responseCode = connection.getResponseCode(); // check result if (responseCode != 200) { @@ -883,13 +851,13 @@ public class DownloadPicturesService extends DefaultBoundedRangeModel implements card.getSet(), card.getName(), responseCode, - url + currentUrl ); errorsList.add(info); if (logger.isDebugEnabled()) { // Shows the returned html from the request to the web server - logger.debug("Returned HTML ERROR:\n" + convertStreamToString(((HttpURLConnection) httpConn).getErrorStream())); + logger.debug("Returned HTML ERROR:\n" + connection.getErrorResponseAsString()); } // go to next try @@ -902,12 +870,12 @@ public class DownloadPicturesService extends DefaultBoundedRangeModel implements } } - // can save result - if (isDownloadOK && httpConn != null) { + // if workable link found then save result + if (isDownloadOK && connection.isConnected()) { // save data to temp - try (InputStream in = new BufferedInputStream(httpConn.getInputStream()); - OutputStream tfileout = new TFileOutputStream(fileTempImage); - OutputStream out = new BufferedOutputStream(tfileout)) { + try (InputStream in = new BufferedInputStream(connection.getGoodResponseAsStream()); + OutputStream tempFileStream = new TFileOutputStream(fileTempImage); + OutputStream out = new BufferedOutputStream(tempFileStream)) { byte[] buf = new byte[1024]; int len; while ((len = in.read(buf)) != -1) { @@ -931,6 +899,8 @@ public class DownloadPicturesService extends DefaultBoundedRangeModel implements } return; } + + // all fine, can save data part out.write(buf, 0, len); } } @@ -947,7 +917,6 @@ public class DownloadPicturesService extends DefaultBoundedRangeModel implements } catch (Exception e) { logger.error("Can't delete temp file: " + e.getMessage(), e); } - } } else { // download errors @@ -1013,11 +982,6 @@ public class DownloadPicturesService extends DefaultBoundedRangeModel implements uiDialog.getStartButton().setEnabled(true); } - @Override - public Proxy getProxy() { - return proxy; - } - @Override public Object getSync() { return sync; diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/utils/CardImageUtils.java b/Mage.Client/src/main/java/org/mage/plugins/card/utils/CardImageUtils.java index 64cf2bd90b3..e61012c8019 100644 --- a/Mage.Client/src/main/java/org/mage/plugins/card/utils/CardImageUtils.java +++ b/Mage.Client/src/main/java/org/mage/plugins/card/utils/CardImageUtils.java @@ -6,6 +6,7 @@ import mage.cards.repository.TokenRepository; import mage.client.MageFrame; import mage.client.constants.Constants; import mage.client.dialog.PreferencesDialog; +import mage.client.remote.XmageURLConnection; import mage.remote.Connection; import mage.remote.Connection.ProxyType; import mage.view.CardView; @@ -201,33 +202,6 @@ public final class CardImageUtils { return null; } - public static Document downloadHtmlDocument(String urlString) throws NumberFormatException, IOException { - Preferences prefs = MageFrame.getPreferences(); - Connection.ProxyType proxyType = Connection.ProxyType.valueByText(prefs.get("proxyType", "None")); - Document doc; - if (proxyType == ProxyType.NONE) { - doc = Jsoup.connect(urlString).timeout(60 * 1000).get(); - } else { - String proxyServer = prefs.get("proxyAddress", ""); - int proxyPort = Integer.parseInt(prefs.get("proxyPort", "0")); - URL url = new URL(urlString); - Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyServer, proxyPort)); - HttpURLConnection uc = (HttpURLConnection) url.openConnection(proxy); - uc.setConnectTimeout(10000); - uc.setReadTimeout(60000); - uc.connect(); - - String line; - StringBuffer tmp = new StringBuffer(); - BufferedReader in = new BufferedReader(new InputStreamReader(uc.getInputStream())); - while ((line = in.readLine()) != null) { - tmp.append(line); - } - doc = Jsoup.parse(String.valueOf(tmp)); - } - return doc; - } - public static void checkAndFixImageFiles() { // search broken, temp or outdated files and delete it // real images check is slow, so it used on images download only (not here) diff --git a/Mage.Client/src/test/java/mage/client/util/DownloaderTest.java b/Mage.Client/src/test/java/mage/client/util/DownloaderTest.java new file mode 100644 index 00000000000..6898daf1ed5 --- /dev/null +++ b/Mage.Client/src/test/java/mage/client/util/DownloaderTest.java @@ -0,0 +1,48 @@ +package mage.client.util; + +import mage.client.remote.XmageURLConnection; +import org.junit.Assert; +import org.junit.Test; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.io.InputStream; + +/** + * @author JayDi85 + */ +public class DownloaderTest { + + @Test + public void test_DownloadText_ByHttp() { + String s = XmageURLConnection.downloadText("http://google.com"); + Assert.assertTrue("must have text data", s.contains("")); + } + + @Test + public void test_DownloadText_ByHttps() { + String s = XmageURLConnection.downloadText("https://google.com"); + Assert.assertTrue("must have text data", s.contains("")); + } + + @Test + public void test_DownloadFile_ByHttp() throws IOException { + // use any public image here + InputStream stream = XmageURLConnection.downloadBinary("http://www.google.com/tia/tia.png"); + Assert.assertNotNull(stream); + BufferedImage image = ImageIO.read(stream); + Assert.assertNotNull(stream); + Assert.assertTrue("must have image data", image.getWidth() > 0); + } + + @Test + public void test_DownloadFile_ByHttps() throws IOException { + // use any public image here + InputStream stream = XmageURLConnection.downloadBinary("https://www.google.com/tia/tia.png"); + Assert.assertNotNull(stream); + BufferedImage image = ImageIO.read(stream); + Assert.assertNotNull(stream); + Assert.assertTrue("must have image data", image.getWidth() > 0); + } +} diff --git a/Mage.Common/src/main/java/mage/utils/MageVersion.java b/Mage.Common/src/main/java/mage/utils/MageVersion.java index 24ac2d102f6..a991e6999cf 100644 --- a/Mage.Common/src/main/java/mage/utils/MageVersion.java +++ b/Mage.Common/src/main/java/mage/utils/MageVersion.java @@ -91,4 +91,8 @@ public class MageVersion implements Serializable, Comparable { public boolean isDeveloperBuild() { return this.buildTime.contains(JarVersion.JAR_BUILD_TIME_FROM_CLASSES); } + + public String getBuildTime() { + return this.buildTime; + } } diff --git a/Mage.Server.Console/src/main/java/mage/server/console/ConsoleFrame.java b/Mage.Server.Console/src/main/java/mage/server/console/ConsoleFrame.java index 393b8efba55..338b6761e25 100644 --- a/Mage.Server.Console/src/main/java/mage/server/console/ConsoleFrame.java +++ b/Mage.Server.Console/src/main/java/mage/server/console/ConsoleFrame.java @@ -6,7 +6,7 @@ import mage.remote.Connection; import mage.remote.Session; import mage.remote.SessionImpl; import mage.util.ThreadUtils; -import mage.util.XMageThreadFactory; +import mage.util.XmageThreadFactory; import mage.utils.MageVersion; import org.apache.log4j.Logger; @@ -32,7 +32,7 @@ public class ConsoleFrame extends javax.swing.JFrame implements MageClient { private static final MageVersion version = new MageVersion(ConsoleFrame.class); private static final ScheduledExecutorService PING_SENDER_EXECUTOR = Executors.newSingleThreadScheduledExecutor( - new XMageThreadFactory(ThreadUtils.THREAD_PREFIX_CLIENT_PING_SENDER) + new XmageThreadFactory(ThreadUtils.THREAD_PREFIX_CLIENT_PING_SENDER) ); /** diff --git a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer6.java b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer6.java index 0498e89f07c..d3fc038f720 100644 --- a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer6.java +++ b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer6.java @@ -32,7 +32,7 @@ import mage.target.TargetCard; import mage.util.CardUtil; import mage.util.RandomUtil; import mage.util.ThreadUtils; -import mage.util.XMageThreadFactory; +import mage.util.XmageThreadFactory; import org.apache.log4j.Logger; import java.util.*; @@ -61,7 +61,7 @@ public class ComputerPlayer6 extends ComputerPlayer { 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(), - new XMageThreadFactory(ThreadUtils.THREAD_PREFIX_AI_SIMULATION_MAD) + new XmageThreadFactory(ThreadUtils.THREAD_PREFIX_AI_SIMULATION_MAD) ); protected int maxDepth; protected int maxNodes; diff --git a/Mage.Server.Plugins/Mage.Player.AIMCTS/src/mage/player/ai/ComputerPlayerMCTS.java b/Mage.Server.Plugins/Mage.Player.AIMCTS/src/mage/player/ai/ComputerPlayerMCTS.java index 1b19a95b4dc..f50503d8d2a 100644 --- a/Mage.Server.Plugins/Mage.Player.AIMCTS/src/mage/player/ai/ComputerPlayerMCTS.java +++ b/Mage.Server.Plugins/Mage.Player.AIMCTS/src/mage/player/ai/ComputerPlayerMCTS.java @@ -13,7 +13,7 @@ import mage.game.combat.CombatGroup; import mage.player.ai.MCTSPlayer.NextAction; import mage.players.Player; import mage.util.ThreadUtils; -import mage.util.XMageThreadFactory; +import mage.util.XmageThreadFactory; import org.apache.log4j.Logger; import java.util.ArrayList; @@ -173,7 +173,7 @@ public class ComputerPlayerMCTS extends ComputerPlayer { 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(), - new XMageThreadFactory(ThreadUtils.THREAD_PREFIX_AI_SIMULATION_MCTS) // TODO: add player/game to thread name? + new XmageThreadFactory(ThreadUtils.THREAD_PREFIX_AI_SIMULATION_MCTS) // TODO: add player/game to thread name? ); } diff --git a/Mage.Server/src/main/java/mage/server/UserManagerImpl.java b/Mage.Server/src/main/java/mage/server/UserManagerImpl.java index 5ca39c8c2c8..571142a9b8d 100644 --- a/Mage.Server/src/main/java/mage/server/UserManagerImpl.java +++ b/Mage.Server/src/main/java/mage/server/UserManagerImpl.java @@ -6,7 +6,7 @@ import mage.server.record.UserStats; import mage.server.record.UserStatsRepository; import mage.server.util.ServerMessagesUtil; import mage.util.ThreadUtils; -import mage.util.XMageThreadFactory; +import mage.util.XmageThreadFactory; import mage.view.UserView; import org.apache.log4j.Logger; @@ -34,10 +34,10 @@ public class UserManagerImpl implements UserManager { private static final Logger logger = Logger.getLogger(UserManagerImpl.class); protected final ScheduledExecutorService CONNECTION_EXPIRED_EXECUTOR = Executors.newSingleThreadScheduledExecutor( - new XMageThreadFactory(ThreadUtils.THREAD_PREFIX_SERVICE_CONNECTION_EXPIRED_CHECK) + new XmageThreadFactory(ThreadUtils.THREAD_PREFIX_SERVICE_CONNECTION_EXPIRED_CHECK) ); protected final ScheduledExecutorService USERS_LIST_REFRESH_EXECUTOR = Executors.newSingleThreadScheduledExecutor( - new XMageThreadFactory(ThreadUtils.THREAD_PREFIX_SERVICE_USERS_LIST_REFRESH) + new XmageThreadFactory(ThreadUtils.THREAD_PREFIX_SERVICE_USERS_LIST_REFRESH) ); private List userInfoList = new ArrayList<>(); // all users list for main room/chat diff --git a/Mage.Server/src/main/java/mage/server/game/GameController.java b/Mage.Server/src/main/java/mage/server/game/GameController.java index 53f5a733abc..44778e69aa2 100644 --- a/Mage.Server/src/main/java/mage/server/game/GameController.java +++ b/Mage.Server/src/main/java/mage/server/game/GameController.java @@ -26,7 +26,7 @@ import mage.server.User; import mage.server.managers.ManagerFactory; import mage.util.MultiAmountMessage; import mage.util.ThreadUtils; -import mage.util.XMageThreadFactory; +import mage.util.XmageThreadFactory; import mage.utils.StreamUtils; import mage.utils.timer.PriorityTimer; import mage.view.*; @@ -244,7 +244,7 @@ public class GameController implements GameCallback { // wait all players if (JOIN_WAITING_EXECUTOR == null) { JOIN_WAITING_EXECUTOR = Executors.newSingleThreadScheduledExecutor( - new XMageThreadFactory(ThreadUtils.THREAD_PREFIX_GAME_JOIN_WAITING + " " + game.getId()) + new XmageThreadFactory(ThreadUtils.THREAD_PREFIX_GAME_JOIN_WAITING + " " + game.getId()) ); } JOIN_WAITING_EXECUTOR.scheduleAtFixedRate(() -> { diff --git a/Mage.Server/src/main/java/mage/server/game/GamesRoomImpl.java b/Mage.Server/src/main/java/mage/server/game/GamesRoomImpl.java index a86b9c716e4..052bbdb0130 100644 --- a/Mage.Server/src/main/java/mage/server/game/GamesRoomImpl.java +++ b/Mage.Server/src/main/java/mage/server/game/GamesRoomImpl.java @@ -12,7 +12,7 @@ import mage.server.RoomImpl; import mage.server.User; import mage.server.managers.ManagerFactory; import mage.util.ThreadUtils; -import mage.util.XMageThreadFactory; +import mage.util.XmageThreadFactory; import mage.view.MatchView; import mage.view.RoomUsersView; import mage.view.TableView; @@ -40,7 +40,7 @@ public class GamesRoomImpl extends RoomImpl implements GamesRoom, Serializable { private static List lobbyMatches = new ArrayList<>(); private static List lobbyUsers = new ArrayList<>(); private static final ScheduledExecutorService UPDATE_LOBBY_EXECUTOR = Executors.newSingleThreadScheduledExecutor( - new XMageThreadFactory(ThreadUtils.THREAD_PREFIX_SERVICE_LOBBY_REFRESH) + new XmageThreadFactory(ThreadUtils.THREAD_PREFIX_SERVICE_LOBBY_REFRESH) ); private final ManagerFactory managerFactory; diff --git a/Mage.Server/src/main/java/mage/server/util/ServerMessagesUtil.java b/Mage.Server/src/main/java/mage/server/util/ServerMessagesUtil.java index 317f5841140..9b04583f10b 100644 --- a/Mage.Server/src/main/java/mage/server/util/ServerMessagesUtil.java +++ b/Mage.Server/src/main/java/mage/server/util/ServerMessagesUtil.java @@ -1,7 +1,7 @@ package mage.server.util; import mage.util.ThreadUtils; -import mage.util.XMageThreadFactory; +import mage.util.XmageThreadFactory; import mage.utils.StreamUtils; import org.apache.log4j.Logger; @@ -47,7 +47,7 @@ public enum ServerMessagesUtil { ServerMessagesUtil() { ScheduledExecutorService NEWS_MESSAGES_EXECUTOR = Executors.newSingleThreadScheduledExecutor( - new XMageThreadFactory(ThreadUtils.THREAD_PREFIX_SERVICE_NEWS_REFRESH) + new XmageThreadFactory(ThreadUtils.THREAD_PREFIX_SERVICE_NEWS_REFRESH) ); NEWS_MESSAGES_EXECUTOR.scheduleAtFixedRate(this::reloadMessages, 5, SERVER_MSG_REFRESH_RATE_SECS, TimeUnit.SECONDS); } diff --git a/Mage.Server/src/main/java/mage/server/util/ThreadExecutorImpl.java b/Mage.Server/src/main/java/mage/server/util/ThreadExecutorImpl.java index ddffccc083c..cd475219829 100644 --- a/Mage.Server/src/main/java/mage/server/util/ThreadExecutorImpl.java +++ b/Mage.Server/src/main/java/mage/server/util/ThreadExecutorImpl.java @@ -3,7 +3,7 @@ package mage.server.util; import mage.server.managers.ConfigSettings; import mage.server.managers.ThreadExecutor; import mage.util.ThreadUtils; -import mage.util.XMageThreadFactory; +import mage.util.XmageThreadFactory; import org.apache.log4j.Logger; import java.util.concurrent.*; @@ -42,31 +42,31 @@ public class ThreadExecutorImpl implements ThreadExecutor { callExecutor = new CachedThreadPoolWithException(); ((ThreadPoolExecutor) callExecutor).setKeepAliveTime(60, TimeUnit.SECONDS); ((ThreadPoolExecutor) callExecutor).allowCoreThreadTimeOut(true); - ((ThreadPoolExecutor) callExecutor).setThreadFactory(new XMageThreadFactory(ThreadUtils.THREAD_PREFIX_CALL_REQUEST)); + ((ThreadPoolExecutor) callExecutor).setThreadFactory(new XmageThreadFactory(ThreadUtils.THREAD_PREFIX_CALL_REQUEST)); //gameExecutor = Executors.newFixedThreadPool(config.getMaxGameThreads()); gameExecutor = new FixedThreadPoolWithException(config.getMaxGameThreads()); ((ThreadPoolExecutor) gameExecutor).setKeepAliveTime(60, TimeUnit.SECONDS); ((ThreadPoolExecutor) gameExecutor).allowCoreThreadTimeOut(true); - ((ThreadPoolExecutor) gameExecutor).setThreadFactory(new XMageThreadFactory(ThreadUtils.THREAD_PREFIX_GAME)); + ((ThreadPoolExecutor) gameExecutor).setThreadFactory(new XmageThreadFactory(ThreadUtils.THREAD_PREFIX_GAME)); //tourney = Executors.newFixedThreadPool(config.getMaxGameThreads() / GAMES_PER_TOURNEY_RATIO); tourneyExecutor = new FixedThreadPoolWithException(config.getMaxGameThreads() / GAMES_PER_TOURNEY_RATIO); ((ThreadPoolExecutor) tourneyExecutor).setKeepAliveTime(60, TimeUnit.SECONDS); ((ThreadPoolExecutor) tourneyExecutor).allowCoreThreadTimeOut(true); - ((ThreadPoolExecutor) tourneyExecutor).setThreadFactory(new XMageThreadFactory(ThreadUtils.THREAD_PREFIX_TOURNEY)); + ((ThreadPoolExecutor) tourneyExecutor).setThreadFactory(new XmageThreadFactory(ThreadUtils.THREAD_PREFIX_TOURNEY)); timeoutExecutor = Executors.newScheduledThreadPool(4); ((ThreadPoolExecutor) timeoutExecutor).setKeepAliveTime(60, TimeUnit.SECONDS); ((ThreadPoolExecutor) timeoutExecutor).allowCoreThreadTimeOut(true); - ((ThreadPoolExecutor) timeoutExecutor).setThreadFactory(new XMageThreadFactory(ThreadUtils.THREAD_PREFIX_TIMEOUT)); + ((ThreadPoolExecutor) timeoutExecutor).setThreadFactory(new XmageThreadFactory(ThreadUtils.THREAD_PREFIX_TIMEOUT)); timeoutIdleExecutor = Executors.newScheduledThreadPool(4); ((ThreadPoolExecutor) timeoutIdleExecutor).setKeepAliveTime(60, TimeUnit.SECONDS); ((ThreadPoolExecutor) timeoutIdleExecutor).allowCoreThreadTimeOut(true); - ((ThreadPoolExecutor) timeoutIdleExecutor).setThreadFactory(new XMageThreadFactory(ThreadUtils.THREAD_PREFIX_TIMEOUT_IDLE)); + ((ThreadPoolExecutor) timeoutIdleExecutor).setThreadFactory(new XmageThreadFactory(ThreadUtils.THREAD_PREFIX_TIMEOUT_IDLE)); - serverHealthExecutor = Executors.newSingleThreadScheduledExecutor(new XMageThreadFactory(ThreadUtils.THREAD_PREFIX_SERVICE_HEALTH)); + serverHealthExecutor = Executors.newSingleThreadScheduledExecutor(new XmageThreadFactory(ThreadUtils.THREAD_PREFIX_SERVICE_HEALTH)); } static class CachedThreadPoolWithException extends ThreadPoolExecutor { diff --git a/Mage.Tests/src/test/java/org/mage/test/load/LoadTest.java b/Mage.Tests/src/test/java/org/mage/test/load/LoadTest.java index 1f587846341..e2da4f62218 100644 --- a/Mage.Tests/src/test/java/org/mage/test/load/LoadTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/load/LoadTest.java @@ -14,7 +14,7 @@ import mage.remote.Session; import mage.remote.SessionImpl; import mage.util.RandomUtil; import mage.util.ThreadUtils; -import mage.util.XMageThreadFactory; +import mage.util.XmageThreadFactory; import mage.view.*; import org.apache.log4j.Logger; import org.junit.Assert; @@ -328,9 +328,9 @@ public class LoadTest { ExecutorService executerService; if (isRunParallel) { - executerService = Executors.newFixedThreadPool(gamesAmount, new XMageThreadFactory(ThreadUtils.THREAD_PREFIX_TESTS_AI_VS_AI_GAMES)); + executerService = Executors.newFixedThreadPool(gamesAmount, new XmageThreadFactory(ThreadUtils.THREAD_PREFIX_TESTS_AI_VS_AI_GAMES)); } else { - executerService = Executors.newSingleThreadExecutor(new XMageThreadFactory(ThreadUtils.THREAD_PREFIX_TESTS_AI_VS_AI_GAMES)); + executerService = Executors.newSingleThreadExecutor(new XmageThreadFactory(ThreadUtils.THREAD_PREFIX_TESTS_AI_VS_AI_GAMES)); } // save random seeds for repeated results (in decks generating) diff --git a/Mage.Verify/src/main/java/mage/verify/mtgjson/MtgJsonCard.java b/Mage.Verify/src/main/java/mage/verify/mtgjson/MtgJsonCard.java index 3386ce12af4..c8f63b04098 100644 --- a/Mage.Verify/src/main/java/mage/verify/mtgjson/MtgJsonCard.java +++ b/Mage.Verify/src/main/java/mage/verify/mtgjson/MtgJsonCard.java @@ -4,11 +4,16 @@ import mage.constants.Rarity; import java.util.List; +/** + * MTGJSON v5: card class + *

+ * Contains card related data nd only used fields, if you need more for tests then just add it here + *

+ * API docs here + * + * @author JayDi85 + */ public final class MtgJsonCard { - // v5 support - // https://mtgjson.com/data-models/card-atomic/ - // contains only used fields, if you need more for tests then just add it here - public String name; public String asciiName; // mtgjson uses it for some cards like El-Hajjaj public String number; // from sets source only, see https://mtgjson.com/data-models/card-set/ @@ -44,7 +49,6 @@ public final class MtgJsonCard { } /** - * * @return single side name like Ice from Fire // Ice */ public String getNameAsFace() { @@ -53,7 +57,6 @@ public final class MtgJsonCard { } /** - * * @return full card name like Fire // Ice */ public String getNameAsFull() { diff --git a/Mage.Verify/src/main/java/mage/verify/mtgjson/MtgJsonMetadata.java b/Mage.Verify/src/main/java/mage/verify/mtgjson/MtgJsonMetadata.java index 10c10dfae35..a9b117dabf6 100644 --- a/Mage.Verify/src/main/java/mage/verify/mtgjson/MtgJsonMetadata.java +++ b/Mage.Verify/src/main/java/mage/verify/mtgjson/MtgJsonMetadata.java @@ -1,9 +1,15 @@ package mage.verify.mtgjson; +/** + * MTGJSON v5: metadata class + *

+ * Contains version info + *

+ * API docs here + * + * @author JayDi85 + */ public final class MtgJsonMetadata { - // MTGJSON metadata - // https://mtgjson.com/file-models/meta/ - public String date; public String version; } diff --git a/Mage.Verify/src/main/java/mage/verify/mtgjson/MtgJsonService.java b/Mage.Verify/src/main/java/mage/verify/mtgjson/MtgJsonService.java index 5bc45c901b8..62ca7fdb5da 100644 --- a/Mage.Verify/src/main/java/mage/verify/mtgjson/MtgJsonService.java +++ b/Mage.Verify/src/main/java/mage/verify/mtgjson/MtgJsonService.java @@ -1,10 +1,13 @@ package mage.verify.mtgjson; import com.google.gson.Gson; +import mage.client.remote.XmageURLConnection; +import org.apache.log4j.Logger; -import java.io.*; -import java.net.URL; -import java.net.URLConnection; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; import java.nio.file.Files; import java.nio.file.StandardCopyOption; import java.text.Normalizer; @@ -12,8 +15,15 @@ import java.util.*; import java.util.stream.Collectors; import java.util.zip.ZipInputStream; +/** + * MTGJSON v5: basic service to work with mtgjson data + * + * @author JayDi85 + */ public final class MtgJsonService { + private static final Logger logger = Logger.getLogger(MtgJsonService.class); + public static Map mtgJsonToXMageCodes = new HashMap<>(); public static Map xMageToMtgJsonCodes = new HashMap<>(); @@ -36,23 +46,36 @@ public final class MtgJsonService { } private static T readFromZip(String filename, Class clazz) throws IOException { + + // build-in file InputStream stream = MtgJsonService.class.getResourceAsStream(filename); - if (stream == null) { - File file = new File(filename); - if (!file.exists()) { - String url = "https://mtgjson.com/api/v5/" + filename; - System.out.println("Downloading " + url + " to " + file.getAbsolutePath()); - URLConnection connection = new URL(url).openConnection(); - connection.setRequestProperty("user-agent", "xmage"); - InputStream download = connection.getInputStream(); - Files.copy(download, file.toPath(), StandardCopyOption.REPLACE_EXISTING); - System.out.println("Downloading DONE"); - } else { - System.out.println("Found file " + filename + " from " + file.getAbsolutePath()); - } - stream = new FileInputStream(file); + if (stream != null) { + logger.info("mtgjson: use build-in file " + filename); + return readFromZip(stream, clazz); } + // already downloaded file + File file = new File(filename); + if (file.exists()) { + logger.info("mtgjson: use existing file " + filename + " from " + file.getAbsolutePath()); + return readFromZip(Files.newInputStream(file.toPath()), clazz); + } + + // new download + String url = "https://mtgjson.com/api/v5/" + filename; + logger.info("mtgjson: downloading new file " + url); + // mtgjson site require user-agent in headers (otherwise it return 403) + stream = XmageURLConnection.downloadBinary(url); + if (stream != null) { + logger.info("mtgjson: download DONE, saved to " + file.getAbsolutePath()); + Files.copy(stream, file.toPath(), StandardCopyOption.REPLACE_EXISTING); + return readFromZip(Files.newInputStream(file.toPath()), clazz); + } + + throw new IOException("mtgjson: can't found or download file, check your connection " + filename); + } + + private static T readFromZip(InputStream stream, Class clazz) throws IOException { try (ZipInputStream zipInputStream = new ZipInputStream(stream)) { zipInputStream.getNextEntry(); return new Gson().fromJson(new InputStreamReader(zipInputStream), clazz); @@ -243,5 +266,4 @@ public final class MtgJsonService { } } } - } diff --git a/Mage.Verify/src/main/java/mage/verify/mtgjson/MtgJsonSet.java b/Mage.Verify/src/main/java/mage/verify/mtgjson/MtgJsonSet.java index f302ec8e02c..4e9a48aee2a 100644 --- a/Mage.Verify/src/main/java/mage/verify/mtgjson/MtgJsonSet.java +++ b/Mage.Verify/src/main/java/mage/verify/mtgjson/MtgJsonSet.java @@ -2,10 +2,17 @@ package mage.verify.mtgjson; import java.util.List; +/** + * MTGJSON v5: set class + *

+ * Contains set info and related cards list + * Only used fields, if you need more for tests then just add it here + *

+ * API docs here + * + * @author JayDi85 + */ public final class MtgJsonSet { - // v5 support - // https://mtgjson.com/data-models/card-atomic/ - // contains only used fields, if you need more for tests then just add it here public List cards; public String code; diff --git a/Mage/src/main/java/mage/game/draft/DraftImpl.java b/Mage/src/main/java/mage/game/draft/DraftImpl.java index f9a08345f53..4f2ff6883db 100644 --- a/Mage/src/main/java/mage/game/draft/DraftImpl.java +++ b/Mage/src/main/java/mage/game/draft/DraftImpl.java @@ -8,7 +8,7 @@ import mage.game.events.TableEvent.EventType; import mage.players.Player; import mage.players.PlayerList; import mage.util.ThreadUtils; -import mage.util.XMageThreadFactory; +import mage.util.XmageThreadFactory; import org.apache.log4j.Logger; import java.util.*; @@ -248,7 +248,7 @@ public abstract class DraftImpl implements Draft { if (this.boosterLoadingExecutor == null) { this.boosterLoadingExecutor = Executors.newSingleThreadScheduledExecutor( - new XMageThreadFactory(ThreadUtils.THREAD_PREFIX_TOURNEY_BOOSTERS_SEND) + new XmageThreadFactory(ThreadUtils.THREAD_PREFIX_TOURNEY_BOOSTERS_SEND) ); } diff --git a/Mage/src/main/java/mage/util/XMageThreadFactory.java b/Mage/src/main/java/mage/util/XmageThreadFactory.java similarity index 86% rename from Mage/src/main/java/mage/util/XMageThreadFactory.java rename to Mage/src/main/java/mage/util/XmageThreadFactory.java index be96e40fc9a..d80ccaa308a 100644 --- a/Mage/src/main/java/mage/util/XMageThreadFactory.java +++ b/Mage/src/main/java/mage/util/XmageThreadFactory.java @@ -8,13 +8,13 @@ import java.util.concurrent.atomic.AtomicInteger; * * @author JayDi85 */ -public class XMageThreadFactory implements ThreadFactory { +public class XmageThreadFactory implements ThreadFactory { private final String prefix; private final AtomicInteger counter = new AtomicInteger(); private final boolean isDaemon; - public XMageThreadFactory(String prefix) { + public XmageThreadFactory(String prefix) { this(prefix, true); } @@ -22,7 +22,7 @@ public class XMageThreadFactory implements ThreadFactory { * @param prefix thread's starting name (can be changed by thread itself later) * @param isDaemon mark thread as daemon on non-writeable tasks (e.g. can be terminated at any time without data loss) */ - public XMageThreadFactory(String prefix, boolean isDaemon) { + public XmageThreadFactory(String prefix, boolean isDaemon) { this.prefix = prefix; this.isDaemon = isDaemon; }