diff --git a/fw/search.subtitlesource.png b/fw/search.subtitlesource.png new file mode 100644 index 00000000..4c504a26 Binary files /dev/null and b/fw/search.subtitlesource.png differ diff --git a/source/net/sourceforge/filebot/resources/search.subtitlesource.png b/source/net/sourceforge/filebot/resources/search.subtitlesource.png new file mode 100644 index 00000000..84bdc41f Binary files /dev/null and b/source/net/sourceforge/filebot/resources/search.subtitlesource.png differ diff --git a/source/net/sourceforge/filebot/ui/panel/subtitle/LanguageResolver.java b/source/net/sourceforge/filebot/ui/panel/subtitle/LanguageResolver.java index 0f45ce0c..7e371dab 100644 --- a/source/net/sourceforge/filebot/ui/panel/subtitle/LanguageResolver.java +++ b/source/net/sourceforge/filebot/ui/panel/subtitle/LanguageResolver.java @@ -2,9 +2,9 @@ package net.sourceforge.filebot.ui.panel.subtitle; -import java.util.HashMap; import java.util.Locale; -import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; public class LanguageResolver { @@ -16,7 +16,7 @@ public class LanguageResolver { return defaultInstance; } - private final Map cache = new HashMap(); + private final ConcurrentMap cache = new ConcurrentHashMap(); /** @@ -25,13 +25,15 @@ public class LanguageResolver { * @param languageName english name of the language * @return the locale for this language or null if no locale for this language exists */ - public synchronized Locale getLocale(String languageName) { + public Locale getLocale(String languageName) { + // case insensitive + String key = languageName.toLowerCase(); - Locale locale = cache.get(languageName.toLowerCase()); + Locale locale = cache.get(key); if (locale == null) { locale = findLocale(languageName); - cache.put(languageName.toLowerCase(), locale); + cache.put(key, locale); } return locale; @@ -62,7 +64,7 @@ public class LanguageResolver { * @return {@link Locale} for the given language, or null if no matching {@link Locale} is * available */ - private Locale findLocale(String languageName) { + protected Locale findLocale(String languageName) { for (Locale locale : Locale.getAvailableLocales()) { if (locale.getDisplayLanguage(Locale.ENGLISH).equalsIgnoreCase(languageName)) return locale; diff --git a/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitlePanel.java b/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitlePanel.java index 614307d6..e4b8bf35 100644 --- a/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitlePanel.java +++ b/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitlePanel.java @@ -20,6 +20,7 @@ import net.sourceforge.filebot.web.SearchResult; import net.sourceforge.filebot.web.SubsceneSubtitleClient; import net.sourceforge.filebot.web.SubtitleClient; import net.sourceforge.filebot.web.SubtitleDescriptor; +import net.sourceforge.filebot.web.SubtitleSourceClient; import net.sourceforge.tuned.ListChangeSynchronizer; import net.sourceforge.tuned.ui.LabelProvider; import net.sourceforge.tuned.ui.SimpleLabelProvider; @@ -51,6 +52,7 @@ public class SubtitlePanel extends AbstractSearchPanel episodes) { - //TODO subtitle tab ui - System.out.println(episodes); + public void process(Collection subtitles) { + getComponent().getPackagePanel().getModel().addAll(subtitles); + } + + + @Override + public SubtitleDownloadPanel getComponent() { + return (SubtitleDownloadPanel) super.getComponent(); } diff --git a/source/net/sourceforge/filebot/web/AnidbClient.java b/source/net/sourceforge/filebot/web/AnidbClient.java index 338021f0..94b8091e 100644 --- a/source/net/sourceforge/filebot/web/AnidbClient.java +++ b/source/net/sourceforge/filebot/web/AnidbClient.java @@ -3,10 +3,10 @@ package net.sourceforge.filebot.web; import static net.sourceforge.filebot.web.WebRequest.getHtmlDocument; -import static net.sourceforge.tuned.XPathUtil.exists; -import static net.sourceforge.tuned.XPathUtil.selectNode; -import static net.sourceforge.tuned.XPathUtil.selectNodes; -import static net.sourceforge.tuned.XPathUtil.selectString; +import static net.sourceforge.tuned.XPathUtilities.exists; +import static net.sourceforge.tuned.XPathUtilities.selectNode; +import static net.sourceforge.tuned.XPathUtilities.selectNodes; +import static net.sourceforge.tuned.XPathUtilities.selectString; import java.io.IOException; import java.net.MalformedURLException; diff --git a/source/net/sourceforge/filebot/web/MovieDescriptor.java b/source/net/sourceforge/filebot/web/MovieDescriptor.java index 7f716c1d..8b01c7f7 100644 --- a/source/net/sourceforge/filebot/web/MovieDescriptor.java +++ b/source/net/sourceforge/filebot/web/MovieDescriptor.java @@ -17,4 +17,15 @@ public class MovieDescriptor extends SearchResult { return imdbId; } + + @Override + public boolean equals(Object object) { + if (object instanceof MovieDescriptor) { + MovieDescriptor other = (MovieDescriptor) object; + return this.getImdbId() == other.getImdbId() && this.getName() == other.getName(); + } + + return super.equals(object); + } + } diff --git a/source/net/sourceforge/filebot/web/OpenSubtitlesSubtitleDescriptor.java b/source/net/sourceforge/filebot/web/OpenSubtitlesSubtitleDescriptor.java index d697d77a..46d331b5 100644 --- a/source/net/sourceforge/filebot/web/OpenSubtitlesSubtitleDescriptor.java +++ b/source/net/sourceforge/filebot/web/OpenSubtitlesSubtitleDescriptor.java @@ -8,9 +8,6 @@ import java.util.Collections; import java.util.EnumMap; import java.util.Map; import java.util.Map.Entry; -import java.util.logging.Level; -import java.util.logging.Logger; - import net.sourceforge.tuned.DownloadTask; @@ -96,32 +93,13 @@ public class OpenSubtitlesSubtitleDescriptor implements SubtitleDescriptor { } - @Override - public String getAuthor() { - return properties.get(Property.UserNickName); - } - - - public long getSize() { - return Long.parseLong(properties.get(Property.SubSize)); - } - - - public URL getDownloadLink() { - String link = properties.get(Property.ZipDownloadLink); - - try { - return new URL(link); - } catch (MalformedURLException e) { - Logger.getLogger("global").log(Level.WARNING, "Invalid download link: " + link); - return null; - } - } - - @Override public DownloadTask createDownloadTask() { - return new DownloadTask(getDownloadLink()); + try { + return new DownloadTask(new URL(properties.get(Property.ZipDownloadLink))); + } catch (MalformedURLException e) { + throw new UnsupportedOperationException(e); + } } diff --git a/source/net/sourceforge/filebot/web/SearchResult.java b/source/net/sourceforge/filebot/web/SearchResult.java index a214d662..811d4439 100644 --- a/source/net/sourceforge/filebot/web/SearchResult.java +++ b/source/net/sourceforge/filebot/web/SearchResult.java @@ -24,4 +24,5 @@ public abstract class SearchResult implements Serializable { public String toString() { return name; } + } diff --git a/source/net/sourceforge/filebot/web/SubsceneSubtitleClient.java b/source/net/sourceforge/filebot/web/SubsceneSubtitleClient.java index bf284eab..ec393ccc 100644 --- a/source/net/sourceforge/filebot/web/SubsceneSubtitleClient.java +++ b/source/net/sourceforge/filebot/web/SubsceneSubtitleClient.java @@ -3,9 +3,9 @@ package net.sourceforge.filebot.web; import static net.sourceforge.filebot.web.WebRequest.getHtmlDocument; -import static net.sourceforge.tuned.XPathUtil.selectNode; -import static net.sourceforge.tuned.XPathUtil.selectNodes; -import static net.sourceforge.tuned.XPathUtil.selectString; +import static net.sourceforge.tuned.XPathUtilities.selectNode; +import static net.sourceforge.tuned.XPathUtilities.selectNodes; +import static net.sourceforge.tuned.XPathUtilities.selectString; import java.io.IOException; import java.net.MalformedURLException; diff --git a/source/net/sourceforge/filebot/web/SubsceneSubtitleDescriptor.java b/source/net/sourceforge/filebot/web/SubsceneSubtitleDescriptor.java index 487ce372..4b7b3a78 100644 --- a/source/net/sourceforge/filebot/web/SubsceneSubtitleDescriptor.java +++ b/source/net/sourceforge/filebot/web/SubsceneSubtitleDescriptor.java @@ -65,7 +65,7 @@ public class SubsceneSubtitleDescriptor implements SubtitleDescriptor { @Override public String toString() { - return String.format("%s [%s]", title, language); + return String.format("%s [%s]", getName(), getLanguageName()); } } diff --git a/source/net/sourceforge/filebot/web/SubtitleDescriptor.java b/source/net/sourceforge/filebot/web/SubtitleDescriptor.java index a53bd477..f7ae5e01 100644 --- a/source/net/sourceforge/filebot/web/SubtitleDescriptor.java +++ b/source/net/sourceforge/filebot/web/SubtitleDescriptor.java @@ -13,9 +13,6 @@ public interface SubtitleDescriptor { public String getLanguageName(); - public String getAuthor(); - - public String getArchiveType(); diff --git a/source/net/sourceforge/filebot/web/SubtitleSourceClient.java b/source/net/sourceforge/filebot/web/SubtitleSourceClient.java new file mode 100644 index 00000000..de89a8b9 --- /dev/null +++ b/source/net/sourceforge/filebot/web/SubtitleSourceClient.java @@ -0,0 +1,151 @@ + +package net.sourceforge.filebot.web; + + +import static net.sourceforge.filebot.web.WebRequest.getDocument; +import static net.sourceforge.tuned.XPathUtilities.getTextContent; +import static net.sourceforge.tuned.XPathUtilities.selectNodes; + +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; + +import javax.swing.Icon; + +import net.sourceforge.filebot.ResourceManager; + +import org.w3c.dom.Document; +import org.w3c.dom.Node; + + +public class SubtitleSourceClient implements SubtitleClient { + + protected static final String HOST = "www.subtitlesource.org"; + + private static final int PAGE_SIZE = 20; + + + @Override + public String getName() { + return "SubtitleSource"; + } + + + @Override + public Icon getIcon() { + return ResourceManager.getIcon("search.subtitlesource"); + } + + + @Override + public List search(String query) throws Exception { + return search(query, "all"); + } + + + public List search(String query, String language) throws Exception { + // e.g. http://www.subtitlesource.org/api/xmlsearch/firefly/all/0 + URL url = new URL("http", HOST, "/api/xmlsearch/" + URLEncoder.encode(query, "utf-8") + "/" + language + "/0"); + + Document dom = getDocument(url); + + Map movieMap = new LinkedHashMap(); + + for (Node node : selectNodes("//sub", dom)) { + Integer imdb = Integer.valueOf(getTextContent("imdb", node)); + + if (!movieMap.containsKey(imdb)) { + String title = getTextContent("title", node); + movieMap.put(imdb, title); + } + } + + // create SearchResult collection + List result = new ArrayList(); + + for (Entry movie : movieMap.entrySet()) { + result.add(new MovieDescriptor(movie.getValue(), movie.getKey())); + } + + return result; + } + + + @Override + public List getSubtitleList(SearchResult searchResult, Locale language) throws Exception { + // english language name or null + String languageFilter = (language == null || language == Locale.ROOT) ? null : language.getDisplayLanguage(Locale.ENGLISH); + + List subtitles = new ArrayList(); + + for (SubtitleDescriptor subtitle : getSubtitleList(searchResult)) { + if (languageFilter == null || languageFilter.equalsIgnoreCase(subtitle.getLanguageName())) { + subtitles.add(subtitle); + } + } + + return subtitles; + } + + + public List getSubtitleList(SearchResult searchResult) throws Exception { + List subtitles = new ArrayList(); + + for (int offset = 0; true; offset += PAGE_SIZE) { + List page = getSubtitleList(searchResult, offset); + + // add new subtitles + subtitles.addAll(page); + + if (page.size() < PAGE_SIZE) { + // last page reached + return subtitles; + } + } + } + + + public List getSubtitleList(SearchResult searchResult, int offset) throws Exception { + int imdb = ((MovieDescriptor) searchResult).getImdbId(); + + // e.g. http://www.subtitlesource.org/api/xmlsearch/0303461/imdb/0 + URL url = new URL("http", HOST, "/api/xmlsearch/" + imdb + "/imdb/" + offset); + + Document dom = getDocument(url); + + List subtitles = new ArrayList(); + + for (Node node : selectNodes("//sub", dom)) { + int id = Integer.parseInt(getTextContent("id", node)); + String releaseName = getTextContent("releasename", node); + String language = getTextContent("language", node); + String title = getTextContent("title", node); + int season = Integer.parseInt(getTextContent("season", node)); + int episode = Integer.parseInt(getTextContent("episode", node)); + + subtitles.add(new SubtitleSourceSubtitleDescriptor(id, releaseName, language, title, season, episode)); + } + + return subtitles; + } + + + @Override + public URI getSubtitleListLink(SearchResult searchResult, Locale language) { + int imdb = ((MovieDescriptor) searchResult).getImdbId(); + + try { + return new URI("http://" + HOST + "/title/" + String.format("tt%07d", imdb)); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/source/net/sourceforge/filebot/web/SubtitleSourceSubtitleDescriptor.java b/source/net/sourceforge/filebot/web/SubtitleSourceSubtitleDescriptor.java new file mode 100644 index 00000000..25416f26 --- /dev/null +++ b/source/net/sourceforge/filebot/web/SubtitleSourceSubtitleDescriptor.java @@ -0,0 +1,90 @@ + +package net.sourceforge.filebot.web; + + +import static net.sourceforge.filebot.web.SubtitleSourceClient.HOST; + +import java.net.MalformedURLException; +import java.net.URL; + +import net.sourceforge.tuned.DownloadTask; + + +public class SubtitleSourceSubtitleDescriptor implements SubtitleDescriptor { + + private final int id; + + private final String releaseName; + private final String language; + + private final String title; + private final int season; + private final int episode; + + + public SubtitleSourceSubtitleDescriptor(int id, String releaseName, String language, String title, int season, int episode) { + this.id = id; + this.releaseName = releaseName; + this.language = language; + this.title = title; + this.season = season; + this.episode = episode; + } + + + @Override + public String getName() { + if (releaseName == null || releaseName.isEmpty()) { + if (season == 0 && episode == 0) { + return title; + } + + StringBuilder sb = new StringBuilder(title).append(" - "); + + if (season != 0) { + sb.append(season); + + if (episode != 0) { + sb.append("x").append(episode); + } + } else { + // episode cannot be 0 at this point + sb.append(episode); + } + + return sb.toString(); + } + + return releaseName; + } + + + @Override + public String getLanguageName() { + return language; + } + + + @Override + public DownloadTask createDownloadTask() { + try { + // e.g. http://www.subtitlesource.org/download/zip/760 + return new DownloadTask(new URL("http", HOST, "/download/zip/" + id)); + } catch (MalformedURLException e) { + throw new UnsupportedOperationException(e); + } + } + + + @Override + public String getArchiveType() { + return "zip"; + } + + + @Override + public String toString() { + return String.format("%s [%s]", getName(), getLanguageName()); + } + +} diff --git a/source/net/sourceforge/filebot/web/TVDotComClient.java b/source/net/sourceforge/filebot/web/TVDotComClient.java index 00bd0a80..9aec870a 100644 --- a/source/net/sourceforge/filebot/web/TVDotComClient.java +++ b/source/net/sourceforge/filebot/web/TVDotComClient.java @@ -3,8 +3,8 @@ package net.sourceforge.filebot.web; import static net.sourceforge.filebot.web.WebRequest.getHtmlDocument; -import static net.sourceforge.tuned.XPathUtil.selectNodes; -import static net.sourceforge.tuned.XPathUtil.selectString; +import static net.sourceforge.tuned.XPathUtilities.selectNodes; +import static net.sourceforge.tuned.XPathUtilities.selectString; import java.io.IOException; import java.net.URI; diff --git a/source/net/sourceforge/filebot/web/TVRageClient.java b/source/net/sourceforge/filebot/web/TVRageClient.java index 13f73fa7..d6c1b8c3 100644 --- a/source/net/sourceforge/filebot/web/TVRageClient.java +++ b/source/net/sourceforge/filebot/web/TVRageClient.java @@ -3,10 +3,10 @@ package net.sourceforge.filebot.web; import static net.sourceforge.filebot.web.WebRequest.getDocument; -import static net.sourceforge.tuned.XPathUtil.getTextContent; -import static net.sourceforge.tuned.XPathUtil.selectInteger; -import static net.sourceforge.tuned.XPathUtil.selectNodes; -import static net.sourceforge.tuned.XPathUtil.selectString; +import static net.sourceforge.tuned.XPathUtilities.getTextContent; +import static net.sourceforge.tuned.XPathUtilities.selectInteger; +import static net.sourceforge.tuned.XPathUtilities.selectNodes; +import static net.sourceforge.tuned.XPathUtilities.selectString; import java.io.IOException; import java.net.URI; diff --git a/source/net/sourceforge/filebot/web/TheTVDBClient.java b/source/net/sourceforge/filebot/web/TheTVDBClient.java index 636ff9ce..85c7d201 100644 --- a/source/net/sourceforge/filebot/web/TheTVDBClient.java +++ b/source/net/sourceforge/filebot/web/TheTVDBClient.java @@ -3,14 +3,15 @@ package net.sourceforge.filebot.web; import static net.sourceforge.filebot.web.WebRequest.getDocument; -import static net.sourceforge.tuned.XPathUtil.getTextContent; -import static net.sourceforge.tuned.XPathUtil.selectInteger; -import static net.sourceforge.tuned.XPathUtil.selectNodes; -import static net.sourceforge.tuned.XPathUtil.selectString; +import static net.sourceforge.tuned.XPathUtilities.getTextContent; +import static net.sourceforge.tuned.XPathUtilities.selectInteger; +import static net.sourceforge.tuned.XPathUtilities.selectNodes; +import static net.sourceforge.tuned.XPathUtilities.selectString; import java.io.FileNotFoundException; import java.io.IOException; import java.net.URI; +import java.net.URISyntaxException; import java.net.URL; import java.net.URLEncoder; import java.util.ArrayList; @@ -205,7 +206,11 @@ public class TheTVDBClient implements EpisodeListClient { public URI getEpisodeListLink(SearchResult searchResult) { int seriesId = ((TheTVDBSearchResult) searchResult).getSeriesId(); - return URI.create("http://www.thetvdb.com/?tab=seasonall&id=" + seriesId); + try { + return new URI("http://" + host + "/?tab=seasonall&id=" + seriesId); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } } @@ -225,9 +230,12 @@ public class TheTVDBClient implements EpisodeListClient { cache.putSeasonId(seriesId, season, seasonId); } - return new URI("http://www.thetvdb.com/?tab=season&seriesid=" + seriesId + "&seasonid=" + seasonId); - } catch (Exception e) { + return new URI("http://" + host + "/?tab=season&seriesid=" + seriesId + "&seasonid=" + seasonId); + } catch (IOException e) { + // log and ignore any IOException Logger.getLogger("global").log(Level.WARNING, "Failed to retrieve season id", e); + } catch (Exception e) { + throw new RuntimeException(e); } return null; diff --git a/source/net/sourceforge/tuned/XPathUtil.java b/source/net/sourceforge/tuned/XPathUtilities.java similarity index 97% rename from source/net/sourceforge/tuned/XPathUtil.java rename to source/net/sourceforge/tuned/XPathUtilities.java index 0a5bbfb4..71f18617 100644 --- a/source/net/sourceforge/tuned/XPathUtil.java +++ b/source/net/sourceforge/tuned/XPathUtilities.java @@ -14,7 +14,7 @@ import org.w3c.dom.Node; import org.w3c.dom.NodeList; -public final class XPathUtil { +public final class XPathUtilities { public static Node selectNode(String xpath, Object node) { try { @@ -95,7 +95,7 @@ public final class XPathUtil { /** * Dummy constructor to prevent instantiation. */ - private XPathUtil() { + private XPathUtilities() { throw new UnsupportedOperationException(); } diff --git a/test/net/sourceforge/filebot/web/SubsceneSubtitleClientTest.java b/test/net/sourceforge/filebot/web/SubsceneSubtitleClientTest.java index 7ed796b7..14c79f3d 100644 --- a/test/net/sourceforge/filebot/web/SubsceneSubtitleClientTest.java +++ b/test/net/sourceforge/filebot/web/SubsceneSubtitleClientTest.java @@ -30,7 +30,7 @@ public class SubsceneSubtitleClientTest { @BeforeClass public static void setUpBeforeClass() throws Exception { - twinpeaksSearchResult = new SubsceneSearchResult("Twin Peaks - First Season (1990)", new URL("http://subscene.com/twin-peaks--first-season/subtitles-32482.aspx"), 17); + twinpeaksSearchResult = new SubsceneSearchResult("Twin Peaks - First Season (1990)", new URL("http://subscene.com/twin-peaks--first-season/subtitles-32482.aspx"), 18); lostSearchResult = new SubsceneSearchResult("Lost - Fourth Season (2008)", new URL("http://subscene.com/Lost-Fourth-Season/subtitles-70963.aspx"), 420); } diff --git a/test/net/sourceforge/filebot/web/SubtitleSourceClientTest.java b/test/net/sourceforge/filebot/web/SubtitleSourceClientTest.java new file mode 100644 index 00000000..2e338413 --- /dev/null +++ b/test/net/sourceforge/filebot/web/SubtitleSourceClientTest.java @@ -0,0 +1,67 @@ + +package net.sourceforge.filebot.web; + + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.List; +import java.util.Locale; + +import org.junit.Test; + + +public class SubtitleSourceClientTest { + + private static final SubtitleSourceClient client = new SubtitleSourceClient(); + + + @Test + public void search() throws Exception { + List list = client.search("babylon 5"); + + MovieDescriptor sample = (MovieDescriptor) list.get(0); + + // check sample entry + assertEquals("Babylon 5", sample.getName()); + assertEquals(105946, sample.getImdbId()); + + // check page size + assertEquals(1, list.size()); + } + + + @Test + public void getSubtitleListAll() throws Exception { + List list = client.getSubtitleList(new MovieDescriptor("Buffy", 118276), Locale.ENGLISH); + + SubtitleDescriptor sample = list.get(0); + + // check sample entry (order is unpredictable) + assertTrue(sample.getName().startsWith("Buffy")); + assertEquals("English", sample.getLanguageName()); + + // check size + assertTrue(list.size() > 100); + } + + + @Test + public void getSubtitleListSinglePage() throws Exception { + List list = client.getSubtitleList(new MovieDescriptor("Firefly", 303461), 0); + + SubtitleDescriptor sample = list.get(0); + + // check sample entry (order is unpredictable) + assertTrue(sample.getName().startsWith("Firefly")); + + // check page size + assertEquals(20, list.size()); + } + + + @Test + public void getSubtitleListLink() { + assertEquals("http://www.subtitlesource.org/title/tt0303461", client.getSubtitleListLink(new MovieDescriptor("Firefly", 303461), null).toString()); + } +} diff --git a/test/net/sourceforge/filebot/web/WebTestSuite.java b/test/net/sourceforge/filebot/web/WebTestSuite.java index 6dc88543..46c885a2 100644 --- a/test/net/sourceforge/filebot/web/WebTestSuite.java +++ b/test/net/sourceforge/filebot/web/WebTestSuite.java @@ -8,7 +8,7 @@ import org.junit.runners.Suite.SuiteClasses; @RunWith(Suite.class) -@SuiteClasses( { TVDotComClientTest.class, AnidbClientTest.class, TVRageClientTest.class, TheTVDBClientTest.class, SubsceneSubtitleClientTest.class, OpenSubtitlesHasherTest.class }) +@SuiteClasses( { TVDotComClientTest.class, AnidbClientTest.class, TVRageClientTest.class, TheTVDBClientTest.class, SubsceneSubtitleClientTest.class, SubtitleSourceClientTest.class, OpenSubtitlesHasherTest.class }) public class WebTestSuite { }