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;
This commit is contained in:
Oleg Agafonov 2024-07-31 16:24:59 +04:00
parent fad63389d0
commit e1cffbde40
35 changed files with 713 additions and 464 deletions

View file

@ -46,7 +46,7 @@ import mage.remote.Connection;
import mage.remote.Connection.ProxyType; import mage.remote.Connection.ProxyType;
import mage.util.DebugUtil; import mage.util.DebugUtil;
import mage.util.ThreadUtils; import mage.util.ThreadUtils;
import mage.util.XMageThreadFactory; import mage.util.XmageThreadFactory;
import mage.utils.MageVersion; import mage.utils.MageVersion;
import mage.view.GameEndView; import mage.view.GameEndView;
import mage.view.UserRequestMessage; 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 MageUI UI = new MageUI();
private static final ScheduledExecutorService PING_SENDER_EXECUTOR = Executors.newSingleThreadScheduledExecutor( 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; private static UpdateMemUsageTask updateMemUsageTask;

View file

@ -26,7 +26,7 @@ import mage.game.GameException;
import mage.remote.Session; import mage.remote.Session;
import mage.util.DeckUtil; import mage.util.DeckUtil;
import mage.util.ThreadUtils; import mage.util.ThreadUtils;
import mage.util.XMageThreadFactory; import mage.util.XmageThreadFactory;
import mage.view.CardView; import mage.view.CardView;
import mage.view.SimpleCardView; import mage.view.SimpleCardView;
import org.apache.log4j.Logger; 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 private void btnSubmitTimerActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_btnSubmitTimerActionPerformed
ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor( ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor(
new XMageThreadFactory(ThreadUtils.THREAD_PREFIX_CLIENT_SUBMIT_TIMER) new XmageThreadFactory(ThreadUtils.THREAD_PREFIX_CLIENT_SUBMIT_TIMER)
); );
timeToSubmit = 60; timeToSubmit = 60;
this.btnSubmitTimer.setEnabled(false); this.btnSubmitTimer.setEnabled(false);

View file

@ -9,7 +9,7 @@ import mage.client.util.gui.ArrowBuilder;
import mage.constants.PlayerAction; import mage.constants.PlayerAction;
import mage.constants.TurnPhase; import mage.constants.TurnPhase;
import mage.util.ThreadUtils; import mage.util.ThreadUtils;
import mage.util.XMageThreadFactory; import mage.util.XmageThreadFactory;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import javax.swing.*; 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 int AUTO_CLOSE_END_DIALOG_TIMEOUT_SECS = 8;
private static final ScheduledExecutorService AUTO_CLOSE_EXECUTOR = Executors.newSingleThreadScheduledExecutor( 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)
); );
/** /**

View file

@ -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
* <p>
* It used stream logic for data access
* <p>
* For text:
* - download text data by XmageURLConnection.downloadText
* <p>
* For binary data (e.g. file download):
* - get stream by XmageURLConnection.downloadBinary
* - save stream to file or process
* <p>
* 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<String, String> 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: <product> / <product-version> <comment>
// 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
* <p>
* 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));
}
}
}

View file

@ -22,13 +22,15 @@ import java.awt.image.BufferedImage;
import java.io.*; import java.io.*;
import java.nio.file.*; import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.BasicFileAttributes;
import java.util.List;
import java.util.*; import java.util.*;
import java.util.concurrent.ConcurrentHashMap; 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.regex.Pattern;
import java.util.stream.IntStream; import java.util.stream.IntStream;
/**
* Mana symbol resources
*/
public final class ManaSymbols { public final class ManaSymbols {
private static final Logger logger = Logger.getLogger(ManaSymbols.class); private static final Logger logger = Logger.getLogger(ManaSymbols.class);
@ -120,18 +122,13 @@ public final class ManaSymbols {
ImageIO.write(image, "png", newFile); ImageIO.write(image, "png", newFile);
} }
} catch (Exception e) { } 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 // preload set images
java.util.List<String> setCodes = ExpansionRepository.instance.getSetCodes(); java.util.List<String> 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) { for (String set : setCodes) {
if (withoutSymbols.contains(set)) { if (withoutSymbols.contains(set)) {
@ -289,9 +286,9 @@ public final class ManaSymbols {
// priority: SVG -> GIF // priority: SVG -> GIF
// gif remain for backward compatibility // gif remain for backward compatibility
AtomicIntegerArray iconErrors = new AtomicIntegerArray(2); // 0 - svg, 1 - gif final List<String> svgFails = new ArrayList<>(); // scryfall
final List<String> otherFails = new ArrayList<>(); // gatherer
AtomicBoolean fileErrors = new AtomicBoolean(false);
Map<String, BufferedImage> sizedSymbols = new ConcurrentHashMap<>(); Map<String, BufferedImage> sizedSymbols = new ConcurrentHashMap<>();
IntStream.range(0, symbols.length).parallel().forEach(i -> { IntStream.range(0, symbols.length).parallel().forEach(i -> {
String symbol = symbols[i]; String symbol = symbols[i];
@ -305,50 +302,53 @@ public final class ManaSymbols {
try { try {
InputStream fileStream = new FileInputStream(file); InputStream fileStream = new FileInputStream(file);
image = loadSymbolAsSVG(fileStream, file.getPath(), size, size); image = loadSymbolAsSVG(fileStream, file.getPath(), size, size);
} catch (FileNotFoundException e) { } catch (FileNotFoundException ignore) {
// it's ok to hide error
} }
} }
} }
// gif
if (image == null) { if (image == null) {
synchronized (svgFails) {
svgFails.add(symbol);
}
}
iconErrors.incrementAndGet(0); // svg fail // gif (if svg fails)
if (image == null) {
file = getSymbolFileNameAsGIF(symbol, size); file = getSymbolFileNameAsGIF(symbol, size);
if (file.exists()) { if (file.exists()) {
image = loadSymbolAsGIF(file, size, size); image = loadSymbolAsGIF(file, size, size);
} }
} }
if (image == null) {
synchronized (otherFails) {
otherFails.add(symbol);
}
}
// save // save
if (image != null) { if (image != null) {
sizedSymbols.put(symbol, image); sizedSymbols.put(symbol, image);
} else {
iconErrors.incrementAndGet(1); // gif fail
fileErrors.set(true);
} }
}); });
// total errors // total errors
String errorInfo = ""; String errorInfo = "";
if (iconErrors.get(0) > 0) { if (!svgFails.isEmpty()) {
errorInfo += "SVG miss - " + iconErrors.get(0); 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()) { if (!errorInfo.isEmpty()) {
errorInfo += ", "; 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()) { 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); manaImages.put(size, sizedSymbols);
return !fileErrors.get(); return errorInfo.isEmpty();
} }
private static void renameSymbols(String path) { private static void renameSymbols(String path) {

View file

@ -4,7 +4,6 @@ import mage.cards.MageCard;
import mage.cards.MagePermanent; import mage.cards.MagePermanent;
import mage.cards.action.ActionCallback; import mage.cards.action.ActionCallback;
import mage.client.util.GUISizeHelper; import mage.client.util.GUISizeHelper;
import mage.client.util.ImageCaches;
import mage.interfaces.plugin.CardPlugin; import mage.interfaces.plugin.CardPlugin;
import mage.view.CardView; import mage.view.CardView;
import mage.view.CounterView; import mage.view.CounterView;
@ -653,38 +652,34 @@ public class CardPluginImpl implements CardPlugin {
final Downloader downloader = new Downloader(); final Downloader downloader = new Downloader();
final DownloadGui downloadGui = new DownloadGui(downloader); final DownloadGui downloadGui = new DownloadGui(downloader);
LOGGER.info("Symbols download prepare..."); LOGGER.info("Download: prepare symbols to download...");
Iterable<DownloadJob> jobs; Iterable<DownloadJob> jobs;
// mana symbols (low quality)
jobs = new GathererSymbols(); jobs = new GathererSymbols();
for (DownloadJob job : jobs) { for (DownloadJob job : jobs) {
downloader.add(job); downloader.add(job);
} }
// set code symbols (low quality)
jobs = new GathererSets(); jobs = new GathererSets();
for (DownloadJob job : jobs) { for (DownloadJob job : jobs) {
downloader.add(job); downloader.add(job);
} }
// mana symbols (high quality)
jobs = new ScryfallSymbolsSource(); jobs = new ScryfallSymbolsSource();
for (DownloadJob job : jobs) { for (DownloadJob job : jobs) {
downloader.add(job); downloader.add(job);
} }
/* // additional resources
* it = new CardFrames(imagesDir); // TODO: delete frames download (not need
* now)
* for (DownloadJob job : it) {
* g.getDownloader().add(job);
* }
*/
jobs = new DirectLinksForDownload(); jobs = new DirectLinksForDownload();
for (DownloadJob job : jobs) { for (DownloadJob job : jobs) {
downloader.add(job); 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 // download GUI dialog
JDialog dialog = new JDialog((Frame) null, "Download symbols", false); JDialog dialog = new JDialog((Frame) null, "Download symbols", false);
@ -711,6 +706,7 @@ public class CardPluginImpl implements CardPlugin {
return null; return null;
} }
}; };
// downloader finisher // downloader finisher
worker.addPropertyChangeListener(new PropertyChangeListener() { worker.addPropertyChangeListener(new PropertyChangeListener() {
@Override @Override
@ -718,7 +714,7 @@ public class CardPluginImpl implements CardPlugin {
if (evt.getPropertyName().equals("state")) { if (evt.getPropertyName().equals("state")) {
if (evt.getNewValue() == SwingWorker.StateValue.DONE) { if (evt.getNewValue() == SwingWorker.StateValue.DONE) {
// all done, can close dialog and refresh symbols for UI // all done, can close dialog and refresh symbols for UI
LOGGER.info("Symbols download finished"); LOGGER.info("Download: symbols download finished");
dialog.dispose(); dialog.dispose();
ManaSymbols.loadImages(); ManaSymbols.loadImages();
GUISizeHelper.refreshGUIAndCards(false); GUISizeHelper.refreshGUIAndCards(false);

View file

@ -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.beans.properties.Property;
import org.mage.plugins.card.dl.lm.AbstractLaternaBean; import org.mage.plugins.card.dl.lm.AbstractLaternaBean;
import org.mage.plugins.card.utils.CardImageUtils;
import javax.swing.*; import javax.swing.*;
import java.io.*; import java.io.File;
import java.net.Proxy; import java.io.FileOutputStream;
import java.net.URL; import java.io.IOException;
import java.net.URLConnection; import java.io.OutputStream;
/** /**
* Downloader job to download one resource * Download: download job to load one resource, used for symbols
* *
* @author Clemens Koza, JayDi85 * @author Clemens Koza, JayDi85
*/ */
@ -22,7 +21,7 @@ public class DownloadJob extends AbstractLaternaBean {
} }
private final String name; private final String name;
private Source source; private String url;
private final Destination destination; private final Destination destination;
private final boolean forceToDownload; // download image everytime, do not keep old image private final boolean forceToDownload; // download image everytime, do not keep old image
private final Property<State> state = properties.property("state", State.NEW); private final Property<State> state = properties.property("state", State.NEW);
@ -30,13 +29,21 @@ public class DownloadJob extends AbstractLaternaBean {
private final Property<Exception> error = properties.property("error"); private final Property<Exception> error = properties.property("error");
private final BoundedRangeModel progress = new DefaultBoundedRangeModel(); 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.name = name;
this.source = source; this.url = url;
this.destination = destination; this.destination = destination;
this.forceToDownload = forceToDownload; 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 job's state. If the state is {@link State#ABORTED}, it instead
* sets the error to "ABORTED" * sets the error to "ABORTED"
@ -80,7 +87,7 @@ public class DownloadJob extends AbstractLaternaBean {
*/ */
public void setError(String message, Exception error) { public void setError(String message, Exception error) {
if (message == null) { 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.state.setValue(State.ABORTED);
this.error.setValue(error); this.error.setValue(error);
@ -145,14 +152,6 @@ public class DownloadJob extends AbstractLaternaBean {
return name; return name;
} }
public Source getSource() {
return source;
}
public void setSource(Source source) {
this.source = source;
}
public Destination getDestination() { public Destination getDestination() {
return destination; return destination;
} }
@ -161,71 +160,6 @@ public class DownloadJob extends AbstractLaternaBean {
return forceToDownload; 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) { public static Destination toFile(final String file) {
return toFile(new File(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 { public interface Destination {
OutputStream open() throws IOException; OutputStream open() throws IOException;

View file

@ -7,8 +7,6 @@ import java.net.Proxy;
*/ */
public interface DownloadServiceInfo { public interface DownloadServiceInfo {
Proxy getProxy();
boolean isNeedCancel(); boolean isNeedCancel();
void incErrorCount(); void incErrorCount();

View file

@ -1,7 +1,8 @@
package org.mage.plugins.card.dl; package org.mage.plugins.card.dl;
import mage.client.remote.XmageURLConnection;
import mage.util.ThreadUtils; import mage.util.ThreadUtils;
import mage.util.XMageThreadFactory; import mage.util.XmageThreadFactory;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import org.jetlang.channels.Channel; import org.jetlang.channels.Channel;
import org.jetlang.channels.MemoryChannel; import org.jetlang.channels.MemoryChannel;
@ -9,13 +10,15 @@ import org.jetlang.core.Callback;
import org.jetlang.fibers.Fiber; import org.jetlang.fibers.Fiber;
import org.jetlang.fibers.PoolFiberFactory; import org.jetlang.fibers.PoolFiberFactory;
import org.mage.plugins.card.dl.DownloadJob.Destination; 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.DownloadJob.State;
import org.mage.plugins.card.dl.lm.AbstractLaternaBean; import org.mage.plugins.card.dl.lm.AbstractLaternaBean;
import javax.swing.*; import javax.swing.*;
import java.io.*; import java.io.BufferedOutputStream;
import java.net.ConnectException; import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
@ -24,7 +27,7 @@ import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
/** /**
* Symbols downloader * Download: symbols download service
* *
* @author Clemens Koza, JayDi85 * @author Clemens Koza, JayDi85
*/ */
@ -37,7 +40,7 @@ public class Downloader extends AbstractLaternaBean {
private CountDownLatch worksCount = null; private CountDownLatch worksCount = null;
private final ExecutorService pool = Executors.newCachedThreadPool( 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<Fiber> fibers = new ArrayList<>(); private final List<Fiber> fibers = new ArrayList<>();
@ -105,11 +108,11 @@ public class Downloader extends AbstractLaternaBean {
worksCount.await(60, TimeUnit.SECONDS); worksCount.await(60, TimeUnit.SECONDS);
if (worksCount.getCount() != 0) { if (worksCount.getCount() != 0) {
logger.warn("Symbols download too long..."); logger.warn("Download: symbols downloading too long");
} }
} }
} catch (InterruptedException e) { } 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 * Performs the download job: Transfers data from source to
* {@link Destination} and updates the download job's state to reflect the * destination and updates the download job's state to reflect the
* progress. * progress.
*/ */
private class DownloadCallback implements Callback<DownloadJob> { private class DownloadCallback implements Callback<DownloadJob> {
@ -133,7 +136,7 @@ public class Downloader extends AbstractLaternaBean {
// take new job // take new job
job.doPrepareAndStartWork(); job.doPrepareAndStartWork();
if (job.getState() != State.WORKING) { 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(); worksCount.countDown();
return; return;
} }
@ -146,7 +149,6 @@ public class Downloader extends AbstractLaternaBean {
// real work for new job // real work for new job
// download and save data // download and save data
try { try {
Source src = job.getSource();
Destination dst = job.getDestination(); Destination dst = job.getDestination();
BoundedRangeModel progress = job.getProgress(); BoundedRangeModel progress = job.getProgress();
@ -155,65 +157,66 @@ public class Downloader extends AbstractLaternaBean {
progress.setMaximum(1); progress.setMaximum(1);
progress.setValue(1); progress.setValue(1);
} else { } else {
// downloading // need to download
// clean local file
if (dst.exists()) { if (dst.exists()) {
try { try {
dst.delete(); dst.delete();
} catch (IOException ex1) { } catch (IOException e) {
logger.warn("While deleting not valid file", ex1); logger.warn("Download: can't delete old file " + e, e);
} }
} }
progress.setMaximum(src.length());
InputStream is = new BufferedInputStream(src.open()); // download
try { // start debug here with breakpoint like job.getName().contains("C/B")
OutputStream os = new BufferedOutputStream(dst.open()); XmageURLConnection connection = new XmageURLConnection(job.getUrl());
try { 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]; byte[] buf = new byte[8 * 1024];
int total = 0; int total = 0;
for (int len; (len = is.read(buf)) != -1; ) { for (int len; (len = inputStream.read(buf)) != -1; ) {
// fast cancel
if (job.getState() == State.ABORTED) { if (job.getState() == State.ABORTED) {
throw new IOException("Job was aborted"); throw new IOException("job was aborted");
} }
progress.setValue(total += len); progress.setValue(total += len);
os.write(buf, 0, len); outputStream.write(buf, 0, len);
} }
} catch (IOException ex) { } catch (IOException e) {
try { // something bad on downloading
dst.delete(); logger.warn("Download: " + job.getName() + " - catch error on downloading network resource "
} catch (IOException ex1) { + job.getUrl() + " - " + e, e);
logger.warn("While deleting", ex1);
}
throw ex;
} finally { } finally {
try { // clean up
os.close();
if (!dst.isValid()) { if (!dst.isValid()) {
logger.warn("Download: " + job.getName() + " - downloaded invalid network resource (not exists?) " + job.getUrl());
dst.delete(); dst.delete();
logger.warn("Resource not found " + job.getName() + " from " + job.getSource().toString());
}
} catch (IOException ex) {
logger.warn("While closing", ex);
} }
} }
} finally {
try {
is.close();
} catch (IOException ex) {
logger.warn("While closing", ex);
}
}
}
job.setState(State.FINISHED);
} catch (ConnectException ex) {
String message;
if (ex.getMessage() != null) {
message = ex.getMessage();
} else { } else {
message = "Unknown error"; // something bad with resource on server (example: wrong url)
logger.warn("Download: " + job.getName() + " - can't find network resource " + job.getUrl());
} }
logger.warn("Error resource download " + job.getName() + " from " + job.getSource().toString() + ": " + message); } else {
} catch (IOException ex) { // something bad with network (example: can't connect due bad network)
job.setError(ex); logger.warn("Download: " + job.getName() + " - can't connect to network resource " + job.getUrl());
}
}
// all done
job.setState(State.FINISHED);
} 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 { } finally {
worksCount.countDown(); worksCount.countDown();
} }

View file

@ -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<String, String> getHttpRequestHeaders(String fullUrl) { default Map<String, String> getHttpRequestHeaders(String fullUrl) {
Map<String, String> headers = new LinkedHashMap<>(); return 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;
} }
default List<String> getSupportedSets() { default List<String> getSupportedSets() {

View file

@ -6,12 +6,11 @@ import org.mage.plugins.card.dl.DownloadJob;
import java.io.File; import java.io.File;
import java.util.*; 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.dl.DownloadJob.toFile;
import static org.mage.plugins.card.utils.CardImageUtils.getImagesDir; 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 * @author noxx
*/ */
@ -44,7 +43,7 @@ public class DirectLinksForDownload implements Iterable<DownloadJob> {
for (Map.Entry<String, String> url : directLinks.entrySet()) { for (Map.Entry<String, String> url : directLinks.entrySet()) {
File dst = new File(outDir, url.getKey()); File dst = new File(outDir, url.getKey());
// download images every time (need to update low quality image) // 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(); return jobs.iterator();
} }

View file

@ -6,17 +6,18 @@ import mage.client.constants.Constants;
import mage.constants.Rarity; import mage.constants.Rarity;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import org.mage.plugins.card.dl.DownloadJob; 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.io.File;
import java.util.*; import java.util.*;
/** import static org.mage.plugins.card.dl.DownloadJob.toFile;
* WARNING, unsupported images plugin, last updates from 2018 import static org.mage.plugins.card.utils.CardImageUtils.getImagesDir;
*/
/**
* Download: set code symbols download from wizards web size
* <p>
* Warning, it's outdated source with low quality images. TODO: must migrate to scryfall like mana icons
*/
public class GathererSets implements Iterable<DownloadJob> { public class GathererSets implements Iterable<DownloadJob> {
private class CheckResult { private class CheckResult {
@ -338,6 +339,6 @@ public class GathererSets implements Iterable<DownloadJob> {
set = codeReplacements.get(set); set = codeReplacements.get(set);
} }
String url = "https://gatherer.wizards.com/Handlers/Image.ashx?type=symbol&set=" + set + "&size=small&rarity=" + urlRarity; 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);
} }
} }

View file

@ -8,15 +8,15 @@ import java.io.File;
import java.util.Iterator; import java.util.Iterator;
import static java.lang.String.format; 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.dl.DownloadJob.toFile;
import static org.mage.plugins.card.utils.CardImageUtils.getImagesDir; import static org.mage.plugins.card.utils.CardImageUtils.getImagesDir;
/** /**
* The class GathererSymbols. * Download: mana symbols download from wizards web size
* <p>
* Warning, it's outdated source with low quality images. Use scryfall source as primary.
* *
* @author Clemens Koza * @author Clemens Koza, JayDi85
* @version V0.0 25.08.2010
*/ */
public class GathererSymbols implements Iterable<DownloadJob> { public class GathererSymbols implements Iterable<DownloadJob> {
@ -125,8 +125,7 @@ public class GathererSymbols implements Iterable<DownloadJob> {
} }
String url = format(urlFmt, sizes[modSizeIndex], symbol); String url = format(urlFmt, sizes[modSizeIndex], symbol);
return new DownloadJob(sym, url, toFile(dst), false);
return new DownloadJob(sym, fromURL(url), toFile(dst), false);
} }
} }
}; };

View file

@ -4,18 +4,16 @@ import com.google.gson.JsonArray;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import com.google.gson.JsonParser; import com.google.gson.JsonParser;
import mage.MageException; import mage.MageException;
import mage.client.remote.XmageURLConnection;
import mage.client.util.CardLanguage; import mage.client.util.CardLanguage;
import mage.util.JsonUtil; import mage.util.JsonUtil;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import org.mage.plugins.card.dl.DownloadServiceInfo; import org.mage.plugins.card.dl.DownloadServiceInfo;
import org.mage.plugins.card.images.CardDownloadData; import org.mage.plugins.card.images.CardDownloadData;
import java.io.FileNotFoundException;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.net.Proxy; import java.net.Proxy;
import java.net.URL;
import java.net.URLConnection;
import java.util.*; import java.util.*;
/** /**
@ -138,7 +136,7 @@ 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 defaultCode = CardLanguage.ENGLISH.getCode();
final String localizedCode = languageAliases.getOrDefault(this.getCurrentLanguage(), defaultCode); final String localizedCode = languageAliases.getOrDefault(this.getCurrentLanguage(), defaultCode);
@ -183,18 +181,16 @@ public class ScryfallImageSource implements CardImageSource {
InputStream jsonStream = null; InputStream jsonStream = null;
String jsonUrl = null; String jsonUrl = null;
for (String currentUrl : needUrls) { for (String currentUrl : needUrls) {
// connect to Scryfall API // find first workable api endpoint
waitBeforeRequest(); waitBeforeRequest();
URL cardUrl = new URL(currentUrl);
URLConnection request = (proxy == null ? cardUrl.openConnection() : cardUrl.openConnection(proxy)); jsonStream = XmageURLConnection.downloadBinary(currentUrl);
request.connect(); if (jsonStream != null) {
try {
jsonStream = (InputStream) request.getContent();
jsonUrl = currentUrl;
// found good url, can stop // found good url, can stop
jsonUrl = currentUrl;
break; break;
} catch (FileNotFoundException e) { } else {
// localized image doesn't exists, try next url // localized image doesn't exist, try next url
} }
} }
@ -224,8 +220,6 @@ public class ScryfallImageSource implements CardImageSource {
@Override @Override
public boolean prepareDownloadList(DownloadServiceInfo downloadServiceInfo, List<CardDownloadData> downloadList) { public boolean prepareDownloadList(DownloadServiceInfo downloadServiceInfo, List<CardDownloadData> downloadList) {
// prepare download list example ( // prepare download list example (
Proxy proxy = downloadServiceInfo.getProxy();
preparedUrls.clear(); preparedUrls.clear();
// prepare stats // prepare stats
@ -248,7 +242,7 @@ public class ScryfallImageSource implements CardImageSource {
if (card.isSecondSide()) { if (card.isSecondSide()) {
currentPrepareCount++; currentPrepareCount++;
try { try {
String url = getFaceImageUrl(proxy, card, card.isToken()); String url = getFaceImageUrl(card, card.isToken());
preparedUrls.put(card, url); preparedUrls.put(card, url);
} catch (Exception e) { } catch (Exception e) {
logger.warn("Failed to prepare image URL (back face) for " + card.getName() + " (" + card.getSet() + ") #" logger.warn("Failed to prepare image URL (back face) for " + card.getName() + " (" + card.getSet() + ") #"

View file

@ -1,7 +1,8 @@
package org.mage.plugins.card.dl.sources; 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.dl.DownloadJob;
import org.mage.plugins.card.utils.CardImageUtils;
import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener; import java.beans.PropertyChangeListener;
@ -65,7 +66,8 @@ public class ScryfallSymbolsSource implements Iterable<DownloadJob> {
try { try {
sourceData = new String(Files.readAllBytes(Paths.get(sourcePath))); sourceData = new String(Files.readAllBytes(Paths.get(sourcePath)));
} catch (IOException e) { } 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 // gen symbols list
@ -129,10 +131,11 @@ public class ScryfallSymbolsSource implements Iterable<DownloadJob> {
// listener for data parse after download complete // listener for data parse after download complete
private class ScryfallDownloadOnFinishedListener implements PropertyChangeListener { private class ScryfallDownloadOnFinishedListener implements PropertyChangeListener {
private String downloadedFile; private String downloadedFile;
public ScryfallDownloadOnFinishedListener(String ADestFile) { public ScryfallDownloadOnFinishedListener(String downloadedFile) {
this.downloadedFile = ADestFile; this.downloadedFile = downloadedFile;
} }
@Override @Override
@ -152,10 +155,15 @@ public class ScryfallSymbolsSource implements Iterable<DownloadJob> {
} }
@Override @Override
public void onPreparing() throws Exception { public void onPreparing() {
// parse help page and find real URL with svg icons on it
this.cssUrl = ""; 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); org.jsoup.select.Elements cssList = doc.select(CSS_SOURCE_SELECTOR);
if (cssList.size() == 1) { if (cssList.size() == 1) {
this.cssUrl = cssList.first().attr("href"); this.cssUrl = cssList.first().attr("href");
@ -164,13 +172,13 @@ public class ScryfallSymbolsSource implements Iterable<DownloadJob> {
if (this.cssUrl.isEmpty()) { if (this.cssUrl.isEmpty()) {
throw new IllegalStateException("Can't find stylesheet url from scryfall colors page."); throw new IllegalStateException("Can't find stylesheet url from scryfall colors page.");
} else { } else {
this.setSource(fromURL(this.cssUrl)); this.setUrl(this.cssUrl);
} }
} }
public ScryfallSymbolsDownloadJob() { public ScryfallSymbolsDownloadJob() {
// download init // download init (real url will be added on prepare)
super("Scryfall symbols source", fromURL(""), toFile(DOWNLOAD_TEMP_FILE), true); // url setup on preparing stage super("Scryfall symbols source", "", toFile(DOWNLOAD_TEMP_FILE), true); // url setup on preparing stage
String destFile = DOWNLOAD_TEMP_FILE; String destFile = DOWNLOAD_TEMP_FILE;
this.addPropertyChangeListener(STATE_PROP_NAME, new ScryfallDownloadOnFinishedListener(destFile)); this.addPropertyChangeListener(STATE_PROP_NAME, new ScryfallDownloadOnFinishedListener(destFile));

View file

@ -4,8 +4,10 @@ import mage.cards.Sets;
import mage.cards.repository.CardCriteria; import mage.cards.repository.CardCriteria;
import mage.cards.repository.CardInfo; import mage.cards.repository.CardInfo;
import mage.cards.repository.CardRepository; import mage.cards.repository.CardRepository;
import mage.client.remote.XmageURLConnection;
import mage.client.util.CardLanguage; import mage.client.util.CardLanguage;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document; import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element; import org.jsoup.nodes.Element;
import org.jsoup.select.Elements; import org.jsoup.select.Elements;
@ -522,7 +524,8 @@ public enum WizardCardsImageSource implements CardImageSource {
while (page < 999) { 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"; 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); 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/]"); Elements cardsImages = doc.select("img[src^=../../Handlers/]");
if (cardsImages.isEmpty()) { if (cardsImages.isEmpty()) {
break; break;
@ -565,14 +568,15 @@ public enum WizardCardsImageSource implements CardImageSource {
return setLinks; return setLinks;
} }
private void getLandVariations(LinkedHashMap<String, String> setLinks, String cardSet, int multiverseId, String cardName) throws IOException, NumberFormatException { private void getLandVariations(LinkedHashMap<String, String> setLinks, String cardSet, int multiverseId, String cardName) {
CardCriteria criteria = new CardCriteria(); CardCriteria criteria = new CardCriteria();
criteria.name(cardName); criteria.name(cardName);
criteria.setCodes(cardSet); criteria.setCodes(cardSet);
List<CardInfo> cards = CardRepository.instance.findCards(criteria); List<CardInfo> cards = CardRepository.instance.findCards(criteria);
String urlLandDocument = "https://gatherer.wizards.com/Pages/Card/Details.aspx?multiverseid=" + multiverseId; 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"); Elements variations = landDoc.select("a.variationlink");
if (!variations.isEmpty()) { if (!variations.isEmpty()) {
if (variations.size() > cards.size()) { if (variations.size() > cards.size()) {
@ -617,9 +621,10 @@ public enum WizardCardsImageSource implements CardImageSource {
} }
} }
private Map<String, Integer> getlocalizedMultiverseIds(Integer englishMultiverseId) throws IOException { private Map<String, Integer> getlocalizedMultiverseIds(Integer englishMultiverseId) {
String cardLanguagesUrl = "https://gatherer.wizards.com/Pages/Card/Languages.aspx?multiverseid=" + 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"); Elements languageTableRows = cardLanguagesDoc.select("tr.cardItem");
Map<String, Integer> localizedIds = new HashMap<>(); Map<String, Integer> localizedIds = new HashMap<>();
if (!languageTableRows.isEmpty()) { if (!languageTableRows.isEmpty()) {

View file

@ -9,12 +9,12 @@ import mage.cards.repository.TokenRepository;
import mage.client.MageFrame; import mage.client.MageFrame;
import mage.client.dialog.DownloadImagesDialog; import mage.client.dialog.DownloadImagesDialog;
import mage.client.dialog.PreferencesDialog; import mage.client.dialog.PreferencesDialog;
import mage.client.remote.XmageURLConnection;
import mage.client.util.CardLanguage; import mage.client.util.CardLanguage;
import mage.client.util.GUISizeHelper; import mage.client.util.GUISizeHelper;
import mage.client.util.sets.ConstructedFormats; import mage.client.util.sets.ConstructedFormats;
import mage.remote.Connection;
import mage.util.ThreadUtils; import mage.util.ThreadUtils;
import mage.util.XMageThreadFactory; import mage.util.XmageThreadFactory;
import net.java.truevfs.access.TFile; import net.java.truevfs.access.TFile;
import net.java.truevfs.access.TFileInputStream; import net.java.truevfs.access.TFileInputStream;
import net.java.truevfs.access.TFileOutputStream; import net.java.truevfs.access.TFileOutputStream;
@ -31,7 +31,8 @@ import java.awt.*;
import java.awt.event.ItemEvent; import java.awt.event.ItemEvent;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.*; import java.io.*;
import java.net.*; import java.net.SocketException;
import java.net.UnknownHostException;
import java.nio.file.AccessDeniedException; import java.nio.file.AccessDeniedException;
import java.util.List; import java.util.List;
import java.util.*; import java.util.*;
@ -83,7 +84,6 @@ public class DownloadPicturesService extends DefaultBoundedRangeModel implements
private static CardImageSource selectedSource; private static CardImageSource selectedSource;
private final Object sync = new Object(); private final Object sync = new Object();
private Proxy proxy = Proxy.NO_PROXY;
enum DownloadSources { enum DownloadSources {
WIZARDS("1. wizards.com - low quality, cards only", WizardCardsImageSource.instance), WIZARDS("1. wizards.com - low quality, cards only", WizardCardsImageSource.instance),
@ -635,34 +635,9 @@ public class DownloadPicturesService extends DefaultBoundedRangeModel implements
base.mkdir(); 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())); int downloadThreadsAmount = Math.max(1, Integer.parseInt((String) uiDialog.getDownloadThreadsCombo().getSelectedItem()));
if (proxy != null) {
logger.info("Started download of " + cardsDownloadQueue.size() + " images" logger.info("Started download of " + cardsDownloadQueue.size() + " images"
+ " from source: " + selectedSource.getSourceName() + " from source: " + selectedSource.getSourceName()
+ ", language: " + selectedSource.getCurrentLanguage().getCode() + ", language: " + selectedSource.getCurrentLanguage().getCode()
@ -672,7 +647,7 @@ public class DownloadPicturesService extends DefaultBoundedRangeModel implements
update(0, cardsDownloadQueue.size()); update(0, cardsDownloadQueue.size());
ExecutorService executor = Executors.newFixedThreadPool( ExecutorService executor = Executors.newFixedThreadPool(
downloadThreadsAmount, downloadThreadsAmount,
new XMageThreadFactory(ThreadUtils.THREAD_PREFIX_CLIENT_IMAGES_DOWNLOADER, false) new XmageThreadFactory(ThreadUtils.THREAD_PREFIX_CLIENT_IMAGES_DOWNLOADER, false)
); );
for (int i = 0; i < cardsDownloadQueue.size() && !this.isNeedCancel(); i++) { for (int i = 0; i < cardsDownloadQueue.size() && !this.isNeedCancel(); i++) {
try { try {
@ -724,7 +699,7 @@ public class DownloadPicturesService extends DefaultBoundedRangeModel implements
} }
} }
} }
}
try { try {
TVFS.umount(); TVFS.umount();
@ -743,11 +718,6 @@ public class DownloadPicturesService extends DefaultBoundedRangeModel implements
GUISizeHelper.refreshGUIAndCards(false); 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 class DownloadTask implements Runnable {
private final CardDownloadData card; private final CardDownloadData card;
@ -834,17 +804,17 @@ public class DownloadPicturesService extends DefaultBoundedRangeModel implements
// can download images from many alternative urls // can download images from many alternative urls
List<String> downloadUrls; List<String> downloadUrls;
XmageURLConnection connection = null;
if (this.urls != null) { if (this.urls != null) {
downloadUrls = this.urls.getDownloadList(); downloadUrls = this.urls.getDownloadList();
} else { } else {
downloadUrls = new ArrayList<>(); downloadUrls = new ArrayList<>();
} }
// try to find first workable link
boolean isDownloadOK = false; boolean isDownloadOK = false;
URLConnection httpConn = null;
List<String> errorsList = new ArrayList<>(); List<String> errorsList = new ArrayList<>();
for (String currentUrl : downloadUrls) { for (String currentUrl : downloadUrls) {
URL url = new URL(currentUrl);
// fast stop on cancel // fast stop on cancel
if (DownloadPicturesService.getInstance().isNeedCancel()) { if (DownloadPicturesService.getInstance().isNeedCancel()) {
@ -852,29 +822,27 @@ public class DownloadPicturesService extends DefaultBoundedRangeModel implements
} }
// timeout before each request // timeout before each request
selectedSource.doPause(url.toString()); selectedSource.doPause(currentUrl);
httpConn = url.openConnection(proxy); connection = new XmageURLConnection(currentUrl);
if (httpConn != null) { connection.startConnection();
if (connection.isConnected()) {
// custom headers like user agent // custom headers (ues
Map<String, String> headers = selectedSource.getHttpRequestHeaders(url.toString()); connection.setRequestHeaders(selectedSource.getHttpRequestHeaders(currentUrl));
for (String key : headers.keySet()) {
httpConn.setRequestProperty(key, headers.get(key));
}
try { try {
httpConn.connect(); connection.connect();
} catch (SocketException e) { } catch (SocketException e) {
incErrorCount(); 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; break;
} catch (UnknownHostException e) { } catch (UnknownHostException e) {
incErrorCount(); 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; break;
} }
int responseCode = ((HttpURLConnection) httpConn).getResponseCode(); int responseCode = connection.getResponseCode();
// check result // check result
if (responseCode != 200) { if (responseCode != 200) {
@ -883,13 +851,13 @@ public class DownloadPicturesService extends DefaultBoundedRangeModel implements
card.getSet(), card.getSet(),
card.getName(), card.getName(),
responseCode, responseCode,
url currentUrl
); );
errorsList.add(info); errorsList.add(info);
if (logger.isDebugEnabled()) { if (logger.isDebugEnabled()) {
// Shows the returned html from the request to the web server // 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 // go to next try
@ -902,12 +870,12 @@ public class DownloadPicturesService extends DefaultBoundedRangeModel implements
} }
} }
// can save result // if workable link found then save result
if (isDownloadOK && httpConn != null) { if (isDownloadOK && connection.isConnected()) {
// save data to temp // save data to temp
try (InputStream in = new BufferedInputStream(httpConn.getInputStream()); try (InputStream in = new BufferedInputStream(connection.getGoodResponseAsStream());
OutputStream tfileout = new TFileOutputStream(fileTempImage); OutputStream tempFileStream = new TFileOutputStream(fileTempImage);
OutputStream out = new BufferedOutputStream(tfileout)) { OutputStream out = new BufferedOutputStream(tempFileStream)) {
byte[] buf = new byte[1024]; byte[] buf = new byte[1024];
int len; int len;
while ((len = in.read(buf)) != -1) { while ((len = in.read(buf)) != -1) {
@ -931,6 +899,8 @@ public class DownloadPicturesService extends DefaultBoundedRangeModel implements
} }
return; return;
} }
// all fine, can save data part
out.write(buf, 0, len); out.write(buf, 0, len);
} }
} }
@ -947,7 +917,6 @@ public class DownloadPicturesService extends DefaultBoundedRangeModel implements
} catch (Exception e) { } catch (Exception e) {
logger.error("Can't delete temp file: " + e.getMessage(), e); logger.error("Can't delete temp file: " + e.getMessage(), e);
} }
} }
} else { } else {
// download errors // download errors
@ -1013,11 +982,6 @@ public class DownloadPicturesService extends DefaultBoundedRangeModel implements
uiDialog.getStartButton().setEnabled(true); uiDialog.getStartButton().setEnabled(true);
} }
@Override
public Proxy getProxy() {
return proxy;
}
@Override @Override
public Object getSync() { public Object getSync() {
return sync; return sync;

View file

@ -6,6 +6,7 @@ import mage.cards.repository.TokenRepository;
import mage.client.MageFrame; import mage.client.MageFrame;
import mage.client.constants.Constants; import mage.client.constants.Constants;
import mage.client.dialog.PreferencesDialog; import mage.client.dialog.PreferencesDialog;
import mage.client.remote.XmageURLConnection;
import mage.remote.Connection; import mage.remote.Connection;
import mage.remote.Connection.ProxyType; import mage.remote.Connection.ProxyType;
import mage.view.CardView; import mage.view.CardView;
@ -201,33 +202,6 @@ public final class CardImageUtils {
return null; 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() { public static void checkAndFixImageFiles() {
// search broken, temp or outdated files and delete it // search broken, temp or outdated files and delete it
// real images check is slow, so it used on images download only (not here) // real images check is slow, so it used on images download only (not here)

View file

@ -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("<head>"));
}
@Test
public void test_DownloadText_ByHttps() {
String s = XmageURLConnection.downloadText("https://google.com");
Assert.assertTrue("must have text data", s.contains("<head>"));
}
@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);
}
}

View file

@ -91,4 +91,8 @@ public class MageVersion implements Serializable, Comparable<MageVersion> {
public boolean isDeveloperBuild() { public boolean isDeveloperBuild() {
return this.buildTime.contains(JarVersion.JAR_BUILD_TIME_FROM_CLASSES); return this.buildTime.contains(JarVersion.JAR_BUILD_TIME_FROM_CLASSES);
} }
public String getBuildTime() {
return this.buildTime;
}
} }

View file

@ -6,7 +6,7 @@ import mage.remote.Connection;
import mage.remote.Session; import mage.remote.Session;
import mage.remote.SessionImpl; import mage.remote.SessionImpl;
import mage.util.ThreadUtils; import mage.util.ThreadUtils;
import mage.util.XMageThreadFactory; import mage.util.XmageThreadFactory;
import mage.utils.MageVersion; import mage.utils.MageVersion;
import org.apache.log4j.Logger; 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 MageVersion version = new MageVersion(ConsoleFrame.class);
private static final ScheduledExecutorService PING_SENDER_EXECUTOR = Executors.newSingleThreadScheduledExecutor( 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)
); );
/** /**

View file

@ -32,7 +32,7 @@ import mage.target.TargetCard;
import mage.util.CardUtil; import mage.util.CardUtil;
import mage.util.RandomUtil; import mage.util.RandomUtil;
import mage.util.ThreadUtils; import mage.util.ThreadUtils;
import mage.util.XMageThreadFactory; import mage.util.XmageThreadFactory;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import java.util.*; import java.util.*;
@ -61,7 +61,7 @@ public class ComputerPlayer6 extends ComputerPlayer {
0L, 0L,
TimeUnit.MILLISECONDS, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(), new LinkedBlockingQueue<>(),
new XMageThreadFactory(ThreadUtils.THREAD_PREFIX_AI_SIMULATION_MAD) new XmageThreadFactory(ThreadUtils.THREAD_PREFIX_AI_SIMULATION_MAD)
); );
protected int maxDepth; protected int maxDepth;
protected int maxNodes; protected int maxNodes;

View file

@ -13,7 +13,7 @@ import mage.game.combat.CombatGroup;
import mage.player.ai.MCTSPlayer.NextAction; import mage.player.ai.MCTSPlayer.NextAction;
import mage.players.Player; import mage.players.Player;
import mage.util.ThreadUtils; import mage.util.ThreadUtils;
import mage.util.XMageThreadFactory; import mage.util.XmageThreadFactory;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import java.util.ArrayList; import java.util.ArrayList;
@ -173,7 +173,7 @@ public class ComputerPlayerMCTS extends ComputerPlayer {
0L, 0L,
TimeUnit.MILLISECONDS, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(), 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?
); );
} }

View file

@ -6,7 +6,7 @@ import mage.server.record.UserStats;
import mage.server.record.UserStatsRepository; import mage.server.record.UserStatsRepository;
import mage.server.util.ServerMessagesUtil; import mage.server.util.ServerMessagesUtil;
import mage.util.ThreadUtils; import mage.util.ThreadUtils;
import mage.util.XMageThreadFactory; import mage.util.XmageThreadFactory;
import mage.view.UserView; import mage.view.UserView;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
@ -34,10 +34,10 @@ public class UserManagerImpl implements UserManager {
private static final Logger logger = Logger.getLogger(UserManagerImpl.class); private static final Logger logger = Logger.getLogger(UserManagerImpl.class);
protected final ScheduledExecutorService CONNECTION_EXPIRED_EXECUTOR = Executors.newSingleThreadScheduledExecutor( 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( 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<UserView> userInfoList = new ArrayList<>(); // all users list for main room/chat private List<UserView> userInfoList = new ArrayList<>(); // all users list for main room/chat

View file

@ -26,7 +26,7 @@ import mage.server.User;
import mage.server.managers.ManagerFactory; import mage.server.managers.ManagerFactory;
import mage.util.MultiAmountMessage; import mage.util.MultiAmountMessage;
import mage.util.ThreadUtils; import mage.util.ThreadUtils;
import mage.util.XMageThreadFactory; import mage.util.XmageThreadFactory;
import mage.utils.StreamUtils; import mage.utils.StreamUtils;
import mage.utils.timer.PriorityTimer; import mage.utils.timer.PriorityTimer;
import mage.view.*; import mage.view.*;
@ -244,7 +244,7 @@ public class GameController implements GameCallback {
// wait all players // wait all players
if (JOIN_WAITING_EXECUTOR == null) { if (JOIN_WAITING_EXECUTOR == null) {
JOIN_WAITING_EXECUTOR = Executors.newSingleThreadScheduledExecutor( 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(() -> { JOIN_WAITING_EXECUTOR.scheduleAtFixedRate(() -> {

View file

@ -12,7 +12,7 @@ import mage.server.RoomImpl;
import mage.server.User; import mage.server.User;
import mage.server.managers.ManagerFactory; import mage.server.managers.ManagerFactory;
import mage.util.ThreadUtils; import mage.util.ThreadUtils;
import mage.util.XMageThreadFactory; import mage.util.XmageThreadFactory;
import mage.view.MatchView; import mage.view.MatchView;
import mage.view.RoomUsersView; import mage.view.RoomUsersView;
import mage.view.TableView; import mage.view.TableView;
@ -40,7 +40,7 @@ public class GamesRoomImpl extends RoomImpl implements GamesRoom, Serializable {
private static List<MatchView> lobbyMatches = new ArrayList<>(); private static List<MatchView> lobbyMatches = new ArrayList<>();
private static List<RoomUsersView> lobbyUsers = new ArrayList<>(); private static List<RoomUsersView> lobbyUsers = new ArrayList<>();
private static final ScheduledExecutorService UPDATE_LOBBY_EXECUTOR = Executors.newSingleThreadScheduledExecutor( 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; private final ManagerFactory managerFactory;

View file

@ -1,7 +1,7 @@
package mage.server.util; package mage.server.util;
import mage.util.ThreadUtils; import mage.util.ThreadUtils;
import mage.util.XMageThreadFactory; import mage.util.XmageThreadFactory;
import mage.utils.StreamUtils; import mage.utils.StreamUtils;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
@ -47,7 +47,7 @@ public enum ServerMessagesUtil {
ServerMessagesUtil() { ServerMessagesUtil() {
ScheduledExecutorService NEWS_MESSAGES_EXECUTOR = Executors.newSingleThreadScheduledExecutor( 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); NEWS_MESSAGES_EXECUTOR.scheduleAtFixedRate(this::reloadMessages, 5, SERVER_MSG_REFRESH_RATE_SECS, TimeUnit.SECONDS);
} }

View file

@ -3,7 +3,7 @@ package mage.server.util;
import mage.server.managers.ConfigSettings; import mage.server.managers.ConfigSettings;
import mage.server.managers.ThreadExecutor; import mage.server.managers.ThreadExecutor;
import mage.util.ThreadUtils; import mage.util.ThreadUtils;
import mage.util.XMageThreadFactory; import mage.util.XmageThreadFactory;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import java.util.concurrent.*; import java.util.concurrent.*;
@ -42,31 +42,31 @@ public class ThreadExecutorImpl implements ThreadExecutor {
callExecutor = new CachedThreadPoolWithException(); callExecutor = new CachedThreadPoolWithException();
((ThreadPoolExecutor) callExecutor).setKeepAliveTime(60, TimeUnit.SECONDS); ((ThreadPoolExecutor) callExecutor).setKeepAliveTime(60, TimeUnit.SECONDS);
((ThreadPoolExecutor) callExecutor).allowCoreThreadTimeOut(true); ((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 = Executors.newFixedThreadPool(config.getMaxGameThreads());
gameExecutor = new FixedThreadPoolWithException(config.getMaxGameThreads()); gameExecutor = new FixedThreadPoolWithException(config.getMaxGameThreads());
((ThreadPoolExecutor) gameExecutor).setKeepAliveTime(60, TimeUnit.SECONDS); ((ThreadPoolExecutor) gameExecutor).setKeepAliveTime(60, TimeUnit.SECONDS);
((ThreadPoolExecutor) gameExecutor).allowCoreThreadTimeOut(true); ((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); //tourney = Executors.newFixedThreadPool(config.getMaxGameThreads() / GAMES_PER_TOURNEY_RATIO);
tourneyExecutor = new FixedThreadPoolWithException(config.getMaxGameThreads() / GAMES_PER_TOURNEY_RATIO); tourneyExecutor = new FixedThreadPoolWithException(config.getMaxGameThreads() / GAMES_PER_TOURNEY_RATIO);
((ThreadPoolExecutor) tourneyExecutor).setKeepAliveTime(60, TimeUnit.SECONDS); ((ThreadPoolExecutor) tourneyExecutor).setKeepAliveTime(60, TimeUnit.SECONDS);
((ThreadPoolExecutor) tourneyExecutor).allowCoreThreadTimeOut(true); ((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); timeoutExecutor = Executors.newScheduledThreadPool(4);
((ThreadPoolExecutor) timeoutExecutor).setKeepAliveTime(60, TimeUnit.SECONDS); ((ThreadPoolExecutor) timeoutExecutor).setKeepAliveTime(60, TimeUnit.SECONDS);
((ThreadPoolExecutor) timeoutExecutor).allowCoreThreadTimeOut(true); ((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); timeoutIdleExecutor = Executors.newScheduledThreadPool(4);
((ThreadPoolExecutor) timeoutIdleExecutor).setKeepAliveTime(60, TimeUnit.SECONDS); ((ThreadPoolExecutor) timeoutIdleExecutor).setKeepAliveTime(60, TimeUnit.SECONDS);
((ThreadPoolExecutor) timeoutIdleExecutor).allowCoreThreadTimeOut(true); ((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 { static class CachedThreadPoolWithException extends ThreadPoolExecutor {

View file

@ -14,7 +14,7 @@ import mage.remote.Session;
import mage.remote.SessionImpl; import mage.remote.SessionImpl;
import mage.util.RandomUtil; import mage.util.RandomUtil;
import mage.util.ThreadUtils; import mage.util.ThreadUtils;
import mage.util.XMageThreadFactory; import mage.util.XmageThreadFactory;
import mage.view.*; import mage.view.*;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import org.junit.Assert; import org.junit.Assert;
@ -328,9 +328,9 @@ public class LoadTest {
ExecutorService executerService; ExecutorService executerService;
if (isRunParallel) { 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 { } 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) // save random seeds for repeated results (in decks generating)

View file

@ -4,11 +4,16 @@ import mage.constants.Rarity;
import java.util.List; import java.util.List;
/**
* MTGJSON v5: card class
* <p>
* Contains card related data nd only used fields, if you need more for tests then just add it here
* <p>
* API docs <a href="https://mtgjson.com/data-models/card/card-set/">here</a>
*
* @author JayDi85
*/
public final class MtgJsonCard { 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 name;
public String asciiName; // mtgjson uses it for some cards like El-Hajjaj 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/ 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 * @return single side name like Ice from Fire // Ice
*/ */
public String getNameAsFace() { public String getNameAsFace() {
@ -53,7 +57,6 @@ public final class MtgJsonCard {
} }
/** /**
*
* @return full card name like Fire // Ice * @return full card name like Fire // Ice
*/ */
public String getNameAsFull() { public String getNameAsFull() {

View file

@ -1,9 +1,15 @@
package mage.verify.mtgjson; package mage.verify.mtgjson;
/**
* MTGJSON v5: metadata class
* <p>
* Contains version info
* <p>
* API docs <a href="https://mtgjson.com/file-models/meta/">here</a>
*
* @author JayDi85
*/
public final class MtgJsonMetadata { public final class MtgJsonMetadata {
// MTGJSON metadata
// https://mtgjson.com/file-models/meta/
public String date; public String date;
public String version; public String version;
} }

View file

@ -1,10 +1,13 @@
package mage.verify.mtgjson; package mage.verify.mtgjson;
import com.google.gson.Gson; import com.google.gson.Gson;
import mage.client.remote.XmageURLConnection;
import org.apache.log4j.Logger;
import java.io.*; import java.io.File;
import java.net.URL; import java.io.IOException;
import java.net.URLConnection; import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.StandardCopyOption; import java.nio.file.StandardCopyOption;
import java.text.Normalizer; import java.text.Normalizer;
@ -12,8 +15,15 @@ import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.zip.ZipInputStream; import java.util.zip.ZipInputStream;
/**
* MTGJSON v5: basic service to work with mtgjson data
*
* @author JayDi85
*/
public final class MtgJsonService { public final class MtgJsonService {
private static final Logger logger = Logger.getLogger(MtgJsonService.class);
public static Map<String, String> mtgJsonToXMageCodes = new HashMap<>(); public static Map<String, String> mtgJsonToXMageCodes = new HashMap<>();
public static Map<String, String> xMageToMtgJsonCodes = new HashMap<>(); public static Map<String, String> xMageToMtgJsonCodes = new HashMap<>();
@ -36,23 +46,36 @@ public final class MtgJsonService {
} }
private static <T> T readFromZip(String filename, Class<T> clazz) throws IOException { private static <T> T readFromZip(String filename, Class<T> clazz) throws IOException {
// build-in file
InputStream stream = MtgJsonService.class.getResourceAsStream(filename); InputStream stream = MtgJsonService.class.getResourceAsStream(filename);
if (stream == null) { if (stream != null) {
File file = new File(filename); logger.info("mtgjson: use build-in file " + filename);
if (!file.exists()) { return readFromZip(stream, clazz);
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);
} }
// 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> T readFromZip(InputStream stream, Class<T> clazz) throws IOException {
try (ZipInputStream zipInputStream = new ZipInputStream(stream)) { try (ZipInputStream zipInputStream = new ZipInputStream(stream)) {
zipInputStream.getNextEntry(); zipInputStream.getNextEntry();
return new Gson().fromJson(new InputStreamReader(zipInputStream), clazz); return new Gson().fromJson(new InputStreamReader(zipInputStream), clazz);
@ -243,5 +266,4 @@ public final class MtgJsonService {
} }
} }
} }
} }

View file

@ -2,10 +2,17 @@ package mage.verify.mtgjson;
import java.util.List; import java.util.List;
/**
* MTGJSON v5: set class
* <p>
* Contains set info and related cards list
* Only used fields, if you need more for tests then just add it here
* <p>
* API docs <a href="https://mtgjson.com/data-models/set/">here</a>
*
* @author JayDi85
*/
public final class MtgJsonSet { 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<MtgJsonCard> cards; public List<MtgJsonCard> cards;
public String code; public String code;

View file

@ -8,7 +8,7 @@ import mage.game.events.TableEvent.EventType;
import mage.players.Player; import mage.players.Player;
import mage.players.PlayerList; import mage.players.PlayerList;
import mage.util.ThreadUtils; import mage.util.ThreadUtils;
import mage.util.XMageThreadFactory; import mage.util.XmageThreadFactory;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import java.util.*; import java.util.*;
@ -248,7 +248,7 @@ public abstract class DraftImpl implements Draft {
if (this.boosterLoadingExecutor == null) { if (this.boosterLoadingExecutor == null) {
this.boosterLoadingExecutor = Executors.newSingleThreadScheduledExecutor( this.boosterLoadingExecutor = Executors.newSingleThreadScheduledExecutor(
new XMageThreadFactory(ThreadUtils.THREAD_PREFIX_TOURNEY_BOOSTERS_SEND) new XmageThreadFactory(ThreadUtils.THREAD_PREFIX_TOURNEY_BOOSTERS_SEND)
); );
} }

View file

@ -8,13 +8,13 @@ import java.util.concurrent.atomic.AtomicInteger;
* *
* @author JayDi85 * @author JayDi85
*/ */
public class XMageThreadFactory implements ThreadFactory { public class XmageThreadFactory implements ThreadFactory {
private final String prefix; private final String prefix;
private final AtomicInteger counter = new AtomicInteger(); private final AtomicInteger counter = new AtomicInteger();
private final boolean isDaemon; private final boolean isDaemon;
public XMageThreadFactory(String prefix) { public XmageThreadFactory(String prefix) {
this(prefix, true); 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 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) * @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.prefix = prefix;
this.isDaemon = isDaemon; this.isDaemon = isDaemon;
} }