<|start_filename|>src/main/java/com/ivkos/wallhaven4j/util/exceptions/DescriptiveParseExceptionSupplier.java<|end_filename|>
package com.ivkos.wallhaven4j.util.exceptions;

import com.google.common.base.Supplier;
import com.ivkos.wallhaven4j.models.AbstractResource;

import static java.lang.String.format;

public class DescriptiveParseExceptionSupplier implements Supplier<ParseException>
{
   private final Supplier<ParseException> parseExceptionSupplier;

   protected DescriptiveParseExceptionSupplier(Supplier<ParseException> parseExceptionSupplier)
   {
      this.parseExceptionSupplier = parseExceptionSupplier;
   }

   public static <R extends AbstractResource> DescriptiveParseExceptionSupplier forResource(R thrower, String parsingWhat)
   {
      return new DescriptiveParseExceptionSupplier(() -> new ParseException(format("Could not parse %s of %s with id %s",
            parsingWhat, thrower.getClass().getSimpleName(), thrower.getId().toString()
      )));
   }

   public static <R extends AbstractResource> DescriptiveParseExceptionSupplier forResource(Class<R> throwingClass, String parsingWhat)
   {
      return new DescriptiveParseExceptionSupplier(() -> new ParseException(format("Could not parse %s of %s",
            parsingWhat, throwingClass.getSimpleName()
      )));
   }

   @Override
   public ParseException get()
   {
      return parseExceptionSupplier.get();
   }

   public ParseException get(Throwable cause)
   {
      ParseException parseException = parseExceptionSupplier.get();
      parseException.initCause(cause);

      return parseException;
   }
}


<|start_filename|>src/main/java/com/ivkos/wallhaven4j/util/httpclient/HttpClient.java<|end_filename|>
package com.ivkos.wallhaven4j.util.httpclient;

import java.util.Map;

public interface HttpClient
{

   HttpResponse get(String url, Map<String, String> headers);

   HttpResponse get(String url, Map<String, String> headers, Map<String, String> queryParams);

   HttpResponse get(String url);

   HttpResponse post(String url, Map<String, String> headers, String body);

   HttpResponse post(String url, Map<String, String> headers, Map<String, String> formParams);

   HttpResponse post(String url, Map<String, String> formParams);
}


<|start_filename|>src/main/java/com/ivkos/wallhaven4j/models/misc/enums/util/BitfieldSum.java<|end_filename|>
package com.ivkos.wallhaven4j.models.misc.enums.util;

import java.util.Collection;
import java.util.EnumSet;

import static java.util.Arrays.asList;

public final class BitfieldSum
{
   private BitfieldSum()
   {
   }

   public static <E extends Enum<E> & BitfieldCompatible> long of(Collection<E> enumsCollection)
   {
      long result = 0;

      if (enumsCollection.isEmpty()) return result;

      EnumSet<E> enumSet = EnumSet.copyOf(enumsCollection);
      for (Enum<E> theEnum : enumSet) {
         BitfieldCompatible bfc = (BitfieldCompatible) theEnum;

         result += bfc.getBitfieldValue();
      }

      return result;
   }

   @SafeVarargs
   public static <E extends Enum<E> & BitfieldCompatible> long of(E... enums)
   {
      return of(asList(enums));
   }

   public static <E extends Enum<E> & BitfieldCompatible> String asThreeBitBinaryString(Collection<E> enumsCollection)
   {
      return String.format("%03d", Long.parseLong(Long.toBinaryString(of(enumsCollection))));
   }

   @SafeVarargs
   public static <E extends Enum<E> & BitfieldCompatible> String asThreeBitBinaryString(E... enums)
   {
      return asThreeBitBinaryString(asList(enums));
   }
}


<|start_filename|>src/test/java/com/ivkos/wallhaven4j/models/misc/enums/ToplistRangeTest.java<|end_filename|>
package com.ivkos.wallhaven4j.models.misc.enums;

import org.junit.Test;

import static com.ivkos.wallhaven4j.models.misc.enums.ToplistRange.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;

public class ToplistRangeTest
{
   @Test
   public void allConstantsArePresent() throws Exception
   {
      assertNotNull(valueOf("LAST_1_DAY"));
      assertNotNull(valueOf("LAST_3_DAYS"));

      assertNotNull(valueOf("LAST_1_WEEK"));

      assertNotNull(valueOf("LAST_1_MONTH"));
      assertNotNull(valueOf("LAST_3_MONTHS"));
      assertNotNull(valueOf("LAST_6_MONTHS"));

      assertNotNull(valueOf("LAST_1_YEAR"));
   }

   @Test
   public void toAString() throws Exception
   {
      assertEquals("1d", LAST_1_DAY.toString());
      assertEquals("3d", LAST_3_DAYS.toString());

      assertEquals("1w", LAST_1_WEEK.toString());

      assertEquals("1M", LAST_1_MONTH.toString());
      assertEquals("3M", LAST_3_MONTHS.toString());
      assertEquals("6M", LAST_6_MONTHS.toString());

      assertEquals("1y", LAST_1_YEAR.toString());
   }
}


<|start_filename|>src/main/java/com/ivkos/wallhaven4j/models/wallpapercollection/WallpaperCollectionFactory.java<|end_filename|>
package com.ivkos.wallhaven4j.models.wallpapercollection;

import com.ivkos.wallhaven4j.models.ResourceFactory;

public interface WallpaperCollectionFactory extends ResourceFactory<WallpaperCollection, WallpaperCollectionIdentifier>
{
   WallpaperCollection create(boolean preloadDom, WallpaperCollectionIdentifier id, String name);
}


<|start_filename|>src/main/java/com/ivkos/wallhaven4j/util/httpclient/apache/ApacheHttpClient.java<|end_filename|>
package com.ivkos.wallhaven4j.util.httpclient.apache;

import com.google.common.collect.ImmutableMap;
import com.google.common.io.CharStreams;
import com.google.inject.Inject;
import com.ivkos.wallhaven4j.util.exceptions.ConnectionException;
import com.ivkos.wallhaven4j.util.exceptions.ResourceNotAccessibleException;
import com.ivkos.wallhaven4j.util.exceptions.ResourceNotFoundException;
import com.ivkos.wallhaven4j.util.httpclient.HttpClient;
import com.ivkos.wallhaven4j.util.httpclient.HttpResponse;
import org.apache.http.Header;
import org.apache.http.NameValuePair;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.StringEntity;
import org.apache.http.message.BasicHeader;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;

import static com.google.common.collect.Collections2.transform;

public class ApacheHttpClient implements HttpClient
{
   private static final int CONNECTION_TIMEOUT_MS = 10 * 1000;

   private final org.apache.http.client.HttpClient apacheClient;

   @Inject
   ApacheHttpClient(org.apache.http.client.HttpClient apacheClient)
   {
      this.apacheClient = apacheClient;
   }

   private HttpResponse execute(final String method, String url, Map<String, String> headers, String body)
   {
      //region Build request
      HttpEntityEnclosingRequestBase base = new HttpEntityEnclosingRequestBase()
      {
         @Override
         public String getMethod()
         {
            return method;
         }
      };

      base.setURI(URI.create(url));
      base.setConfig(RequestConfig.custom()
            .setConnectionRequestTimeout(CONNECTION_TIMEOUT_MS)
            .setConnectTimeout(CONNECTION_TIMEOUT_MS)
            .setSocketTimeout(CONNECTION_TIMEOUT_MS)
            .build());

      if (headers != null && !headers.isEmpty()) {
         base.setHeaders(
               transform(headers.entrySet(), input -> new BasicHeader(input.getKey(), input.getValue()))
                     .toArray(new Header[0])
         );
      }

      if (body != null) {
         try {
            base.setEntity(new StringEntity(body));
         } catch (UnsupportedEncodingException e) {
            throw new ConnectionException(e);
         }
      }
      //endregion

      org.apache.http.HttpResponse response;
      try {
         response = apacheClient.execute(base);
      } catch (IOException e) {
         throw new ConnectionException(e);
      }

      //region Handle error codes
      int statusCode = response.getStatusLine().getStatusCode();

      if (statusCode >= 400) {
         EntityUtils.consumeQuietly(response.getEntity());
      }

      if (statusCode == 404) {
         throw new ResourceNotFoundException("Resource not found");
      }

      if (statusCode == 403) {
         throw new ResourceNotAccessibleException("Access to resource is forbidden");
      }

      if (statusCode >= 400) {
         throw new ConnectionException("Could not get resource due to HTTP error " + statusCode);
      }
      //endregion

      ApacheHttpResponse theResponse = new ApacheHttpResponse(response);

      base.releaseConnection();

      return theResponse;
   }

   @Override
   public HttpResponse get(String url, Map<String, String> headers)
   {
      return execute("GET", url, headers, null);
   }

   @Override
   public HttpResponse get(String url, Map<String, String> headers, Map<String, String> queryParams)
   {
      URIBuilder uriBuilder;
      try {
         uriBuilder = new URIBuilder(url);
      } catch (URISyntaxException e) {
         throw new ConnectionException(e);
      }

      for (Map.Entry<String, String> entry : queryParams.entrySet()) {
         uriBuilder.addParameter(entry.getKey(), entry.getValue());
      }

      return get(uriBuilder.toString(), headers);
   }

   public HttpResponse get(String url)
   {
      return get(url, Collections.emptyMap());
   }

   @Override
   public HttpResponse post(String url, Map<String, String> headers, String body)
   {
      return execute("POST", url, headers, body);
   }

   @Override
   public HttpResponse post(String url, Map<String, String> headers, Map<String, String> formParams)
   {
      Collection<NameValuePair> pairs = transform(formParams.entrySet(),
            input -> new BasicNameValuePair(input.getKey(), input.getValue()));

      Header contentType;
      String urlEncodedBody;
      try {
         UrlEncodedFormEntity urlEncodedFormEntity = new UrlEncodedFormEntity(pairs);
         contentType = urlEncodedFormEntity.getContentType();

         InputStream content = urlEncodedFormEntity.getContent();
         urlEncodedBody = CharStreams.toString(new InputStreamReader(content));
      } catch (IOException e) {
         throw new ConnectionException(e);
      }

      ImmutableMap<String, String> headers2 = ImmutableMap.<String, String>builder()
            .putAll(headers)
            .put(contentType.getName(), contentType.getValue())
            .build();

      return post(url, headers2, urlEncodedBody);
   }

   @Override
   public HttpResponse post(String url, Map<String, String> formParams)
   {
      return post(url, Collections.emptyMap(), formParams);
   }
}


<|start_filename|>src/main/java/com/ivkos/wallhaven4j/util/searchquery/SearchQueryDefaults.java<|end_filename|>
package com.ivkos.wallhaven4j.util.searchquery;

import com.ivkos.wallhaven4j.models.misc.Ratio;
import com.ivkos.wallhaven4j.models.misc.Resolution;
import com.ivkos.wallhaven4j.models.misc.enums.*;

import java.util.EnumSet;
import java.util.Set;

import static com.ivkos.wallhaven4j.models.misc.enums.Category.*;
import static com.ivkos.wallhaven4j.models.misc.enums.Order.DESC;
import static com.ivkos.wallhaven4j.models.misc.enums.Purity.SFW;
import static com.ivkos.wallhaven4j.models.misc.enums.Sorting.RELEVANCE;
import static com.ivkos.wallhaven4j.models.misc.enums.ToplistRange.LAST_1_MONTH;
import static java.util.Collections.emptySet;

public abstract class SearchQueryDefaults
{
   public static final String DEFAULT_KEYWORDS = "";
   public static final EnumSet<Category> DEFAULT_CATEGORIES = EnumSet.of(GENERAL, ANIME, PEOPLE);
   public static final EnumSet<Purity> DEFAULT_PURITY = EnumSet.of(SFW);
   public static final Sorting DEFAULT_SORTING = RELEVANCE;
   public static final Order DEFAULT_ORDER = DESC;
   public static final ToplistRange DEFAULT_TOPLIST_RANGE = LAST_1_MONTH;
   public static final Set<Resolution> DEFAULT_RESOLUTIONS = emptySet();
   public static final Set<Ratio> DEFAULT_RATIOS = emptySet();
   public static final long DEFAULT_PAGES = 1;
}


<|start_filename|>src/main/java/com/ivkos/wallhaven4j/util/searchquery/SearchQuery.java<|end_filename|>
package com.ivkos.wallhaven4j.util.searchquery;

import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableMap;
import com.ivkos.wallhaven4j.models.misc.Ratio;
import com.ivkos.wallhaven4j.models.misc.Resolution;
import com.ivkos.wallhaven4j.models.misc.enums.*;
import com.ivkos.wallhaven4j.util.UrlPrefixes;
import org.apache.http.client.utils.URIBuilder;

import java.net.URISyntaxException;
import java.util.EnumSet;
import java.util.Map;
import java.util.Set;

import static com.ivkos.wallhaven4j.models.misc.enums.util.BitfieldSum.asThreeBitBinaryString;
import static com.ivkos.wallhaven4j.util.searchquery.SearchQueryDefaults.*;

public class SearchQuery
{
   protected String keywords = DEFAULT_KEYWORDS;
   protected EnumSet<Category> categories = DEFAULT_CATEGORIES;
   protected EnumSet<Purity> purity = DEFAULT_PURITY;
   protected Sorting sorting = DEFAULT_SORTING;
   protected Order order = DEFAULT_ORDER;
   protected ToplistRange toplistRange = DEFAULT_TOPLIST_RANGE;
   protected Set<Resolution> resolutions = DEFAULT_RESOLUTIONS;
   protected Set<Ratio> ratios = DEFAULT_RATIOS;
   protected long pages = DEFAULT_PAGES;

   protected SearchQuery()
   {
   }

   //region getters
   public String getKeywords()
   {
      return keywords;
   }

   public EnumSet<Category> getCategories()
   {
      return categories;
   }

   public EnumSet<Purity> getPurity()
   {
      return purity;
   }

   public Sorting getSorting()
   {
      return sorting;
   }

   public Order getOrder()
   {
      return order;
   }

   public ToplistRange getToplistRange()
   {
      return toplistRange;
   }

   public Set<Resolution> getResolutions()
   {
      return resolutions;
   }

   public Set<Ratio> getRatios()
   {
      return ratios;
   }

   public long getPages()
   {
      return pages;
   }
   //endregion

   public Map<String, String> getQueryParamsMap()
   {
      return ImmutableMap.<String, String>builder()
            .put("q", keywords)
            .put("categories", asThreeBitBinaryString(categories))
            .put("purity", asThreeBitBinaryString(purity))
            .put("resolutions", joinRatioSet(resolutions))
            .put("ratios", joinRatioSet(ratios))
            .put("sorting", sorting.toString())
            .put("order", order.toString())
            .put("topRange", toplistRange.toString())
            .build();
   }

   public String getUrl()
   {
      URIBuilder builder;
      try {
         builder = new URIBuilder(UrlPrefixes.URL_SEARCH);
      } catch (URISyntaxException e) {
         throw new RuntimeException(e);
      }

      for (Map.Entry<String, String> entry : getQueryParamsMap().entrySet()) {
         builder.addParameter(entry.getKey(), entry.getValue());
      }

      return builder.toString();
   }

   protected static <T extends Ratio> String joinRatioSet(Set<T> ratios)
   {
      return Joiner.on(",").join(ratios);
   }
}


<|start_filename|>src/main/java/com/ivkos/wallhaven4j/util/WallhavenGuiceModule.java<|end_filename|>
package com.ivkos.wallhaven4j.util;

import com.google.inject.AbstractModule;
import com.google.inject.Provides;
import com.google.inject.assistedinject.FactoryModuleBuilder;
import com.ivkos.wallhaven4j.models.tag.TagFactory;
import com.ivkos.wallhaven4j.models.tagcategory.TagCategoryFactory;
import com.ivkos.wallhaven4j.models.user.UserFactory;
import com.ivkos.wallhaven4j.models.wallpaper.WallpaperFactory;
import com.ivkos.wallhaven4j.models.wallpapercollection.WallpaperCollectionFactory;
import com.ivkos.wallhaven4j.util.htmlparser.HtmlParser;
import com.ivkos.wallhaven4j.util.htmlparser.jsoup.JsoupHtmlParser;
import com.ivkos.wallhaven4j.util.httpclient.HttpClient;
import com.ivkos.wallhaven4j.util.httpclient.apache.ApacheHttpClient;
import com.ivkos.wallhaven4j.util.jsonserializer.GsonJsonSerializer;
import com.ivkos.wallhaven4j.util.jsonserializer.JsonSerializer;
import com.ivkos.wallhaven4j.util.pagecrawler.thumbnailpage.ThumbnailPageCrawlerFactory;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;

import java.io.File;

import static com.google.inject.Scopes.SINGLETON;

public class WallhavenGuiceModule extends AbstractModule
{
   private static final Class[] factories = {
         TagFactory.class,
         TagCategoryFactory.class,
         UserFactory.class,
         WallpaperFactory.class,
         WallpaperCollectionFactory.class,

         ThumbnailPageCrawlerFactory.class
   };

   private final File cookiesFile;

   public WallhavenGuiceModule()
   {
      this.cookiesFile = null;
   }

   public WallhavenGuiceModule(File cookiesFile)
   {
      this.cookiesFile = cookiesFile;
   }

   @Override
   protected void configure()
   {
      bind(HttpClient.class).to(ApacheHttpClient.class).in(SINGLETON);
      bind(HtmlParser.class).to(JsoupHtmlParser.class).in(SINGLETON);
      bind(JsonSerializer.class).to(GsonJsonSerializer.class).in(SINGLETON);

      for (Class factoryClass : factories) {
         install(new FactoryModuleBuilder().build(factoryClass));
      }
   }

   @Provides
   private org.apache.http.client.HttpClient provideApacheHttpClient(JsonSerializer jsonSerializer)
   {
      HttpClientBuilder builder = HttpClients.custom();

      if (cookiesFile != null) {
         builder.setDefaultCookieStore(new FileCookieStore(jsonSerializer, cookiesFile));
      }

      return builder.build();
   }
}


<|start_filename|>src/test/java/com/ivkos/wallhaven4j/models/tag/TagTest.java<|end_filename|>
package com.ivkos.wallhaven4j.models.tag;

import com.ivkos.wallhaven4j.AbstractWallhavenTest;
import com.ivkos.wallhaven4j.models.misc.enums.Purity;
import org.junit.BeforeClass;
import org.junit.Test;

import static org.junit.Assert.*;

public class TagTest extends AbstractWallhavenTest
{
   private static Tag nature;
   private static Tag trees;

   @BeforeClass
   public static void setUp() throws Exception
   {
      nature = getWallhaven().getTag(37);
      trees = getWallhaven().getTag(115);

      assertNotNull(nature);
      assertNotNull(trees);
   }

   @Test
   public void toAString() throws Exception
   {
      assertEquals("#nature", nature.toString());
      assertEquals("#trees", trees.toString());
   }

   @Test
   public void getName() throws Exception
   {
      assertEquals("nature", nature.getName());
   }

   @Test
   public void getPurity() throws Exception
   {
      assertEquals(Purity.SFW, nature.getPurity());
   }

   @Test
   public void getCategory() throws Exception
   {
      assertNotNull(nature.getCategory());
      assertEquals("Nature", nature.getCategory().getName());
      assertNull(nature.getCategory().getParentCategory());

      assertNotNull(trees.getCategory());
      assertNotNull(trees.getCategory().getParentCategory());
   }

   @Test
   public void getAliases() throws Exception
   {
      assertFalse(nature.getAliases().isEmpty());
   }

   @Test
   public void getTaggedWallpapersCount() throws Exception
   {
      assertTrue(nature.getTaggedWallpapersCount() > 0);
   }

   @Test
   public void getTaggedWallpapersViewCount() throws Exception
   {
      assertTrue(nature.getTaggedWallpapersViewCount() > 0);
   }

   @Test
   public void getSubscribersCount() throws Exception
   {
      assertTrue(nature.getSubscribersCount() > 0);
   }

   @Test
   public void getUser() throws Exception
   {
      assertEquals("SuperBastard", nature.getUser().getId());
   }

   @Test
   public void getDateCreated() throws Exception
   {
      assertNotNull(nature.getDateCreated());
   }
}


<|start_filename|>src/test/java/com/ivkos/wallhaven4j/models/misc/ResolutionTest.java<|end_filename|>
package com.ivkos.wallhaven4j.models.misc;

import org.junit.Assert;
import org.junit.Test;

import static org.junit.Assert.assertEquals;

public class ResolutionTest
{
   private Resolution resolution = new Resolution(1920, 1080);

   @Test
   public void parseResolution() throws Exception
   {
      Resolution r720p = Resolution.parse("1280x720");

      assertEquals(new Resolution(1280, 720), r720p);
   }

   @Test(expected = IllegalArgumentException.class)
   public void parseIllegalResolution() throws Exception
   {
      Resolution.parse("1280 x 720");
   }

   @Test(expected = IllegalArgumentException.class)
   public void parseNullResolution() throws Exception
   {
      Resolution.parse(null);
   }

   @Test(expected = IllegalArgumentException.class)
   public void parseEmptyResolution() throws Exception
   {
      Resolution.parse("");
   }

   @Test
   public void getWidth() throws Exception
   {
      assertEquals(1920, resolution.getWidth());
   }

   @Test
   public void getHeight() throws Exception
   {
      assertEquals(1080, resolution.getHeight());
   }

   @Test
   public void toAString() throws Exception
   {
      assertEquals("1920x1080", resolution.toString());
   }

   @Test
   public void thatEquals() throws Exception
   {
      Assert.assertTrue(new Resolution(1920, 1080).equals(new Resolution(1920, 1080)));
      Assert.assertFalse(new Resolution(1080, 1920).equals(new Resolution(1920, 1080)));
   }

   @Test
   public void thatHashCodeIsEqual() throws Exception
   {
      Assert.assertTrue(new Resolution(1920, 1080).hashCode() == new Resolution(1920, 1080).hashCode());
      Assert.assertFalse(new Resolution(1080, 1080).hashCode() == new Resolution(1920, 1080).hashCode());
   }

   @Test
   public void getRatio() throws Exception
   {
      assertEquals(new Ratio(16, 9), new Resolution(1920, 1080).getRatio());
      assertEquals(new Ratio(9, 16), new Resolution(1080, 1920).getRatio());

      assertEquals(new Ratio(4, 3), new Resolution(1024, 768).getRatio());

      assertEquals(new Ratio(16, 10), new Resolution(1920, 1200).getRatio());
      assertEquals(new Ratio(10, 16), new Resolution(1200, 1920).getRatio());

      assertEquals(new Ratio(8, 5), new Resolution(1920, 1200).getRatio());

      assertEquals(new Ratio(21, 9), new Resolution(2560, 1080).getRatio());
      assertEquals(new Ratio(9, 21), new Resolution(1080, 2560).getRatio());

      assertEquals(new Ratio(1, 1), new Resolution(800, 800).getRatio());
   }
}


<|start_filename|>src/main/java/com/ivkos/wallhaven4j/util/pagecrawler/thumbnailpage/ThumbnailPageCrawler.java<|end_filename|>
package com.ivkos.wallhaven4j.util.pagecrawler.thumbnailpage;

import com.google.common.collect.ImmutableMap;
import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject;
import com.ivkos.wallhaven4j.models.wallpaper.Wallpaper;
import com.ivkos.wallhaven4j.util.exceptions.WallhavenException;
import com.ivkos.wallhaven4j.util.htmlparser.HtmlElement;
import com.ivkos.wallhaven4j.util.htmlparser.HtmlParser;
import com.ivkos.wallhaven4j.util.httpclient.HttpClient;
import com.ivkos.wallhaven4j.util.httpclient.HttpResponse;
import com.ivkos.wallhaven4j.util.pagecrawler.PageCrawler;

import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.Lists.newArrayList;
import static com.google.common.collect.Lists.transform;
import static com.google.common.net.HttpHeaders.CONTENT_TYPE;
import static com.google.common.net.HttpHeaders.X_REQUESTED_WITH;
import static com.google.common.net.MediaType.JSON_UTF_8;
import static java.util.Collections.unmodifiableList;

public class ThumbnailPageCrawler implements PageCrawler<Wallpaper>
{
   private final ThumbnailTransformer thumbnailTransformer;
   private final HttpClient httpClient;
   private final HtmlParser htmlParser;

   private final String url;

   @AssistedInject
   ThumbnailPageCrawler(ThumbnailTransformer thumbnailTransformer,
                        HttpClient httpClient,
                        HtmlParser htmlParser,
                        @Assisted String url)
   {
      this.thumbnailTransformer = thumbnailTransformer;
      this.httpClient = httpClient;
      this.htmlParser = htmlParser;

      this.url = url;
   }

   @Override
   public String getUrl()
   {
      return url;
   }

   @Override
   public List<Wallpaper> getPage(long pageNumber, Map<String, String> queryParams)
   {
      checkArgument(pageNumber > 0, "page number must be greater than zero");

      ImmutableMap<String, String> params = ImmutableMap.<String, String>builder()
            .put("page", Long.toString(pageNumber))
            .putAll(queryParams)
            .build();

      HttpResponse response = httpClient.get(getUrl(), ImmutableMap.of(
            X_REQUESTED_WITH, "XMLHttpRequest",
            CONTENT_TYPE, JSON_UTF_8.toString()
      ), params);

      HtmlElement dom = htmlParser.parse(response.getBody());

      List<HtmlElement> figureElements = dom.find("li > figure");

      return unmodifiableList(transform(figureElements, thumbnailTransformer::transform));
   }

   @Override
   public List<Wallpaper> getPageSequence(long pageCount, Map<String, String> queryParams)
   {
      checkArgument(pageCount > 0, "page count must be greater than zero");

      ExecutorService executorService = Executors.newCachedThreadPool();

      List<Future<List<Wallpaper>>> futures = newArrayList();
      for (long page = 1; page <= pageCount; page++) {
         long finalPage = page;
         futures.add(executorService.submit(() -> getPage(finalPage, queryParams)));
      }

      List<Wallpaper> wallpapers = newArrayList();
      for (Future<List<Wallpaper>> future : futures) {
         try {
            wallpapers.addAll(future.get());
         } catch (InterruptedException e) {
            throw new WallhavenException("Could not get wallpapers in page", e);
         } catch (ExecutionException e) {
            throw new WallhavenException("Could not get wallpapers in page", e.getCause());
         }
      }

      executorService.shutdown();

      return unmodifiableList(wallpapers);
   }
}


<|start_filename|>src/main/java/com/ivkos/wallhaven4j/models/tag/TagFactory.java<|end_filename|>
package com.ivkos.wallhaven4j.models.tag;

import com.ivkos.wallhaven4j.models.ResourceFactory;
import com.ivkos.wallhaven4j.models.misc.enums.Purity;

public interface TagFactory extends ResourceFactory<Tag, Long>
{
   Tag create(boolean preloadDom, Long id, String name, Purity purity);
}


<|start_filename|>src/main/java/com/ivkos/wallhaven4j/util/httpclient/HttpResponse.java<|end_filename|>
package com.ivkos.wallhaven4j.util.httpclient;

public interface HttpResponse
{
   String getBody();
}


<|start_filename|>src/main/java/com/ivkos/wallhaven4j/models/tag/Tag.java<|end_filename|>
package com.ivkos.wallhaven4j.models.tag;


import com.google.common.collect.ImmutableSet;
import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject;
import com.ivkos.wallhaven4j.models.AbstractResource;
import com.ivkos.wallhaven4j.models.ResourceFactoryFactory;
import com.ivkos.wallhaven4j.models.misc.enums.Purity;
import com.ivkos.wallhaven4j.models.tagcategory.TagCategory;
import com.ivkos.wallhaven4j.models.tagcategory.TagCategoryFactory;
import com.ivkos.wallhaven4j.models.user.User;
import com.ivkos.wallhaven4j.util.ResourceFieldGetter;
import com.ivkos.wallhaven4j.util.UrlPrefixes;
import com.ivkos.wallhaven4j.util.WallhavenSession;
import com.ivkos.wallhaven4j.util.exceptions.ParseException;
import com.ivkos.wallhaven4j.util.htmlparser.HtmlElement;
import com.ivkos.wallhaven4j.util.htmlparser.OptionalSelector;
import com.ivkos.wallhaven4j.util.htmlparser.TimeElementParser;
import org.joda.time.DateTime;

import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static com.google.common.collect.Lists.transform;
import static com.ivkos.wallhaven4j.models.misc.enums.Purity.*;
import static java.lang.Long.parseLong;
import static java.lang.String.format;

public class Tag extends AbstractResource<Long>
{
   private final ResourceFactoryFactory rff;

   private String name;
   private Purity purity;
   private TagCategory category;
   private Set<String> aliases;

   private Long taggedWallpapersCount;
   private Long taggedWallpapersViewCount;
   private Long subscribersCount;

   private User user;
   private DateTime dateCreated;

   @AssistedInject
   Tag(WallhavenSession session,
       ResourceFactoryFactory rff,
       @Assisted boolean preloadDom,
       @Assisted long id)
   {
      super(session, preloadDom, id);
      this.rff = rff;

      if (preloadDom) populateFields();
   }

   @AssistedInject
   Tag(WallhavenSession session,
       ResourceFactoryFactory rff,
       @Assisted boolean preloadDom,
       @Assisted long id,
       @Assisted String name,
       @Assisted Purity purity)
   {
      super(session, preloadDom, id);
      this.rff = rff;
      this.name = name;
      this.purity = purity;

      if (preloadDom) populateFields();
   }

   /**
    * Returns the tag's name preceded by a # (e.g. #cars)
    *
    * @return the tag's name preceded by a #
    */
   @Override
   public String toString()
   {
      return "#" + name;
   }

   @Override
   public String getUrl()
   {
      return UrlPrefixes.URL_TAG + "/" + id;
   }

   /**
    * Returns the name
    *
    * @return the name
    */
   @ResourceFieldGetter
   public String getName()
   {
      if (this.name != null) return this.name;

      this.name = OptionalSelector.of(getDom(), "#tag > h1.tagname").get().getText();

      return this.name;
   }

   /**
    * Returns the purity
    *
    * @return the purity
    */
   @ResourceFieldGetter
   public Purity getPurity()
   {
      if (this.purity != null) return this.purity;

      HtmlElement element = OptionalSelector.of(getDom(), "#tag > h1.tagname").get();

      this.purity = element.hasClass("sfw") ? SFW
            : element.hasClass("sketchy") ? SKETCHY
            : element.hasClass("nsfw") ? NSFW
            : null;

      if (purity == null) throw new ParseException("Could not parse purity of tag");

      return this.purity;
   }

   /**
    * Returns the category
    *
    * @return the category
    */
   @ResourceFieldGetter
   public TagCategory getCategory()
   {
      if (this.category != null) return this.category;

      List<HtmlElement> elements = getDom().find("h2.tag-categories > a");

      if (elements.isEmpty()) throw new ParseException("Could not parse tag category");

      final TagCategoryFactory tcf = rff.getFactoryFor(TagCategory.class);
      List<TagCategory> categories = transform(elements, input -> {
         String href = input.getAttribute("href");

         Pattern pattern = Pattern.compile(Pattern.quote(UrlPrefixes.URL_TAGS + "/") + "(\\d+)");
         Matcher matcher = pattern.matcher(href);

         if (!matcher.matches()) {
            throw new ParseException(
                  format(
                        "Could not parse tag category URL\nFound URL %s does not match pattern %s",
                        href,
                        pattern.toString()
                  )
            );
         }

         long id1 = parseLong(matcher.group(1));
         String name1 = input.getText();

         return tcf.create(false, id1, name1);
      });

      TagCategory top = null;
      for (TagCategory current : categories) {
         if (top == null) {
            top = tcf.create(false, current.getId(), current.getName());
         } else {
            top = tcf.create(false, current.getId(), current.getName(), top);
         }
      }

      this.category = top;

      return this.category;
   }

   /**
    * Returns the tag's aliases as a Set of strings
    *
    * @return the tag's aliases in a Set of strings
    */
   @ResourceFieldGetter
   public Set<String> getAliases()
   {
      if (this.aliases != null) return this.aliases;

      HtmlElement element = OptionalSelector.of(getDom(), "#tag > h1.tagname").get();
      String originalTitle = element.getAttribute("title");

      Pattern pattern = Pattern.compile("^" + Pattern.quote("Aliases: ") + "((.+)(,\\s.+)*)?$");
      Matcher matcher = pattern.matcher(originalTitle);

      if (!matcher.matches()) {
         throw new ParseException("Could not parse tag aliases");
      }

      String group = matcher.group(1);

      if (group == null) {
         this.aliases = Collections.emptySet();
         return this.aliases;
      }

      String[] split = group.split(",\\s");

      this.aliases = ImmutableSet.copyOf(split);

      return this.aliases;
   }

   /**
    * Returns the number of wallpapers tagged with this tag
    *
    * @return the number of wallpapers tagged with this tag
    */
   @ResourceFieldGetter
   public Long getTaggedWallpapersCount()
   {
      if (taggedWallpapersCount != null) return taggedWallpapersCount;

      String text = OptionalSelector.of(getDom(), "#tag-info > dl > dd:nth-child(2)")
            .get()
            .getText()
            .replace(",", "");

      try {
         this.taggedWallpapersCount = parseLong(text);
      } catch (NumberFormatException e) {
         throw new ParseException("Could not parse tagged wallpapers count", e);
      }

      return taggedWallpapersCount;
   }

   /**
    * Returns the number of views wallpapers tagged with this tag have gotten
    *
    * @return the number of views wallpapers tagged with this tag have gotten
    */
   @ResourceFieldGetter
   public Long getTaggedWallpapersViewCount()
   {
      if (taggedWallpapersViewCount != null) return taggedWallpapersViewCount;


      String text = OptionalSelector.of(getDom(), "#tag-info > dl > dd:nth-child(4)")
            .get()
            .getText()
            .replace(",", "");

      try {
         this.taggedWallpapersViewCount = parseLong(text);
      } catch (NumberFormatException e) {
         throw new ParseException("Could not parse tagged wallpapers views count", e);
      }

      return taggedWallpapersViewCount;
   }

   /**
    * Returns the number of users subscribed to this tag
    *
    * @return the number of users subscribed to this tag
    */
   @ResourceFieldGetter
   public Long getSubscribersCount()
   {
      if (subscribersCount != null) return subscribersCount;

      String text = OptionalSelector.of(getDom(), "#tag-info > dl > dd:nth-child(6)")
            .get()
            .getText()
            .replace(",", "");

      try {
         this.subscribersCount = parseLong(text);
      } catch (NumberFormatException e) {
         throw new ParseException("Could not parse subscribers count", e);
      }

      return subscribersCount;
   }

   /**
    * Returns the user who's created this tag
    *
    * @return the user who's created this tag
    */
   @ResourceFieldGetter
   public User getUser()
   {
      if (user != null) return user;

      String username = OptionalSelector
            .of(getDom(), "#tag-info > dl > dd:nth-child(8) > a.username")
            .get()
            .getText();

      this.user = rff.getFactoryFor(User.class).create(false, username);

      return user;
   }

   /**
    * Returns the date and time this tag has been created
    *
    * @return the date and time this tag has been created
    */
   @ResourceFieldGetter
   public DateTime getDateCreated()
   {
      if (dateCreated != null) return dateCreated;

      HtmlElement timeElement = OptionalSelector.of(getDom(), "#tag-info > dl > dd:nth-child(8) > time").get();

      dateCreated = TimeElementParser.parse(timeElement);

      return dateCreated;
   }
}


<|start_filename|>src/main/java/com/ivkos/wallhaven4j/util/htmlparser/OptionalSelector.java<|end_filename|>
package com.ivkos.wallhaven4j.util.htmlparser;

import com.google.common.base.Supplier;
import com.ivkos.wallhaven4j.util.exceptions.ParseException;

import java.util.List;

import static com.google.common.base.Preconditions.checkNotNull;

public class OptionalSelector
{
   private final HtmlElement context;
   private final String cssSelector;

   protected OptionalSelector(HtmlElement context, String cssSelector)
   {
      checkNotNull(context, "context must not be null");
      checkNotNull(context, "cssSelector must not be null");

      this.context = context;
      this.cssSelector = cssSelector;
   }

   public static OptionalSelector of(HtmlElement context, String cssSelector)
   {
      return new OptionalSelector(context, cssSelector);
   }

   public HtmlElement get() throws ParseException
   {
      List<HtmlElement> elements = select();

      if (elements.isEmpty()) {
         throw new ParseException("No such element: " + cssSelector);
      }

      return elements.get(0);
   }

   public <X extends Throwable> HtmlElement orElseThrow(Supplier<? extends X> throwable) throws X
   {
      List<HtmlElement> elements = select();

      if (elements.isEmpty()) {
         X x = throwable.get();

         if (x.getCause() == null) {
            x.initCause(new ParseException("No such element: " + cssSelector));
         }

         throw x;
      }

      return elements.get(0);
   }

   public boolean isPresent()
   {
      return !select().isEmpty();
   }

   private List<HtmlElement> select()
   {
      return context.find(cssSelector);
   }
}


<|start_filename|>src/main/java/com/ivkos/wallhaven4j/util/exceptions/LoginException.java<|end_filename|>
package com.ivkos.wallhaven4j.util.exceptions;

/**
 * Thrown to indicate that logging in to Wallhaven was unsuccessful due to incorrect credentials.
 */
public class LoginException extends WallhavenException
{
   public LoginException()
   {
   }

   public LoginException(String message)
   {
      super(message);
   }

   public LoginException(String message, Throwable cause)
   {
      super(message, cause);
   }

   public LoginException(Throwable cause)
   {
      super(cause);
   }

   public LoginException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace)
   {
      super(message, cause, enableSuppression, writableStackTrace);
   }
}


<|start_filename|>src/test/java/com/ivkos/wallhaven4j/models/misc/enums/PurityTest.java<|end_filename|>
package com.ivkos.wallhaven4j.models.misc.enums;

import org.junit.Test;

import static com.ivkos.wallhaven4j.models.misc.enums.Purity.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;

public class PurityTest
{
   @Test
   public void valuesAreCorrect() throws Exception
   {
      assertEquals(4, SFW.getBitfieldValue());
      assertEquals(2, SKETCHY.getBitfieldValue());
      assertEquals(1, NSFW.getBitfieldValue());
   }

   @Test
   public void constantsArePresent() throws Exception
   {
      assertNotNull(Purity.valueOf("SFW"));
      assertNotNull(Purity.valueOf("SKETCHY"));
      assertNotNull(Purity.valueOf("NSFW"));
   }
}


<|start_filename|>src/main/java/com/ivkos/wallhaven4j/util/httpclient/apache/ApacheHttpResponse.java<|end_filename|>
package com.ivkos.wallhaven4j.util.httpclient.apache;

import com.ivkos.wallhaven4j.util.exceptions.ConnectionException;
import com.ivkos.wallhaven4j.util.httpclient.HttpResponse;
import org.apache.http.HttpEntity;
import org.apache.http.util.EntityUtils;

import java.io.IOException;

class ApacheHttpResponse implements HttpResponse
{
   private final org.apache.http.HttpResponse apacheResponse;
   private final String body;

   ApacheHttpResponse(org.apache.http.HttpResponse apacheResponse)
   {
      this.apacheResponse = apacheResponse;
      this.body = getBodyEagerly();
   }

   @Override
   public String getBody()
   {
      return body;
   }

   private String getBodyEagerly()
   {
      HttpEntity theEntity = apacheResponse.getEntity();

      String body;
      try {
         body = EntityUtils.toString(theEntity);
         EntityUtils.consume(theEntity);
      } catch (IOException e) {
         throw new ConnectionException("Could not get response body", e);
      }

      return body;
   }
}


<|start_filename|>src/main/java/com/ivkos/wallhaven4j/util/pagecrawler/PageCrawlerFactory.java<|end_filename|>
package com.ivkos.wallhaven4j.util.pagecrawler;

public interface PageCrawlerFactory<C extends PageCrawler<?>>
{
   C create(String pageUrl);
}


<|start_filename|>src/main/java/com/ivkos/wallhaven4j/models/ResourceFactory.java<|end_filename|>
package com.ivkos.wallhaven4j.models;

public interface ResourceFactory<R extends AbstractResource<I>, I>
{
   R create(boolean preloadDom, I id);
}


<|start_filename|>src/main/java/com/ivkos/wallhaven4j/util/WallhavenSession.java<|end_filename|>
package com.ivkos.wallhaven4j.util;

import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableMap;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.ivkos.wallhaven4j.models.ResourceFactoryFactory;
import com.ivkos.wallhaven4j.models.user.User;
import com.ivkos.wallhaven4j.util.exceptions.LoginException;
import com.ivkos.wallhaven4j.util.exceptions.ParseException;
import com.ivkos.wallhaven4j.util.htmlparser.HtmlElement;
import com.ivkos.wallhaven4j.util.htmlparser.HtmlParser;
import com.ivkos.wallhaven4j.util.htmlparser.OptionalSelector;
import com.ivkos.wallhaven4j.util.httpclient.HttpClient;

import static com.ivkos.wallhaven4j.util.UrlPrefixes.URL_BASE;
import static com.ivkos.wallhaven4j.util.UrlPrefixes.URL_LOGIN;

@Singleton
public class WallhavenSession
{
   private final HttpClient httpClient;
   private final HtmlParser htmlParser;
   private final ResourceFactoryFactory rff;

   private User currentUser;

   @Inject
   WallhavenSession(HttpClient httpClient, HtmlParser htmlParser, ResourceFactoryFactory rff)
   {
      this.httpClient = httpClient;
      this.htmlParser = htmlParser;
      this.rff = rff;
   }

   public HttpClient getHttpClient()
   {
      return httpClient;
   }

   public HtmlParser getHtmlParser()
   {
      return htmlParser;
   }

   public void login(String username, String password)
   {
      Preconditions.checkArgument(!Strings.isNullOrEmpty(username), "username may not be empty");
      Preconditions.checkArgument(!Strings.isNullOrEmpty(password), "password may not be empty");

      String token;
      {
         String indexHtml = getHttpClient().get(URL_BASE).getBody();
         HtmlElement indexDom = getHtmlParser().parse(indexHtml, URL_BASE);

         if (username.equals(getLoggedInUsername(indexDom))) {
            this.currentUser = rff.getFactoryFor(User.class).create(false, username);
            return;
         }

         token = getLoginToken(indexDom);
      }

      String html = httpClient.post(URL_LOGIN, ImmutableMap.of(
            "_token", token,
            "username", username,
            "password", password
      )).getBody();

      Supplier<ParseException> parseExceptionSupplier = () -> new ParseException("Unexpected login redirect behavior");

      HtmlElement dom = htmlParser.parse(html, URL_LOGIN);
      HtmlElement metaRefresh = OptionalSelector.of(dom.findFirst("head"), "meta[http-equiv=refresh]")
            .orElseThrow(parseExceptionSupplier);

      String redirectUrl;
      try {
         redirectUrl = metaRefresh.getAttribute("content").split("url=")[1];
      } catch (IndexOutOfBoundsException e) {
         ParseException parseException = parseExceptionSupplier.get();
         parseException.initCause(e);
         throw parseException;
      }

      if (Strings.isNullOrEmpty(redirectUrl)) {
         throw parseExceptionSupplier.get();
      }

      if (redirectUrl.equals(URL_LOGIN)) {
         throw new LoginException("Username or password incorrect");
      }

      if (!redirectUrl.equals(URL_BASE)) {
         throw parseExceptionSupplier.get();
      }

      this.currentUser = rff.getFactoryFor(User.class).create(false, username);
   }

   public User getCurrentUser()
   {
      return currentUser;
   }

   private String getLoginToken(HtmlElement dom)
   {
      Supplier<ParseException> parseExceptionSupplier = () -> new ParseException("Could not parse login token");

      String token = OptionalSelector
            .of(dom, "input[name=\"_token\"]")
            .orElseThrow(parseExceptionSupplier)
            .getValue();

      if (token.isEmpty()) {
         throw parseExceptionSupplier.get();
      }

      return token;
   }

   private String getLoggedInUsername(HtmlElement dom)
   {
      OptionalSelector os = OptionalSelector.of(dom, "#userpanel > a > span.username");

      if (!os.isPresent()) return null;

      return os.get().getText();
   }
}


<|start_filename|>src/main/java/com/ivkos/wallhaven4j/models/misc/Resolution.java<|end_filename|>
package com.ivkos.wallhaven4j.models.misc;

import com.google.common.base.Preconditions;

import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static com.google.common.base.Strings.isNullOrEmpty;
import static java.lang.Long.parseLong;

public class Resolution extends Ratio
{
   /**
    * Creates a new <code>Resolution</code>.
    *
    * @param width  the width
    * @param height the height
    * @throws IllegalArgumentException if the width or height are not greater than zero
    */
   public Resolution(long width, long height)
   {
      super(width, height);
   }

   /**
    * Parses a resolution string and returns a corresponding Resolution object
    *
    * @param resolutionString resolution string in the format of WxH
    * @return the resolution object
    */
   public static Resolution parse(String resolutionString)
   {
      Preconditions.checkArgument(!isNullOrEmpty(resolutionString), "resolution string must not be null or empty");

      Pattern pattern = Pattern.compile("^(\\d+)[xX](\\d+)$");
      Matcher matcher = pattern.matcher(resolutionString.trim());

      if (!matcher.matches()) {
         throw new IllegalArgumentException("Illegal resolution string format");
      }

      String wString = matcher.group(1);
      String hString = matcher.group(2);

      long w = parseLong(wString);
      long h = parseLong(hString);

      return new Resolution(w, h);
   }

   /**
    * Calculates and returns the <code>Ratio</code> of this resolution.
    *
    * @return the <code>Ratio</code> of this resolution.
    * @see Ratio
    */
   public Ratio getRatio()
   {
      if (getWidth() == getHeight()) {
         return new Ratio(1, 1);
      }

      long divisor = gcd(getWidth(), getHeight());

      long w = getWidth() / divisor;
      long h = getHeight() / divisor;

      //region handle some ratios having better known terms than their "real" ones
      if ((w == 8 && h == 5) || (w == 5 && h == 8)) {
         w *= 2;
         h *= 2;
      }

      if (w == 64 && h == 27) {
         w = 21;
         h = 9;
      }

      if (w == 27 && h == 64) {
         w = 9;
         h = 21;
      }
      //endregion

      return new Ratio(w, h);
   }

   private long gcd(long a, long b)
   {
      if (b == 0) return a;
      else return gcd(b, a % b);
   }

   @Override
   public boolean equals(Object o)
   {
      if (this == o) return true;
      if (!(o instanceof Resolution)) return false;

      Resolution that = (Resolution) o;

      return getWidth() == that.getWidth() &&
            getHeight() == that.getHeight();
   }

   @Override
   public int hashCode()
   {
      return Objects.hash(getWidth(), getHeight());
   }
}


<|start_filename|>src/main/java/com/ivkos/wallhaven4j/util/pagecrawler/thumbnailpage/ThumbnailPageCrawlerFactory.java<|end_filename|>
package com.ivkos.wallhaven4j.util.pagecrawler.thumbnailpage;

import com.ivkos.wallhaven4j.util.pagecrawler.PageCrawlerFactory;

public interface ThumbnailPageCrawlerFactory extends PageCrawlerFactory<ThumbnailPageCrawler>
{
}


<|start_filename|>src/test/java/com/ivkos/wallhaven4j/models/wallpapercollection/WallpaperCollectionTest.java<|end_filename|>
package com.ivkos.wallhaven4j.models.wallpapercollection;

import com.ivkos.wallhaven4j.AbstractWallhavenTest;
import com.ivkos.wallhaven4j.models.misc.enums.Purity;
import com.ivkos.wallhaven4j.models.user.User;
import com.ivkos.wallhaven4j.models.wallpaper.Wallpaper;
import com.ivkos.wallhaven4j.util.exceptions.ResourceNotAccessibleException;
import org.junit.BeforeClass;
import org.junit.Test;

import java.util.List;

import static com.google.common.collect.Collections2.filter;
import static com.google.common.collect.Lists.newArrayList;
import static org.junit.Assert.*;

public class WallpaperCollectionTest extends AbstractWallhavenTest
{
   private static WallpaperCollection wc;
   private static WallpaperCollection wcLoggedIn;

   @BeforeClass
   public static void setUp() throws Exception
   {
      wc = getWallhaven().getWallpaperCollection("Gandalf", 2);
      wcLoggedIn = getWallhaven(true).getWallpaperCollection("Gandalf", 2);
   }

   @Test
   public void getUser() throws Exception
   {
      assertEquals("Gandalf", wc.getUser().getUsername());
   }

   @Test
   public void getName() throws Exception
   {
      assertNotNull(wc.getName());
   }

   @Test
   public void getWallpapersCount() throws Exception
   {
      assertTrue(wc.getWallpapersCount() > 0);
   }

   @Test
   public void getViewsCount() throws Exception
   {
      assertTrue(wc.getViewsCount() > 0);
   }

   @Test
   public void getSubscribersCount() throws Exception
   {
      assertTrue(wc.getSubscribersCount() > 0);
   }

   @Test
   public void getSubscribers() throws Exception
   {
      List<WallpaperCollection> currentUserCollections = getWallhaven(true).getCurrentUser().getCollections();

      if (currentUserCollections.isEmpty()) return;

      List<WallpaperCollection> nonZeroCollections = newArrayList(filter(currentUserCollections,
            c -> c.getSubscribersCount() > 0));

      if (nonZeroCollections.isEmpty()) return;

      WallpaperCollection defaultCollection = nonZeroCollections.get(0);
      List<User> subscribers = defaultCollection.getSubscribers();

      assertNotNull(subscribers);
   }

   @Test(expected = ResourceNotAccessibleException.class)
   public void getSubscribersFailsForForeignCollections() throws Exception
   {
      wc.getSubscribers();
   }

   @Test(expected = ResourceNotAccessibleException.class)
   public void getSubscribersFailsForForeignCollectionsWhileLoggedIn() throws Exception
   {
      wcLoggedIn.getSubscribers();
   }

   @Test
   public void getWallpapers() throws Exception
   {
      List<Wallpaper> wallpapers = wc.getWallpapers(3, Purity.SFW, Purity.SKETCHY, Purity.NSFW);

      assertEquals(3 * 24, wallpapers.size());
   }
}


<|start_filename|>src/test/java/com/ivkos/wallhaven4j/models/misc/RatioTest.java<|end_filename|>
package com.ivkos.wallhaven4j.models.misc;

import org.junit.Assert;
import org.junit.Test;

public class RatioTest
{
   private Ratio ratio = new Ratio(16, 9);

   @Test(expected = IllegalArgumentException.class)
   public void ratioWontAcceptZeroWidth() throws Exception
   {
      new Ratio(0, 1);
   }

   @Test(expected = IllegalArgumentException.class)
   public void ratioWontAcceptZeroHeight() throws Exception
   {
      new Ratio(1, 0);
   }

   @Test(expected = IllegalArgumentException.class)
   public void ratioWontAcceptNegativeWidth() throws Exception
   {
      new Ratio(-2, 1);
   }

   @Test(expected = IllegalArgumentException.class)
   public void ratioWontAcceptNegativeHeight() throws Exception
   {
      new Ratio(1, -2);
   }

   @Test
   public void getWidth() throws Exception
   {
      Assert.assertEquals(16, ratio.getWidth());
   }

   @Test
   public void getHeight() throws Exception
   {
      Assert.assertEquals(9, ratio.getHeight());
   }

   @Test
   public void toAString() throws Exception
   {
      Assert.assertEquals("16x9", ratio.toString());
   }

   @Test
   public void thatEquals() throws Exception
   {
      Assert.assertTrue(new Ratio(16, 9).equals(new Ratio(16, 9)));
      Assert.assertFalse(new Ratio(9, 16).equals(new Ratio(16, 9)));
   }

   @Test
   public void thatHashCodeIsEqual() throws Exception
   {
      Assert.assertTrue(new Ratio(16, 9).hashCode() == new Ratio(16, 9).hashCode());
      Assert.assertTrue(new Ratio(16, 9).hashCode() == new Ratio(32, 18).hashCode());

      Assert.assertFalse(new Ratio(1080, 1080).hashCode() == new Ratio(1920, 1080).hashCode());
   }
}


<|start_filename|>src/main/java/com/ivkos/wallhaven4j/Wallhaven.java<|end_filename|>
package com.ivkos.wallhaven4j;

import com.google.common.base.Strings;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.ivkos.wallhaven4j.models.ResourceFactoryFactory;
import com.ivkos.wallhaven4j.models.tag.Tag;
import com.ivkos.wallhaven4j.models.user.User;
import com.ivkos.wallhaven4j.models.wallpaper.Wallpaper;
import com.ivkos.wallhaven4j.models.wallpapercollection.WallpaperCollection;
import com.ivkos.wallhaven4j.models.wallpapercollection.WallpaperCollectionIdentifier;
import com.ivkos.wallhaven4j.util.UrlPrefixes;
import com.ivkos.wallhaven4j.util.WallhavenGuiceModule;
import com.ivkos.wallhaven4j.util.WallhavenSession;
import com.ivkos.wallhaven4j.util.exceptions.LoginException;
import com.ivkos.wallhaven4j.util.exceptions.ResourceNotAccessibleException;
import com.ivkos.wallhaven4j.util.exceptions.ResourceNotFoundException;
import com.ivkos.wallhaven4j.util.pagecrawler.thumbnailpage.ThumbnailPageCrawlerFactory;
import com.ivkos.wallhaven4j.util.searchquery.SearchQuery;

import java.io.File;
import java.util.List;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;

public class Wallhaven
{
   private final WallhavenSession session;
   private final ResourceFactoryFactory rff;
   private final ThumbnailPageCrawlerFactory thumbnailPageCrawlerFactory;

   /**
    * Creates a new anonymous Wallhaven session.
    *
    * @param cookiesFile file to save cookies in
    */
   public Wallhaven(File cookiesFile)
   {
      Injector injector = Guice.createInjector(new WallhavenGuiceModule(cookiesFile));

      session = injector.getInstance(WallhavenSession.class);
      rff = injector.getInstance(ResourceFactoryFactory.class);
      thumbnailPageCrawlerFactory = injector.getInstance(ThumbnailPageCrawlerFactory.class);
   }

   /**
    * Creates a new Wallhaven session and logs in with the specified credentials.
    *
    * @param username    the username
    * @param password    the password
    * @param cookiesFile file to save cookies in
    * @throws LoginException if the credentials are incorrect
    */
   public Wallhaven(String username, String password, File cookiesFile)
   {
      this(cookiesFile);

      session.login(username, password);
   }

   /**
    * Creates a new anonymous Wallhaven session.
    */
   public Wallhaven()
   {
      this(null);
   }

   /**
    * Creates a new Wallhaven session and logs in with the specified credentials.
    *
    * @param username the username
    * @param password the password
    * @throws LoginException if the credentials are incorrect
    */
   public Wallhaven(String username, String password)
   {
      this(username, password, null);
   }

   /**
    * Returns the currently logged in user, or <code>null</code> if the session is anonymous.
    *
    * @return the currently logged in user, or <code>null</code> if the session is anonymous.
    */
   public User getCurrentUser()
   {
      return session.getCurrentUser();
   }

   /**
    * Finds the tag with the specified id and returns a {@link Tag} object that represents it.
    *
    * @param id the tag's id
    * @return the tag
    * @throws ResourceNotFoundException if there is no tag with the specified id
    * @throws IllegalArgumentException  if the specified id is not greater than zero
    */
   public Tag getTag(long id)
   {
      checkArgument(id > 0, "id must be greater than zero");

      return rff.getFactoryFor(Tag.class).create(true, id);
   }

   /**
    * Finds the user with the specified username and returns a {@link User} object that represents it.
    *
    * @param username the username
    * @return the tag
    * @throws ResourceNotFoundException if there is no user with the specified username
    * @throws IllegalArgumentException  if the specified username is <code>null</code> or empty
    */
   public User getUser(String username)
   {
      checkArgument(!Strings.isNullOrEmpty(username), "username must not be null or empty");

      return rff.getFactoryFor(User.class).create(true, username);
   }

   /**
    * Finds the wallpaper with the specified id and returns a {@link Wallpaper} object that represents it.
    *
    * @param id the wallpaper's id
    * @return the wallpaper
    * @throws ResourceNotFoundException      if there is no wallpaper with the specified id
    * @throws ResourceNotAccessibleException if the wallpaper is NSFW but the current Wallhaven session is anonymous
    * @throws IllegalArgumentException       if the specified id is not greater than zero
    */
   public Wallpaper getWallpaper(long id)
   {
      checkArgument(id > 0, "id must be greater than zero");

      return rff.getFactoryFor(Wallpaper.class).create(true, id);
   }

   /**
    * Finds the wallpaper collection with the specified id owned by the specified user
    * and returns a {@link WallpaperCollection} object that represents it.
    *
    * @param username the username of the user that owns the wallpaper collection
    * @param id       the wallpaper collection id
    * @return the wallpaper collection
    * @throws ResourceNotFoundException if there is no such wallpaper collection
    * @throws IllegalArgumentException  if the username is <code>null</code> or empty, or the id is not greater than zero
    */
   public WallpaperCollection getWallpaperCollection(String username, long id)
   {
      checkArgument(!Strings.isNullOrEmpty(username), "username must not be null or empty");
      checkArgument(id > 0, "id must be greater than zero");

      User user = rff.getFactoryFor(User.class).create(false, username);

      return rff.getFactoryFor(WallpaperCollection.class).create(true, new WallpaperCollectionIdentifier(id, user));
   }

   /**
    * Executes the search query and returns a list of wallpapers matching the query
    *
    * @param searchQuery the search query
    * @return a list of wallpapers matching the query
    * @throws NullPointerException if searchQuery is null
    */
   public List<Wallpaper> search(SearchQuery searchQuery)
   {
      checkNotNull(searchQuery, "searchQuery must not be null");

      return thumbnailPageCrawlerFactory
            .create(UrlPrefixes.URL_SEARCH)
            .getPageSequence(
                  searchQuery.getPages(),
                  searchQuery.getQueryParamsMap()
            );
   }

   /**
    * Executes the search query and returns a list of the wallpapers in the specified page
    *
    * @param searchQuery the search query
    * @param page        the page to fetch
    * @return a list of wallpapers matching the query
    * @throws IllegalArgumentException if page is not greater than zero
    * @throws NullPointerException     if searchQuery is null
    */
   public List<Wallpaper> search(SearchQuery searchQuery, long page)
   {
      checkNotNull(searchQuery, "searchQuery must not be null");
      checkArgument(page > 0, "page must be greater than zero");

      return thumbnailPageCrawlerFactory
            .create(UrlPrefixes.URL_SEARCH)
            .getPage(page, searchQuery.getQueryParamsMap());
   }
}


<|start_filename|>src/main/java/com/ivkos/wallhaven4j/models/wallpaper/WallpaperTransformers.java<|end_filename|>
package com.ivkos.wallhaven4j.models.wallpaper;

import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.ivkos.wallhaven4j.models.ResourceFactoryFactory;
import com.ivkos.wallhaven4j.models.misc.Color;
import com.ivkos.wallhaven4j.models.misc.enums.Purity;
import com.ivkos.wallhaven4j.models.tag.Tag;
import com.ivkos.wallhaven4j.models.tag.TagFactory;
import com.ivkos.wallhaven4j.models.user.User;
import com.ivkos.wallhaven4j.models.wallpapercollection.WallpaperCollection;
import com.ivkos.wallhaven4j.models.wallpapercollection.WallpaperCollectionFactory;
import com.ivkos.wallhaven4j.models.wallpapercollection.WallpaperCollectionIdentifier;
import com.ivkos.wallhaven4j.util.exceptions.ParseException;
import com.ivkos.wallhaven4j.util.htmlparser.HtmlElement;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static com.ivkos.wallhaven4j.models.misc.enums.Purity.*;
import static com.ivkos.wallhaven4j.util.exceptions.DescriptiveParseExceptionSupplier.forResource;
import static com.ivkos.wallhaven4j.util.htmlparser.OptionalSelector.of;
import static java.lang.Long.parseLong;
import static java.lang.String.format;


@Singleton
class WallpaperTransformers
{
   private final ResourceFactoryFactory rff;

   @Inject
   WallpaperTransformers(ResourceFactoryFactory rff)
   {
      this.rff = rff;
   }

   WallpaperCollection transformToWallpaperCollection(HtmlElement input)
   {
      String username = of(input, "a.username")
            .orElseThrow(forResource(WallpaperCollection.class, "username"))
            .getText();

      User user = rff.getFactoryFor(User.class).create(false, username);

      HtmlElement lastLink = input.findLast("a");
      if (lastLink == null)
         throw forResource(WallpaperCollection.class, "URL of collection").get();

      String name = lastLink.getText();
      String href = lastLink.getAttribute("href");
      Pattern pattern = Pattern.compile(Pattern.quote(user.getUrl()) + "/favorites/(\\d+)");
      Matcher matcher = pattern.matcher(href);

      if (!matcher.matches())
         throw new ParseException(
               format("Could not parse wallpaper collection URL.\nFound URL %s does not match pattern %s",
                     href, pattern.toString()));

      long id = parseLong(matcher.group(1));

      return ((WallpaperCollectionFactory) rff.getFactoryFor(WallpaperCollection.class)).create(
            false,
            new WallpaperCollectionIdentifier(id, user),
            name
      );
   }

   Tag transformToTag(HtmlElement input)
   {
      long tagId = parseLong(input.getDataAttributes().get("tag-id"));
      String tagName = input.findFirst("a.tagname").getText();

      Purity tagPurity = input.hasClass("tag-sfw") ? SFW
            : input.hasClass("tag-sketchy") ? SKETCHY
            : input.hasClass("tag-nsfw") ? NSFW
            : null;

      if (tagPurity == null) throw new ParseException("Could not parse purity of tag");

      return ((TagFactory) rff.getFactoryFor(Tag.class)).create(false, tagId, tagName, tagPurity);
   }

   Color transformToColor(HtmlElement input)
   {
      Pattern PATTERN_COLOR = Pattern.compile("background-color:(#[0-9A-Fa-f]{6})");
      Matcher matcher = PATTERN_COLOR.matcher(input.getAttribute("style"));

      if (!matcher.matches()) throw new ParseException("Could not parse color");

      String hex = matcher.group(1);

      return new Color(hex);
   }
}


<|start_filename|>src/main/java/com/ivkos/wallhaven4j/util/UrlPrefixes.java<|end_filename|>
package com.ivkos.wallhaven4j.util;

public abstract class UrlPrefixes
{
   public static final String URL_BASE = "https://alpha.wallhaven.cc";
   public static final String URL_WALLPAPER = URL_BASE + "/wallpaper";
   public static final String URL_WALLPAPER_SHORT = "https://whvn.cc";
   public static final String URL_TAG = URL_BASE + "/tag";
   public static final String URL_TAGS = URL_BASE + "/tags";
   public static final String URL_USER = URL_BASE + "/user";
   public static final String URL_LOGIN = URL_BASE + "/auth/login";
   public static final String URL_SEARCH = URL_BASE + "/search";

   public static final String URL_IMAGE_THUMB = "https://wallpapers.wallhaven.cc/wallpapers/thumb/small/th-";
}


<|start_filename|>src/main/java/com/ivkos/wallhaven4j/models/misc/enums/Category.java<|end_filename|>
package com.ivkos.wallhaven4j.models.misc.enums;

import com.ivkos.wallhaven4j.models.misc.enums.util.BitfieldCompatible;

public enum Category implements BitfieldCompatible
{
   PEOPLE, ANIME, GENERAL
}


<|start_filename|>src/test/java/com/ivkos/wallhaven4j/models/misc/enums/CategoryTest.java<|end_filename|>
package com.ivkos.wallhaven4j.models.misc.enums;

import org.junit.Test;

import static com.ivkos.wallhaven4j.models.misc.enums.Category.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;

public class CategoryTest
{
   @Test
   public void valuesAreCorrect() throws Exception
   {
      assertEquals(4, GENERAL.getBitfieldValue());
      assertEquals(2, ANIME.getBitfieldValue());
      assertEquals(1, PEOPLE.getBitfieldValue());
   }

   @Test
   public void constantsArePresent() throws Exception
   {
      assertNotNull(Category.valueOf("GENERAL"));
      assertNotNull(Category.valueOf("ANIME"));
      assertNotNull(Category.valueOf("PEOPLE"));
   }
}


<|start_filename|>src/test/java/com/ivkos/wallhaven4j/models/tagcategory/TagCategoryTest.java<|end_filename|>
package com.ivkos.wallhaven4j.models.tagcategory;

import com.ivkos.wallhaven4j.AbstractWallhavenTest;
import com.ivkos.wallhaven4j.models.tag.Tag;
import org.junit.BeforeClass;
import org.junit.Test;

import static org.junit.Assert.*;

public class TagCategoryTest extends AbstractWallhavenTest
{
   private static Tag nature;
   private static Tag cat;

   private static TagCategory natureCategory;
   private static TagCategory animalsCategory;

   @BeforeClass
   public static void setUp() throws Exception
   {
      nature = getWallhaven().getTag(37);
      cat = getWallhaven().getTag(43);

      assertNotNull(nature);
      assertNotNull(cat);

      natureCategory = nature.getCategory();
      animalsCategory = cat.getCategory();

      assertNotNull(natureCategory);
      assertNotNull(animalsCategory);
   }

   @Test
   public void getName() throws Exception
   {
      assertEquals("Nature", natureCategory.getName());
      assertEquals("Animals", animalsCategory.getName());
   }

   @Test
   public void getParentCategory() throws Exception
   {
      assertNull(natureCategory.getParentCategory());

      assertEquals(natureCategory, animalsCategory.getParentCategory());

      assertNull(animalsCategory.getParentCategory().getParentCategory());
   }

   @Test
   public void toAString() throws Exception
   {
      assertEquals("Nature", natureCategory.toString());
      assertEquals("Animals", animalsCategory.toString());
   }
}


<|start_filename|>src/main/java/com/ivkos/wallhaven4j/util/FileCookieStore.java<|end_filename|>
package com.ivkos.wallhaven4j.util;

import com.google.common.io.Files;
import com.google.common.reflect.TypeToken;
import com.ivkos.wallhaven4j.util.exceptions.WallhavenException;
import com.ivkos.wallhaven4j.util.jsonserializer.JsonSerializer;
import org.apache.http.client.CookieStore;
import org.apache.http.cookie.Cookie;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.cookie.BasicClientCookie;

import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Date;
import java.util.List;
import java.util.Set;

class FileCookieStore implements CookieStore
{
   private final JsonSerializer jsonSerializer;
   private final File cookiesFile;
   private final CookieStore store = new BasicCookieStore();

   FileCookieStore(JsonSerializer jsonSerializer, File cookiesFile)
   {
      this.jsonSerializer = jsonSerializer;
      this.cookiesFile = cookiesFile;

      if (!cookiesFile.exists()) {
         try {
            Files.createParentDirs(cookiesFile);
            Files.touch(cookiesFile);
         } catch (IOException e) {
            throw new WallhavenException("Could not create cookie file", e);
         }
      }

      readCookiesFromFile();
   }

   private void readCookiesFromFile()
   {
      String json;
      try {
         json = Files.toString(cookiesFile, Charset.defaultCharset());
      } catch (IOException e) {
         throw new WallhavenException("Could not load cookies from file", e);
      }

      if (json.isEmpty()) return;

      TypeToken<Set<BasicClientCookie>> typeToken = new TypeToken<Set<BasicClientCookie>>()
      {
      };

      Set<Cookie> cookiesInFile = jsonSerializer.fromJson(json, typeToken.getType());

      for (Cookie cookie : cookiesInFile) {
         store.addCookie(cookie);
      }
   }

   private void writeCookiesToFile()
   {
      String json = jsonSerializer.toJson(store.getCookies());

      try {
         Files.write(json.getBytes(), cookiesFile);
      } catch (IOException e) {
         throw new WallhavenException("Could not write cookies to file", e);
      }
   }

   @Override
   public void addCookie(Cookie cookie)
   {
      store.addCookie(cookie);
      writeCookiesToFile();
   }

   @Override
   public List<Cookie> getCookies()
   {
      return store.getCookies();
   }

   @Override
   public boolean clearExpired(Date date)
   {
      boolean result = store.clearExpired(date);
      writeCookiesToFile();
      return result;
   }

   @Override
   public void clear()
   {
      store.clear();
      writeCookiesToFile();
   }
}


<|start_filename|>src/main/java/com/ivkos/wallhaven4j/util/htmlparser/jsoup/JsoupHtmlElement.java<|end_filename|>
package com.ivkos.wallhaven4j.util.htmlparser.jsoup;

import com.ivkos.wallhaven4j.util.htmlparser.HtmlElement;
import org.jsoup.nodes.Element;

import java.util.Collections;
import java.util.List;
import java.util.Map;

import static com.google.common.collect.Lists.transform;

class JsoupHtmlElement implements HtmlElement
{
   private final Element jsoupElement;

   JsoupHtmlElement(Element jsoupElement)
   {
      this.jsoupElement = jsoupElement;
   }

   @Override
   public List<HtmlElement> find(String cssQuery)
   {
      return Collections.unmodifiableList(transform(jsoupElement.select(cssQuery), JsoupHtmlElement::new));
   }

   @Override
   public HtmlElement findFirst(String cssQuery)
   {
      List<HtmlElement> result = find(cssQuery);

      return result.isEmpty() ? null : result.get(0);
   }

   @Override
   public HtmlElement findLast(String cssQuery)
   {
      List<HtmlElement> result = find(cssQuery);

      return result.isEmpty() ? null : result.get(result.size() - 1);
   }

   @Override
   public HtmlElement findElementById(String id)
   {
      return new JsoupHtmlElement(jsoupElement.getElementById(id));
   }

   @Override
   public HtmlElement getNextElementSibling()
   {
      return new JsoupHtmlElement(jsoupElement.nextElementSibling());
   }

   @Override
   public String getText()
   {
      return jsoupElement.text();
   }

   @Override
   public String getAttribute(String attributeName)
   {
      return jsoupElement.attr(attributeName);
   }

   @Override
   public boolean hasClass(String className)
   {
      return jsoupElement.hasClass(className);
   }

   @Override
   public Map<String, String> getDataAttributes()
   {
      return jsoupElement.dataset();
   }

   @Override
   public String getValue()
   {
      return jsoupElement.val();
   }

   @Override
   public String getTagName()
   {
      return jsoupElement.tagName();
   }
}


<|start_filename|>src/main/java/com/ivkos/wallhaven4j/util/htmlparser/HtmlElement.java<|end_filename|>
package com.ivkos.wallhaven4j.util.htmlparser;

import java.util.List;
import java.util.Map;

public interface HtmlElement
{
   List<HtmlElement> find(String cssQuery);

   HtmlElement findFirst(String cssQuery);

   HtmlElement findLast(String cssQuery);

   HtmlElement findElementById(String id);

   HtmlElement getNextElementSibling();

   String getText();

   String getAttribute(String attributeName);

   boolean hasClass(String className);

   Map<String, String> getDataAttributes();

   String getValue();

   String getTagName();
}


<|start_filename|>src/main/java/com/ivkos/wallhaven4j/models/wallpapercollection/WallpaperCollectionTransformers.java<|end_filename|>
package com.ivkos.wallhaven4j.models.wallpapercollection;

import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.ivkos.wallhaven4j.models.ResourceFactoryFactory;
import com.ivkos.wallhaven4j.models.user.User;
import com.ivkos.wallhaven4j.util.htmlparser.HtmlElement;
import com.ivkos.wallhaven4j.util.htmlparser.OptionalSelector;

import static com.ivkos.wallhaven4j.util.exceptions.DescriptiveParseExceptionSupplier.forResource;

@Singleton
class WallpaperCollectionTransformers
{
   private final ResourceFactoryFactory rff;

   @Inject
   WallpaperCollectionTransformers(ResourceFactoryFactory rff)
   {
      this.rff = rff;
   }

   public User transformSubscribers(HtmlElement input)
   {
      String username = OptionalSelector.of(input, "a.username")
            .orElseThrow(forResource(WallpaperCollection.class, "subscribed user"))
            .getText()
            .trim();

      return rff.getFactoryFor(User.class).create(false, username);
   }
}


<|start_filename|>src/main/java/com/ivkos/wallhaven4j/models/user/UserFactory.java<|end_filename|>
package com.ivkos.wallhaven4j.models.user;

import com.ivkos.wallhaven4j.models.ResourceFactory;

public interface UserFactory extends ResourceFactory<User, String>
{

}


<|start_filename|>src/test/java/com/ivkos/wallhaven4j/models/wallpaper/WallpaperTest.java<|end_filename|>
package com.ivkos.wallhaven4j.models.wallpaper;

import com.ivkos.wallhaven4j.AbstractWallhavenTest;
import com.ivkos.wallhaven4j.models.misc.enums.Category;
import com.ivkos.wallhaven4j.models.misc.enums.Purity;
import com.ivkos.wallhaven4j.models.wallpapercollection.WallpaperCollection;
import org.junit.BeforeClass;
import org.junit.Test;

import java.util.List;

import static org.junit.Assert.*;

public class WallpaperTest extends AbstractWallhavenTest
{
   private static long WALLPAPER_ID_SFW = 254637;
   private static long WALLPAPER_ID_SKETCHY = 553;
   private static long WALLPAPER_ID_NSFW = 271712;

   private static Wallpaper wSfw, wSketchy, wNsfw;

   @BeforeClass
   public static void setUp() throws Exception
   {
      wSfw = getWallhaven().getWallpaper(WALLPAPER_ID_SFW);
      assertNotNull(wSfw);

      wSketchy = getWallhaven().getWallpaper(WALLPAPER_ID_SKETCHY);
      assertNotNull(wSketchy);

      wNsfw = getWallhaven(true).getWallpaper(WALLPAPER_ID_NSFW);
      assertNotNull(wNsfw);
   }

   @Test
   public void getShortLink() throws Exception
   {
      assertEquals("https://whvn.cc/" + WALLPAPER_ID_SFW, wSfw.getShortLink());
   }

   @Test
   public void getResolution() throws Exception
   {
      assertNotNull(wSfw.getResolution());
      assertNotNull(wSketchy.getResolution());
      assertNotNull(wNsfw.getResolution());
   }

   @Test
   public void getColors() throws Exception
   {
      assertNotNull(wSfw.getColors());
      assertFalse(wSfw.getColors().isEmpty());

      assertNotNull(wSketchy.getColors());
      assertFalse(wSketchy.getColors().isEmpty());

      assertNotNull(wNsfw.getColors());
      assertFalse(wNsfw.getColors().isEmpty());
   }

   @Test
   public void getTags() throws Exception
   {
      assertNotNull(wSfw.getTags());
      assertFalse(wSfw.getTags().isEmpty());

      assertNotNull(wSketchy.getTags());
      assertFalse(wSketchy.getTags().isEmpty());

      assertNotNull(wNsfw.getTags());
      assertFalse(wNsfw.getTags().isEmpty());
   }

   @Test
   public void getPurity() throws Exception
   {
      assertEquals(Purity.SFW, wSfw.getPurity());
      assertEquals(Purity.SKETCHY, wSketchy.getPurity());
      assertEquals(Purity.NSFW, wNsfw.getPurity());
   }

   @Test
   public void getUser() throws Exception
   {
      assertNotNull(wSfw.getUser());
      assertEquals("JB00GIE", wSfw.getUser().getId());

      assertNotNull(wSketchy.getUser());
      assertNotNull(wNsfw.getUser());
   }

   @Test
   public void getDateCreated() throws Exception
   {
      assertNotNull(wSfw.getDateCreated());
      assertNotNull(wSketchy.getDateCreated());
      assertNotNull(wNsfw.getDateCreated());
   }

   @Test
   public void getCategory() throws Exception
   {
      assertEquals(Category.GENERAL, wSfw.getCategory());
      assertEquals(Category.PEOPLE, wSketchy.getCategory());
      assertEquals(Category.PEOPLE, wNsfw.getCategory());
   }

   @Test
   public void getSize() throws Exception
   {
      assertTrue(wSfw.getSize() > 0);
      assertTrue(wSketchy.getSize() > 0);
      assertTrue(wNsfw.getSize() > 0);
   }

   @Test
   public void getViewsCount() throws Exception
   {
      assertTrue(wSfw.getViewsCount() > 0);
      assertTrue(wSketchy.getViewsCount() > 0);
      assertTrue(wNsfw.getViewsCount() > 0);
   }

   @Test
   public void getFavoritesCount() throws Exception
   {
      assertTrue(wSfw.getFavoritesCount() > 0);
      assertTrue(wSketchy.getFavoritesCount() > 0);
      assertTrue(wNsfw.getFavoritesCount() > 0);
   }

   @Test
   public void getCollections() throws Exception
   {
      List<WallpaperCollection> collectionsSfw = wSfw.getCollections();
      assertNotNull(collectionsSfw);
      assertFalse(collectionsSfw.isEmpty());

      List<WallpaperCollection> collectionsSketchy = wSketchy.getCollections();
      assertNotNull(collectionsSketchy);
      assertFalse(collectionsSketchy.isEmpty());

      List<WallpaperCollection> collectionsNsfw = wNsfw.getCollections();
      assertNotNull(collectionsNsfw);
      assertFalse(collectionsNsfw.isEmpty());
   }

   @Test
   public void getImageUrl() throws Exception
   {
      String imageUrl = wSfw.getImageUrl();
      assertNotNull(imageUrl);
      assertTrue(imageUrl.endsWith(".jpg"));

      Wallpaper pngWallpaper = getWallhaven().getWallpaper(154065);
      String pngImageUrl = pngWallpaper.getImageUrl();

      assertNotNull(pngImageUrl);
      assertTrue(pngImageUrl.endsWith(".png"));
   }

   @Test
   public void getThumbnailUrl() throws Exception
   {
      String thumbnailUrl = wSfw.getThumbnailUrl();
      assertNotNull(thumbnailUrl);
      assertFalse(thumbnailUrl.isEmpty());
   }
}


<|start_filename|>src/main/java/com/ivkos/wallhaven4j/models/user/User.java<|end_filename|>
package com.ivkos.wallhaven4j.models.user;

import com.google.common.collect.Collections2;
import com.google.common.net.UrlEscapers;
import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject;
import com.ivkos.wallhaven4j.models.AbstractResource;
import com.ivkos.wallhaven4j.models.ResourceFactoryFactory;
import com.ivkos.wallhaven4j.models.wallpapercollection.WallpaperCollection;
import com.ivkos.wallhaven4j.models.wallpapercollection.WallpaperCollectionFactory;
import com.ivkos.wallhaven4j.models.wallpapercollection.WallpaperCollectionIdentifier;
import com.ivkos.wallhaven4j.util.ResourceFieldGetter;
import com.ivkos.wallhaven4j.util.UrlPrefixes;
import com.ivkos.wallhaven4j.util.WallhavenSession;
import com.ivkos.wallhaven4j.util.exceptions.DescriptiveParseExceptionSupplier;
import com.ivkos.wallhaven4j.util.htmlparser.HtmlElement;
import com.ivkos.wallhaven4j.util.htmlparser.OptionalSelector;
import com.ivkos.wallhaven4j.util.htmlparser.TimeElementParser;
import org.joda.time.DateTime;

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static com.google.common.collect.Lists.newArrayList;
import static com.google.common.collect.Lists.transform;
import static com.ivkos.wallhaven4j.util.exceptions.DescriptiveParseExceptionSupplier.forResource;
import static java.lang.Long.parseLong;
import static java.util.Collections.unmodifiableList;

public class User extends AbstractResource<String>
{
   private final ResourceFactoryFactory rff;

   private String groupName;
   private String description;

   private DateTime lastActiveTime;
   private DateTime dateCreated;

   private Long uploadedWallpapersCount;
   private Long favoriteWallpapersCount;
   private Long taggedWallpapersCount;
   private Long flaggedWallpapersCount;

   private Long subscribersCount;
   private Long profileViewsCount;
   private Long profileCommentsCount;
   private Long forumPostsCount;

   @AssistedInject
   User(WallhavenSession session, ResourceFactoryFactory rff, @Assisted boolean preloadDom, @Assisted String id)
   {
      super(session, preloadDom, id);
      this.rff = rff;

      if (preloadDom) populateFields();
   }

   @Override
   public String getUrl()
   {
      return UrlPrefixes.URL_USER + "/" + UrlEscapers.urlPathSegmentEscaper().escape(id);
   }

   /**
    * Returns the username
    *
    * @return the username
    */
   public String getUsername()
   {
      return getId();
   }

   /**
    * Returns the user group's name this user belongs to (e.g. Developer, User, Administrator, ...)
    *
    * @return the user group's name this user belongs to
    */
   @ResourceFieldGetter
   public String getGroupName()
   {
      if (groupName != null) return groupName;

      groupName = OptionalSelector.of(getDom(), "#user > h4.groupname").get().getText();

      return groupName;
   }

   /**
    * Returns the user's description text or <code>null</code> if none is set
    *
    * @return the user's description text or <code>null</code> if none is set
    */
   @ResourceFieldGetter
   public String getDescription()
   {
      if (description != null) return description;

      OptionalSelector sel = OptionalSelector.of(getDom(), "#profile-bio");

      // Description is optional
      if (!sel.isPresent()) return null;

      description = sel.get().getText();

      return description;
   }

   /**
    * Returns the last time the user has been active online
    *
    * @return the last time the user has been active online
    */
   @ResourceFieldGetter
   public DateTime getLastActiveTime()
   {
      if (lastActiveTime != null) return lastActiveTime;

      HtmlElement lastActiveLabel = getElementNextToLabel("Last Active");
      if (lastActiveLabel == null) return null;

      HtmlElement timeElement = OptionalSelector.of(lastActiveLabel, "time").get();
      lastActiveTime = TimeElementParser.parse(timeElement);

      return lastActiveTime;
   }

   /**
    * Returns the date and time this user account has been created
    *
    * @return the date and time this user account has been created
    */
   @ResourceFieldGetter
   public DateTime getDateCreated()
   {
      if (dateCreated != null) return dateCreated;

      HtmlElement timeElement = OptionalSelector.of(getElementNextToLabel("Joined"), "time").get();

      dateCreated = TimeElementParser.parse(timeElement);

      return dateCreated;
   }

   /**
    * Returns the number of wallpaper this user has uploaded
    *
    * @return the number of wallpaper this user has uploaded
    */
   @ResourceFieldGetter
   public long getUploadedWallpapersCount()
   {
      if (uploadedWallpapersCount != null) return uploadedWallpapersCount;

      String uploadsText = OptionalSelector.of(getElementNextToLabel("Uploads"), "dd")
            .get()
            .getText()
            .replace(",", "");

      uploadedWallpapersCount = parseLong(uploadsText);

      return uploadedWallpapersCount;
   }

   /**
    * Returns the number of favorite wallpapers the user has
    *
    * @return the number of favorite wallpapers the user has
    */
   @ResourceFieldGetter
   public Long getFavoriteWallpapersCount()
   {
      if (favoriteWallpapersCount != null) return favoriteWallpapersCount;

      String favsText = OptionalSelector.of(getElementNextToLabel("Favorites"), "dd")
            .get()
            .getText()
            .replace(",", "");

      favoriteWallpapersCount = parseLong(favsText);

      return favoriteWallpapersCount;
   }

   /**
    * Returns the number of wallpapers this user has tagged
    *
    * @return the number of wallpapers this user has tagged
    */
   @ResourceFieldGetter
   public Long getTaggedWallpapersCount()
   {
      if (taggedWallpapersCount != null) return taggedWallpapersCount;

      String taggedText = OptionalSelector.of(getElementNextToLabel("Wallpapers Tagged"), "dd")
            .get()
            .getText()
            .replace(",", "");

      taggedWallpapersCount = parseLong(taggedText);

      return taggedWallpapersCount;
   }

   /**
    * Returns the number of wallpapers this user has flagged
    *
    * @return the number of wallpapers this user has flagged
    */
   @ResourceFieldGetter
   public Long getFlaggedWallpapersCount()
   {
      if (flaggedWallpapersCount != null) return flaggedWallpapersCount;

      String flaggedText = OptionalSelector.of(getElementNextToLabel("Wallpapers Flagged"), "dd")
            .get()
            .getText()
            .replace(",", "");

      flaggedWallpapersCount = parseLong(flaggedText);

      return flaggedWallpapersCount;
   }

   /**
    * Returns the number of users subscribed to this user
    *
    * @return the number of users subscribed to this user
    */
   @ResourceFieldGetter
   public Long getSubscribersCount()
   {
      if (subscribersCount != null) return subscribersCount;

      String flaggedText = OptionalSelector.of(getElementNextToLabel("Subscribers"), "dd")
            .get()
            .getText()
            .replace(",", "");

      subscribersCount = parseLong(flaggedText);

      return subscribersCount;
   }

   /**
    * Returns the number of views the user's profile has received
    *
    * @return the number of views the user's profile has received
    */
   @ResourceFieldGetter
   public Long getProfileViewsCount()
   {
      if (profileViewsCount != null) return profileViewsCount;

      String flaggedText = OptionalSelector.of(getElementNextToLabel("Profile Views"), "dd")
            .get()
            .getText()
            .replace(",", "");

      profileViewsCount = parseLong(flaggedText);

      return profileViewsCount;
   }

   /**
    * Returns the number of comments on the user's profile page
    *
    * @return the number of comments on the user's profile page
    */
   @ResourceFieldGetter
   public Long getProfileCommentsCount()
   {
      if (profileCommentsCount != null) return profileCommentsCount;

      String flaggedText = OptionalSelector.of(getElementNextToLabel("Profile Comments"), "dd")
            .get()
            .getText()
            .replace(",", "");

      profileCommentsCount = parseLong(flaggedText);

      return profileCommentsCount;
   }

   /**
    * Returns the number of posts the user has posted on the forum
    *
    * @return the number of posts the user has posted on the forum
    */
   @ResourceFieldGetter
   public Long getForumPostsCount()
   {
      if (forumPostsCount != null) return forumPostsCount;

      String flaggedText = OptionalSelector.of(getElementNextToLabel("Forum Posts"), "dd")
            .get()
            .getText()
            .replace(",", "");

      forumPostsCount = parseLong(flaggedText);

      return forumPostsCount;
   }

   /**
    * Returns a list of the user's wallpaper collections
    *
    * @return a list of the user's wallpaper collections
    */
   public List<WallpaperCollection> getCollections()
   {
      String url = getUrl() + "/favorites";

      String html = getSession().getHttpClient().get(url).getBody();
      HtmlElement dom = getSession().getHtmlParser().parse(html, url);

      List<HtmlElement> lis = dom.find("ul#collections > li.collection");

      return unmodifiableList(newArrayList(transform(lis, input -> {
         DescriptiveParseExceptionSupplier supplier = forResource(WallpaperCollection.class, "collection link in user's page");

         HtmlElement a = OptionalSelector.of(input, "a")
               .orElseThrow(supplier);

         String href = a.getAttribute("href").trim();
         Pattern pattern = Pattern.compile("^" + Pattern.quote(getUrl() + "/favorites/") + "(\\d+)$");
         Matcher matcher = pattern.matcher(href);

         if (!matcher.matches()) {
            throw supplier.get();
         }

         long id = 0;
         try {
            id = parseLong(matcher.group(1));
         } catch (NumberFormatException e) {
            throw supplier.get(e);
         }

         String name = OptionalSelector.of(a, "span.collection-label")
               .orElseThrow(forResource(WallpaperCollection.class, "collection name")).getText();

         return ((WallpaperCollectionFactory) rff.getFactoryFor(WallpaperCollection.class))
               .create(false, new WallpaperCollectionIdentifier(id, this), name);
      })));
   }

   private HtmlElement getElementNextToLabel(String labelContent)
   {
      List<HtmlElement> dts = getDom().find("dl.datalist > dt");

      ArrayList<HtmlElement> list = newArrayList(Collections2.filter(dts, e -> labelContent.equals(e.getText())));

      if (list.isEmpty()) return null;

      return list.get(0).getNextElementSibling();
   }
}


<|start_filename|>src/main/java/com/ivkos/wallhaven4j/util/exceptions/ParseException.java<|end_filename|>
package com.ivkos.wallhaven4j.util.exceptions;

/**
 * Thrown to indicate that there has been a problem parsing Wallhaven's HTML.
 */
public class ParseException extends WallhavenException
{
   public ParseException()
   {
   }

   public ParseException(String message)
   {
      super(message);
   }

   public ParseException(String message, Throwable cause)
   {
      super(message, cause);
   }

   public ParseException(Throwable cause)
   {
      super(cause);
   }

   public ParseException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace)
   {
      super(message, cause, enableSuppression, writableStackTrace);
   }
}


<|start_filename|>src/test/java/com/ivkos/wallhaven4j/util/htmlparser/OptionalSelectorTest.java<|end_filename|>
package com.ivkos.wallhaven4j.util.htmlparser;

import com.ivkos.wallhaven4j.util.exceptions.ParseException;
import com.ivkos.wallhaven4j.util.htmlparser.jsoup.JsoupHtmlParser;
import org.junit.BeforeClass;
import org.junit.Test;

import static org.junit.Assert.*;

public class OptionalSelectorTest
{
   private static HtmlElement dom;
   private static OptionalSelector legitSelector;

   @BeforeClass
   public static void setUp() throws Exception
   {
      HtmlParser parser = new JsoupHtmlParser();

      dom = parser.parse("<body><div id=\"id1\"></div></body>");
      assertNotNull(dom);

      legitSelector = OptionalSelector.of(dom, "#id1");
      assertNotNull(legitSelector);
   }

   @Test
   public void getWorks() throws Exception
   {
      HtmlElement htmlElement = legitSelector.get();

      assertNotNull(htmlElement);
      assertEquals("div", htmlElement.getTagName());
   }

   @Test
   public void itDoesNotThrow() throws Exception
   {
      HtmlElement htmlElement = legitSelector.orElseThrow(Exception::new);

      assertNotNull(htmlElement);
      assertEquals("div", htmlElement.getTagName());
   }

   @Test(expected = IllegalArgumentException.class)
   public void itThrowsTheSpecifiedException() throws Exception
   {
      OptionalSelector.of(dom, ".nope").orElseThrow(IllegalArgumentException::new);
   }

   @Test(expected = ParseException.class)
   public void itThrowsParseException() throws Exception
   {
      OptionalSelector.of(dom, ".nope").get();
   }

   @Test
   public void isPresent() throws Exception
   {
      assertTrue(legitSelector.isPresent());
      assertFalse(OptionalSelector.of(dom, "#nope").isPresent());
   }
}


<|start_filename|>src/main/java/com/ivkos/wallhaven4j/models/misc/enums/Order.java<|end_filename|>
package com.ivkos.wallhaven4j.models.misc.enums;

public enum Order
{
   ASC, DESC;

   @Override
   public String toString()
   {
      return name().toLowerCase();
   }
}


<|start_filename|>src/main/java/com/ivkos/wallhaven4j/util/searchquery/SearchQueryBuilder.java<|end_filename|>
package com.ivkos.wallhaven4j.util.searchquery;

import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableSet;
import com.ivkos.wallhaven4j.models.misc.Ratio;
import com.ivkos.wallhaven4j.models.misc.Resolution;
import com.ivkos.wallhaven4j.models.misc.enums.*;

import java.util.Collection;
import java.util.EnumSet;

import static com.ivkos.wallhaven4j.util.searchquery.SearchQueryDefaults.*;
import static java.util.Arrays.asList;

public class SearchQueryBuilder
{
   private final SearchQuery query = new SearchQuery();

   public SearchQueryBuilder()
   {
   }

   private static <T> T checkNotNull(T ref, String refName)
   {
      return Preconditions.checkNotNull(ref, "%s must not be null", refName);
   }

   //region keywords

   /**
    * Sets keywords to search for.
    *
    * @param keywords A collection of keywords
    * @return the current <code>SearchQueryBuilder</code>
    * @throws NullPointerException if the collection of keywords is <code>null</code>
    */
   public SearchQueryBuilder keywords(Collection<String> keywords)
   {
      checkNotNull(keywords, "keywords");

      if (keywords.isEmpty()) {
         this.query.keywords = DEFAULT_KEYWORDS;
         return this;
      }

      Joiner joiner = Joiner.on(" ");
      this.query.keywords = joiner.join(keywords);
      return this;
   }

   /**
    * Sets keywords to search for.
    *
    * @param keywords An array or varargs of keywords
    * @return the current <code>SearchQueryBuilder</code>
    */
   public SearchQueryBuilder keywords(String... keywords)
   {
      return keywords(asList(keywords));
   }
   //endregion

   //region categories

   /**
    * Sets the categories of wallpapers to include in search result.
    *
    * @param categories A collection of categories
    * @return the current <code>SearchQueryBuilder</code>
    * @throws NullPointerException if the collection of categories is <code>null</code>
    * @see Category
    */
   public SearchQueryBuilder categories(Collection<Category> categories)
   {
      checkNotNull(categories, "categories");

      if (categories.isEmpty()) {
         this.query.categories = DEFAULT_CATEGORIES;
         return this;
      }

      this.query.categories = EnumSet.copyOf(categories);
      return this;
   }

   /**
    * Sets the categories of wallpapers to include in search result.
    *
    * @param categories An array or varargs of categories
    * @return the current <code>SearchQueryBuilder</code>
    * @see Category
    */
   public SearchQueryBuilder categories(Category... categories)
   {
      return categories(ImmutableSet.copyOf(categories));
   }
   //endregion

   //region purity

   /**
    * Set the purity of wallpapers to include in search result.
    *
    * @param purity Collection of purities.
    * @return the current <code>SearchQueryBuilder</code>
    * @throws NullPointerException if the collection of purities is <code>null</code>
    * @see Purity
    */
   public SearchQueryBuilder purity(Collection<Purity> purity)
   {
      checkNotNull(purity, "purity");

      if (purity.isEmpty()) {
         this.query.purity = DEFAULT_PURITY;
         return this;
      }

      this.query.purity = EnumSet.copyOf(purity);
      return this;
   }

   /**
    * Set the purity of wallpapers to include in search result.
    *
    * @param purity An array or varargs of purities.
    * @return the current <code>SearchQueryBuilder</code>
    * @see Purity
    */
   public SearchQueryBuilder purity(Purity... purity)
   {
      return purity(ImmutableSet.copyOf(purity));
   }
   //endregion

   /**
    * Sets the property that wallpapers in search result will be sorted by.
    *
    * @param sorting Sorting property.
    * @return the current <code>SearchQueryBuilder</code>
    * @throws NullPointerException if sorting is <code>null</code>
    * @see Sorting
    */
   public SearchQueryBuilder sorting(Sorting sorting)
   {
      checkNotNull(sorting, "sorting");

      this.query.sorting = sorting;
      return this;
   }

   /**
    * Sets the order that wallpapers in search result will be returned.
    *
    * @param order Order. Could be either <code>Order.DESC</code>, or <code>Order.ASC</code>.
    * @return the current <code>SearchQueryBuilder</code>
    * @throws NullPointerException if order is <code>null</code>
    * @see Order
    */
   public SearchQueryBuilder order(Order order)
   {
      checkNotNull(order, "order");

      this.query.order = order;
      return this;
   }

   /**
    * Sets the range of the toplist. In order for this to have effect, sorting must be set to {@link Sorting#TOPLIST}
    *
    * @param range The range of the toplist.
    * @return the current <code>SearchQueryBuilder</code>
    * @throws NullPointerException if range is <code>null</code>
    * @see ToplistRange
    */
   public SearchQueryBuilder toplistRange(ToplistRange range)
   {
      checkNotNull(range, "range");

      this.query.toplistRange = range;
      return this;
   }

   //region resolutions

   /**
    * Sets the resolutions of wallpapers to include in search result.
    *
    * @param resolutions A collection of <code>Resolution</code>s
    * @return the current <code>SearchQueryBuilder</code>
    * @throws NullPointerException if the collection of <code>Resolution</code>s is <code>null</code>
    * @see Resolution
    */
   public SearchQueryBuilder resolutions(Collection<Resolution> resolutions)
   {
      checkNotNull(resolutions, "resolutions");

      if (resolutions.isEmpty()) {
         this.query.resolutions = DEFAULT_RESOLUTIONS;
         return this;
      }

      this.query.resolutions = ImmutableSet.copyOf(resolutions);
      return this;
   }

   /**
    * Sets the resolutions of wallpapers to include in search result.
    *
    * @param resolutions An array or varargs of <code>Resolution</code>s
    * @return the current <code>SearchQueryBuilder</code>
    * @see Resolution
    */
   public SearchQueryBuilder resolutions(Resolution... resolutions)
   {
      return resolutions(ImmutableSet.copyOf(resolutions));
   }
   //endregion

   //region ratios

   /**
    * Sets the ratios of wallpapers to include in search result.
    *
    * @param ratios A collection of <code>Ratio</code>s
    * @return the current <code>SearchQueryBuilder</code>
    * @throws NullPointerException if the collection of <code>Ratio</code>s is <code>null</code>
    * @see Ratio
    */
   public SearchQueryBuilder ratios(Collection<Ratio> ratios)
   {
      checkNotNull(ratios, "ratios");

      if (ratios.isEmpty()) {
         this.query.ratios = DEFAULT_RATIOS;
         return this;
      }

      this.query.ratios = ImmutableSet.copyOf(ratios);
      return this;
   }

   /**
    * Sets the ratios of wallpapers to include in search result.
    *
    * @param ratios A collection of <code>Ratio</code>s
    * @return the current <code>SearchQueryBuilder</code>
    * @see Ratio
    */
   public SearchQueryBuilder ratios(Ratio... ratios)
   {
      return ratios(ImmutableSet.copyOf(ratios));
   }
   //endregion

   /**
    * Sets how many pages of wallpapers to fetch.
    * One page could contain 24 (default), 32 or 64 wallpapers,
    * depending on the preference of the logged in user, set in Wallhaven's settings page.
    *
    * @param pages The number of pages to fetch.
    * @return the current <code>SearchQueryBuilder</code>
    * @throws IllegalArgumentException if <code>pages</code> is not greater than zero
    */
   public SearchQueryBuilder pages(long pages)
   {
      Preconditions.checkArgument(pages > 0, "pages must be greater than zero");

      this.query.pages = pages;
      return this;
   }

   /**
    * Builds a <code>SearchQuery</code> object.
    *
    * @return a <code>SearchQuery</code> object.
    */
   public SearchQuery build()
   {
      return query;
   }
}


<|start_filename|>src/main/java/com/ivkos/wallhaven4j/models/misc/Color.java<|end_filename|>
package com.ivkos.wallhaven4j.models.misc;

import java.util.Objects;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;

public class Color
{
   private final int red;
   private final int green;
   private final int blue;

   /**
    * Constructs a new Color object from the specified RGB values
    *
    * @param red   red value
    * @param green green value
    * @param blue  blue value
    * @throws IllegalArgumentException if any of the color values are out of the valid ranges
    */
   public Color(int red, int green, int blue)
   {
      checkArgument(red >= 0 && red <= 255, "red must be between 0 and 255");
      checkArgument(green >= 0 && green <= 255, "green must be between 0 and 255");
      checkArgument(blue >= 0 && blue <= 255, "blue must be between 0 and 255");

      this.red = red;
      this.green = green;
      this.blue = blue;
   }

   /**
    * Constructs a new Color object from the specified RGB hex string
    *
    * @param hex hex string
    * @throws NullPointerException     if the specified string is null
    * @throws IllegalArgumentException if the hex string is invalid
    */
   public Color(String hex)
   {
      checkNotNull(hex, "hex must not be null");
      checkArgument(hex.matches("#?[0-9A-Fa-f]{6}"), "invalid hex string (%s)", hex);

      int offset = hex.startsWith("#") ? 1 : 0;

      red = Integer.parseInt(hex.substring(offset, 2 + offset), 16);
      green = Integer.parseInt(hex.substring(2 + offset, 4 + offset), 16);
      blue = Integer.parseInt(hex.substring(4 + offset, 6 + offset), 16);
   }

   /**
    * Returns a hex string (#rrggbb) representing the color
    *
    * @return hex string representing the color
    */
   public String toHex()
   {
      return String.format("#%02X%02X%02X", red, green, blue);
   }

   /**
    * Returns an <code>rgb(R, G, B)</code> string representing the color
    *
    * @return rgb(...) string representing the color
    */
   public String toRgb()
   {
      return String.format("rgb(%d,%d,%d)", red, green, blue);
   }

   public int getRed()
   {
      return red;
   }

   public int getGreen()
   {
      return green;
   }

   public int getBlue()
   {
      return blue;
   }

   /**
    * Returns the hex string (#rrggbb) representing the color
    *
    * @return hex string representing the color
    */
   @Override
   public String toString()
   {
      return toHex();
   }

   @Override
   public boolean equals(Object o)
   {
      if (this == o) return true;
      if (!(o instanceof Color)) return false;
      Color color = (Color) o;
      return red == color.red &&
            green == color.green &&
            blue == color.blue;
   }

   @Override
   public int hashCode()
   {
      return Objects.hash(red, green, blue);
   }
}


<|start_filename|>src/test/java/com/ivkos/wallhaven4j/models/user/UserTest.java<|end_filename|>
package com.ivkos.wallhaven4j.models.user;

import com.ivkos.wallhaven4j.AbstractWallhavenTest;
import org.junit.BeforeClass;
import org.junit.Test;

import static org.junit.Assert.*;

public class UserTest extends AbstractWallhavenTest
{
   private static User gandalf;
   private static User ivkos;

   @BeforeClass
   public static void setUpUserTest() throws Exception
   {
      gandalf = getWallhaven().getUser("Gandalf");
      ivkos = getWallhaven().getUser("ivkos");
   }

   @Test
   public void getUsername() throws Exception
   {
      assertEquals("ivkos", ivkos.getId());
      assertEquals("ivkos", ivkos.getUsername());
   }

   @Test
   public void getGroupName() throws Exception
   {
      assertEquals("Developer", gandalf.getGroupName());
      assertEquals("User", ivkos.getGroupName());
   }

   @Test
   public void getDescription() throws Exception
   {
      assertNotNull(gandalf.getDescription());
      assertNull(ivkos.getDescription());
   }

   @Test
   public void getLastActiveTime() throws Exception
   {
      assertNotNull(ivkos.getLastActiveTime());
   }

   @Test
   public void getDateCreated() throws Exception
   {
      assertNotNull(gandalf.getDateCreated());
      assertNotNull(ivkos.getDateCreated());
   }

   @Test
   public void getUploadedWallpapersCount() throws Exception
   {
      assertTrue(gandalf.getUploadedWallpapersCount() > 0);
   }

   @Test
   public void getFavoriteWallpapersCount() throws Exception
   {
      assertTrue(gandalf.getFavoriteWallpapersCount() > 0);
   }

   @Test
   public void getTaggedWallpapersCount() throws Exception
   {
      assertTrue(gandalf.getTaggedWallpapersCount() > 0);
   }

   @Test
   public void getFlaggedWallpapersCount() throws Exception
   {
      assertTrue(gandalf.getFlaggedWallpapersCount() > 0);
   }

   @Test
   public void getSubscribersCount() throws Exception
   {
      assertTrue(gandalf.getSubscribersCount() > 0);
   }

   @Test
   public void getProfileViewsCount() throws Exception
   {
      assertTrue(gandalf.getProfileViewsCount() > 0);
   }

   @Test
   public void getProfileCommentsCount() throws Exception
   {
      assertTrue(gandalf.getProfileCommentsCount() > 0);
   }

   @Test
   public void getForumPostsCount() throws Exception
   {
      assertTrue(gandalf.getForumPostsCount() > 0);
   }

   @Test
   public void getCollections() throws Exception
   {
      assertFalse(gandalf.getCollections().isEmpty());
   }
}


<|start_filename|>src/test/java/com/ivkos/wallhaven4j/models/misc/ColorTest.java<|end_filename|>
package com.ivkos.wallhaven4j.models.misc;

import org.junit.Assert;
import org.junit.Test;

import static org.junit.Assert.assertEquals;

public class ColorTest
{
   private Color red = new Color(255, 0, 0);
   private Color abbeef = new Color("#abbeef");

   @Test
   public void getRed() throws Exception
   {
      assertEquals(255, red.getRed());
      assertEquals(171, abbeef.getRed());
   }

   @Test
   public void getGreen() throws Exception
   {
      assertEquals(0, red.getGreen());
      assertEquals(190, abbeef.getGreen());
   }

   @Test
   public void getBlue() throws Exception
   {
      assertEquals(0, red.getBlue());
      assertEquals(239, abbeef.getBlue());
   }

   @Test
   public void toHex() throws Exception
   {
      assertEquals("#FF0000", red.toHex());
      assertEquals("#ABBEEF", abbeef.toHex());
   }

   @Test
   public void toRgb() throws Exception
   {
      assertEquals("rgb(255,0,0)", red.toRgb());
      assertEquals("rgb(171,190,239)", abbeef.toRgb());
   }

   @Test
   public void toAString() throws Exception
   {
      assertEquals(red.toHex(), red.toString());
   }

   @Test
   public void thatEquals() throws Exception
   {
      Color red1 = new Color(255, 0, 0);
      Color red2 = new Color("FF0000");

      Assert.assertTrue(red1.equals(red2));
   }

   @Test
   public void thatHashCodeIsEqual() throws Exception
   {
      Color red1 = new Color(255, 0, 0);
      Color red2 = new Color("FF0000");

      Assert.assertTrue(red1.hashCode() == red2.hashCode());
   }
}


<|start_filename|>src/main/java/com/ivkos/wallhaven4j/util/htmlparser/TimeElementParser.java<|end_filename|>
package com.ivkos.wallhaven4j.util.htmlparser;

import com.ivkos.wallhaven4j.util.exceptions.ParseException;
import org.joda.time.DateTime;

public abstract class TimeElementParser
{
   public static DateTime parse(HtmlElement htmlElement)
   {
      if (!htmlElement.getTagName().equals("time")) {
         throw new IllegalArgumentException("htmlElement must be a 'time' element");
      }

      String datetime = htmlElement.getAttribute("datetime");

      if (datetime.isEmpty()) throw new ParseException("datetime is empty");

      DateTime result;
      try {
         result = DateTime.parse(datetime);
      } catch (IllegalArgumentException e) {
         throw new ParseException("Could not parse time element", e);
      }

      return result;
   }
}


<|start_filename|>src/test/java/com/ivkos/wallhaven4j/WallhavenTest.java<|end_filename|>
package com.ivkos.wallhaven4j;

import com.ivkos.wallhaven4j.models.misc.Ratio;
import com.ivkos.wallhaven4j.models.misc.Resolution;
import com.ivkos.wallhaven4j.models.misc.enums.Category;
import com.ivkos.wallhaven4j.models.misc.enums.Order;
import com.ivkos.wallhaven4j.models.misc.enums.Sorting;
import com.ivkos.wallhaven4j.models.user.User;
import com.ivkos.wallhaven4j.models.wallpaper.Wallpaper;
import com.ivkos.wallhaven4j.util.exceptions.LoginException;
import com.ivkos.wallhaven4j.util.exceptions.ResourceNotAccessibleException;
import com.ivkos.wallhaven4j.util.exceptions.ResourceNotFoundException;
import com.ivkos.wallhaven4j.util.searchquery.SearchQuery;
import com.ivkos.wallhaven4j.util.searchquery.SearchQueryBuilder;
import org.junit.Test;

import java.io.File;
import java.util.List;

import static org.junit.Assert.*;

public class WallhavenTest extends AbstractWallhavenTest
{
   @Test(expected = LoginException.class)
   public void loginWithIncorrectCredentials()
   {
      new Wallhaven(getWallhavenUsername(), "thewrongpassword");
   }

   @Test(expected = ResourceNotFoundException.class)
   public void getNonExistentTag() throws Exception
   {
      getWallhaven().getTag(999999999);
   }

   @Test(expected = ResourceNotFoundException.class)
   public void getNonExistentUser() throws Exception
   {
      getWallhaven().getUser("nonexistentuser");
   }

   @Test(expected = ResourceNotFoundException.class)
   public void getNonExistentWallpaperCollection() throws Exception
   {
      getWallhaven().getWallpaperCollection("ivkos", 999999999);
   }

   @Test(expected = ResourceNotFoundException.class)
   public void getNonExistentWallpaper() throws Exception
   {
      getWallhaven().getWallpaper(1);
   }

   @Test(expected = ResourceNotAccessibleException.class)
   public void getNsfwWallpaperWhenNotLoggedIn() throws Exception
   {
      getWallhaven().getWallpaper(8273);
   }

   @Test
   public void getCurrentUser() throws Exception
   {
      assertNull(getWallhaven().getCurrentUser());

      User currentUser = getWallhaven(true).getCurrentUser();
      assertNotNull(currentUser);
      assertEquals(getWallhavenUsername(), currentUser.getUsername());
   }

   @Test(expected = IllegalArgumentException.class)
   public void getTagWithZeroId() throws Exception
   {
      getWallhaven().getTag(0);
   }

   @Test(expected = IllegalArgumentException.class)
   public void getTagWithNegativeId() throws Exception
   {
      getWallhaven().getTag(-1);
   }

   @Test(expected = IllegalArgumentException.class)
   public void getUserWithNullUsername() throws Exception
   {
      getWallhaven().getUser(null);
   }

   @Test(expected = IllegalArgumentException.class)
   public void getUserWithEmptyUsername() throws Exception
   {
      getWallhaven().getUser("");
   }

   @Test(expected = IllegalArgumentException.class)
   public void getWallpaperWithZeroId() throws Exception
   {
      getWallhaven().getWallpaper(0);
   }

   @Test(expected = IllegalArgumentException.class)
   public void getWallpaperWithNegativeId() throws Exception
   {
      getWallhaven().getWallpaper(-1);
   }

   @Test(expected = IllegalArgumentException.class)
   public void getWallpaperCollectionWithNullUsername() throws Exception
   {
      getWallhaven().getWallpaperCollection(null, 0);
   }

   @Test(expected = IllegalArgumentException.class)
   public void getWallpaperCollectionWithEmptyUsername() throws Exception
   {
      getWallhaven().getWallpaperCollection("", 0);
   }

   @Test(expected = IllegalArgumentException.class)
   public void getWallpaperCollectionWithZeroId() throws Exception
   {
      getWallhaven().getWallpaperCollection("someone", 0);
   }

   @Test(expected = IllegalArgumentException.class)
   public void getWallpaperCollectionWithNegativeId() throws Exception
   {
      getWallhaven().getWallpaperCollection("someone", -1);
   }

   @Test
   public void searchWithQuery() throws Exception
   {
      List<Wallpaper> result1 = getWallhaven().search(new SearchQueryBuilder()
            .keywords("cars")
            .resolutions(new Resolution(1920, 1080))
            .sorting(Sorting.VIEWS)
            .order(Order.DESC)
            .build()
      );
      assertFalse(result1.isEmpty());

      List<Wallpaper> result2 = getWallhaven(true).search(new SearchQueryBuilder()
            .sorting(Sorting.FAVORITES)
            .order(Order.DESC)
            .build()
      );
      assertFalse(result2.isEmpty());
   }

   @Test
   public void searchLatest() throws Exception
   {
      List<Wallpaper> result = getWallhaven().search(new SearchQueryBuilder()
            .sorting(Sorting.DATE_ADDED)
            .order(Order.DESC)
            .build());

      assertFalse(result.isEmpty());
   }

   @Test(expected = NullPointerException.class)
   public void searchWithNullQuery() throws Exception
   {
      getWallhaven().search(null);
   }

   @Test
   public void searchPageWithQuery() throws Exception
   {
      SearchQuery query = new SearchQueryBuilder()
            .keywords("minimal")
            .categories(Category.GENERAL)
            .ratios(new Ratio(9, 16))
            .build();

      List<Wallpaper> page2 = getWallhaven().search(query, 2);

      assertFalse(page2.isEmpty());
   }

   @Test(expected = NullPointerException.class)
   public void searchPageWithNullQuery() throws Exception
   {
      getWallhaven().search(null, 1);
   }

   @Test(expected = IllegalArgumentException.class)
   public void searchPageWithZeroPage() throws Exception
   {
      getWallhaven().search(new SearchQueryBuilder().build(), 0);
   }

   @Test(expected = IllegalArgumentException.class)
   public void searchPageWithNegativePage() throws Exception
   {
      getWallhaven().search(new SearchQueryBuilder().build(), -1);
   }

   @Test
   public void testCookies()
   {
      File cookiesFile = new File("testcookies.json");

      Wallhaven wh1 = new Wallhaven(
            getWallhavenUsername(),
            getWallhavenPassword(),
            cookiesFile
      );

      assertNotNull(wh1.getCurrentUser());

      // will reuse the session cookies from the file
      Wallhaven wh2 = new Wallhaven(
            getWallhavenUsername(),
            "doesntmatter",
            cookiesFile
      );

      assertNotNull(wh2.getCurrentUser());

      assertEquals(getWallhavenUsername(), wh2.getCurrentUser().getId());
      assertTrue(cookiesFile.delete());
   }
}


<|start_filename|>src/test/java/com/ivkos/wallhaven4j/AbstractWallhavenTest.java<|end_filename|>
package com.ivkos.wallhaven4j;

import org.junit.BeforeClass;

import static org.junit.Assert.assertTrue;

public abstract class AbstractWallhavenTest
{
   private static Wallhaven wallhavenAnonymous;
   private static Wallhaven wallhavenLoggedIn;

   protected static String getWallhavenUsername()
   {
      return System.getenv("WALLHAVEN_USERNAME");
   }

   protected static String getWallhavenPassword()
   {
      return System.getenv("WALLHAVEN_PASSWORD");
   }

   @BeforeClass
   public static final void setUpBase()
   {
      assertTrue("Environment variable WALLHAVEN_USERNAME is not set",
            getWallhavenUsername() != null && !getWallhavenUsername().isEmpty());

      assertTrue("Environment variable WALLHAVEN_PASSWORD is not set",
            getWallhavenPassword() != null && !getWallhavenPassword().isEmpty());

      wallhavenAnonymous = new Wallhaven();
      wallhavenLoggedIn = new Wallhaven(
            getWallhavenUsername(),
            getWallhavenPassword()
      );
   }

   protected static Wallhaven getWallhaven(boolean loggedIn)
   {
      return loggedIn ? wallhavenLoggedIn : wallhavenAnonymous;
   }

   protected static Wallhaven getWallhaven()
   {
      return getWallhaven(false);
   }
}


<|start_filename|>src/main/java/com/ivkos/wallhaven4j/util/exceptions/ResourceNotAccessibleException.java<|end_filename|>
package com.ivkos.wallhaven4j.util.exceptions;

/**
 * Thrown to indicate that the requested resource cannot be accessed due to privacy or purity restrictions.
 */
public class ResourceNotAccessibleException extends WallhavenException
{
   public ResourceNotAccessibleException()
   {
   }

   public ResourceNotAccessibleException(String message)
   {
      super(message);
   }

   public ResourceNotAccessibleException(String message, Throwable cause)
   {
      super(message, cause);
   }

   public ResourceNotAccessibleException(Throwable cause)
   {
      super(cause);
   }

   public ResourceNotAccessibleException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace)
   {
      super(message, cause, enableSuppression, writableStackTrace);
   }
}


<|start_filename|>src/main/java/com/ivkos/wallhaven4j/util/htmlparser/HtmlParser.java<|end_filename|>
package com.ivkos.wallhaven4j.util.htmlparser;

public interface HtmlParser
{
   HtmlElement parse(String html);

   HtmlElement parse(String html, String baseUrl);
}


<|start_filename|>src/test/java/com/ivkos/wallhaven4j/models/misc/enums/SortingTest.java<|end_filename|>
package com.ivkos.wallhaven4j.models.misc.enums;

import org.junit.Test;

import static com.ivkos.wallhaven4j.models.misc.enums.Sorting.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;

public class SortingTest
{
   @Test
   public void allConstantsArePresent() throws Exception
   {
      assertNotNull(valueOf("RELEVANCE"));
      assertNotNull(valueOf("RANDOM"));
      assertNotNull(valueOf("DATE_ADDED"));
      assertNotNull(valueOf("VIEWS"));
      assertNotNull(valueOf("FAVORITES"));
      assertNotNull(valueOf("TOPLIST"));
   }

   @Test
   public void toStringIsCorrect() throws Exception
   {
      assertEquals("relevance", RELEVANCE.toString());
      assertEquals("date_added", DATE_ADDED.toString());
   }
}


<|start_filename|>src/main/java/com/ivkos/wallhaven4j/models/tagcategory/TagCategory.java<|end_filename|>
package com.ivkos.wallhaven4j.models.tagcategory;

import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject;
import com.ivkos.wallhaven4j.models.AbstractResource;
import com.ivkos.wallhaven4j.util.UrlPrefixes;
import com.ivkos.wallhaven4j.util.WallhavenSession;

public class TagCategory extends AbstractResource<Long>
{
   private String name;
   private TagCategory parentCategory;

   @AssistedInject
   TagCategory(WallhavenSession session, @Assisted boolean preloadDom, @Assisted long id)
   {
      super(session, preloadDom, id);
   }

   @AssistedInject
   TagCategory(WallhavenSession session, @Assisted boolean preloadDom, @Assisted long id, @Assisted String name)
   {
      super(session, preloadDom, id);
      this.name = name;
   }

   @AssistedInject
   TagCategory(WallhavenSession session, @Assisted boolean preloadDom, @Assisted long id, @Assisted String name, @Assisted TagCategory parentCategory)
   {
      super(session, preloadDom, id);
      this.name = name;
      this.parentCategory = parentCategory;
   }

   /**
    * Returns the name of the tag category
    *
    * @return the name of the tag category
    */
   public String getName()
   {
      return name;
   }

   /**
    * Returns the parent tag category or <code>null</code> if this is a root tag category (i.e. has no parents)
    *
    * @return the parent tag category or <code>null</code> if this is a root tag category (i.e. has no parents)
    */
   public TagCategory getParentCategory()
   {
      return parentCategory;
   }

   /**
    * Returns the name of the tag category
    *
    * @return the name of the tag category
    */
   @Override
   public String toString()
   {
      return name;
   }

   @Override
   public String getUrl()
   {
      return UrlPrefixes.URL_TAGS + "/" + id;
   }
}


<|start_filename|>src/test/java/com/ivkos/wallhaven4j/models/misc/enums/OrderTest.java<|end_filename|>
package com.ivkos.wallhaven4j.models.misc.enums;

import org.junit.Test;

import static com.ivkos.wallhaven4j.models.misc.enums.Order.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;

public class OrderTest
{
   @Test
   public void allConstantsArePresent() throws Exception
   {
      assertNotNull(valueOf("ASC"));
      assertNotNull(valueOf("DESC"));
   }

   @Test
   public void toStringIsCorrect() throws Exception
   {
      assertEquals("asc", ASC.toString());
      assertEquals("desc", DESC.toString());
   }
}


<|start_filename|>src/test/java/com/ivkos/wallhaven4j/util/htmlparser/TimeElementParserTest.java<|end_filename|>
package com.ivkos.wallhaven4j.util.htmlparser;

import com.ivkos.wallhaven4j.util.exceptions.ParseException;
import org.junit.Test;

import java.util.List;
import java.util.Map;

public class TimeElementParserTest
{
   @Test(expected = IllegalArgumentException.class)
   public void parseWrongElement() throws Exception
   {
      HtmlElement div = new HtmlElement()
      {
         @Override
         public List<HtmlElement> find(String cssQuery)
         {
            return null;
         }

         @Override
         public HtmlElement findFirst(String cssQuery)
         {
            return null;
         }

         @Override
         public HtmlElement findLast(String cssQuery)
         {
            return null;
         }

         @Override
         public HtmlElement findElementById(String id)
         {
            return null;
         }

         @Override
         public HtmlElement getNextElementSibling()
         {
            return null;
         }

         @Override
         public String getText()
         {
            return null;
         }

         @Override
         public String getAttribute(String attributeName)
         {
            return null;
         }

         @Override
         public boolean hasClass(String className)
         {
            return false;
         }

         @Override
         public Map<String, String> getDataAttributes()
         {
            return null;
         }

         @Override
         public String getValue()
         {
            return null;
         }

         @Override
         public String getTagName()
         {
            return "div";
         }
      };

      TimeElementParser.parse(div);
   }

   @Test(expected = ParseException.class)
   public void parseEmptyDateTime() throws Exception
   {
      HtmlElement div = new HtmlElement()
      {
         @Override
         public List<HtmlElement> find(String cssQuery)
         {
            return null;
         }

         @Override
         public HtmlElement findFirst(String cssQuery)
         {
            return null;
         }

         @Override
         public HtmlElement findLast(String cssQuery)
         {
            return null;
         }

         @Override
         public HtmlElement findElementById(String id)
         {
            return null;
         }

         @Override
         public HtmlElement getNextElementSibling()
         {
            return null;
         }

         @Override
         public String getText()
         {
            return null;
         }

         @Override
         public String getAttribute(String attributeName)
         {
            return "";
         }

         @Override
         public boolean hasClass(String className)
         {
            return false;
         }

         @Override
         public Map<String, String> getDataAttributes()
         {
            return null;
         }

         @Override
         public String getValue()
         {
            return null;
         }

         @Override
         public String getTagName()
         {
            return "time";
         }
      };

      TimeElementParser.parse(div);
   }

   @Test(expected = ParseException.class)
   public void parseIllegalDatetime() throws Exception
   {
      HtmlElement div = new HtmlElement()
      {
         @Override
         public List<HtmlElement> find(String cssQuery)
         {
            return null;
         }

         @Override
         public HtmlElement findFirst(String cssQuery)
         {
            return null;
         }

         @Override
         public HtmlElement findLast(String cssQuery)
         {
            return null;
         }

         @Override
         public HtmlElement findElementById(String id)
         {
            return null;
         }

         @Override
         public HtmlElement getNextElementSibling()
         {
            return null;
         }

         @Override
         public String getText()
         {
            return null;
         }

         @Override
         public String getAttribute(String attributeName)
         {
            return "a1b2c3";
         }

         @Override
         public boolean hasClass(String className)
         {
            return false;
         }

         @Override
         public Map<String, String> getDataAttributes()
         {
            return null;
         }

         @Override
         public String getValue()
         {
            return null;
         }

         @Override
         public String getTagName()
         {
            return "time";
         }
      };

      TimeElementParser.parse(div);
   }
}


<|start_filename|>src/main/java/com/ivkos/wallhaven4j/models/AbstractResource.java<|end_filename|>
package com.ivkos.wallhaven4j.models;

import com.ivkos.wallhaven4j.util.ResourceFieldGetter;
import com.ivkos.wallhaven4j.util.WallhavenSession;
import com.ivkos.wallhaven4j.util.exceptions.WallhavenException;
import com.ivkos.wallhaven4j.util.htmlparser.HtmlElement;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Objects;

import static com.google.common.base.Preconditions.checkNotNull;

public abstract class AbstractResource<T>
{
   protected final T id;

   private final WallhavenSession session;
   private HtmlElement dom;

   protected AbstractResource(WallhavenSession session, boolean preloadDom, T id)
   {
      this.session = session;
      this.id = checkNotNull(id, "id must not be null");

      if (preloadDom) {
         getDom();
      }
   }

   protected final HtmlElement getDom()
   {
      if (this.dom != null) {
         return this.dom;
      }

      String html = getSession().getHttpClient().get(getUrl()).getBody();
      this.dom = getSession().getHtmlParser().parse(html, getUrl());

      return this.dom;
   }

   protected final void populateFields()
   {
      for (Method method : getClass().getMethods()) {
         boolean isFieldGetter = method.isAnnotationPresent(ResourceFieldGetter.class);

         if (!isFieldGetter) continue;

         try {
            method.invoke(this);
         } catch (IllegalAccessException e) {
            throw new WallhavenException("Could not populate fields of resource (" + this.toString(true) + ")", e);
         } catch (InvocationTargetException e) {
            throw new WallhavenException("Could not populate fields of resource (" + this.toString(true) + ")",
                  e.getCause());
         }
      }
   }

   /**
    * Returns the ID of this resource
    *
    * @return the ID of this resource
    */
   public final T getId()
   {
      return id;
   }

   protected final WallhavenSession getSession()
   {
      return session;
   }

   /**
    * Returns a browseable URL of this resource
    *
    * @return the URL of this resource
    */
   public abstract String getUrl();

   //region equals, hashCode, toString
   @Override
   public boolean equals(Object o)
   {
      if (this == o) return true;
      if (!(o instanceof AbstractResource)) return false;
      if (this.getClass() != o.getClass()) return false;

      AbstractResource<?> other = (AbstractResource<?>) o;

      return Objects.equals(this.getId(), other.getId());
   }

   @Override
   public int hashCode()
   {
      return Objects.hash(this.getClass(), this.getId());
   }

   /**
    * Returns a string representation of the ID of this resource
    *
    * @return a string representation of the ID of this resource
    */
   @Override
   public String toString()
   {
      return this.toString(false);
   }

   /**
    * Returns a string description of this resource including its resource type and ID, e.g. "Wallpaper 42"
    *
    * @param withResourceType whether to include the resource type in the description
    * @return string description of this resource
    */
   public String toString(boolean withResourceType)
   {
      String id = this.getId().toString();

      return withResourceType
            ? String.format("%s %s", this.getClass().getSimpleName(), id)
            : id;
   }
   //endregion
}


<|start_filename|>src/main/java/com/ivkos/wallhaven4j/models/misc/enums/Sorting.java<|end_filename|>
package com.ivkos.wallhaven4j.models.misc.enums;

public enum Sorting
{
   RELEVANCE, RANDOM, DATE_ADDED, VIEWS, FAVORITES, TOPLIST;

   @Override
   public String toString()
   {
      return name().toLowerCase();
   }
}


<|start_filename|>src/main/java/com/ivkos/wallhaven4j/util/pagecrawler/thumbnailpage/ThumbnailTransformer.java<|end_filename|>
package com.ivkos.wallhaven4j.util.pagecrawler.thumbnailpage;

import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.ivkos.wallhaven4j.models.ResourceFactoryFactory;
import com.ivkos.wallhaven4j.models.misc.Resolution;
import com.ivkos.wallhaven4j.models.misc.enums.Category;
import com.ivkos.wallhaven4j.models.misc.enums.Purity;
import com.ivkos.wallhaven4j.models.wallpaper.Wallpaper;
import com.ivkos.wallhaven4j.models.wallpaper.WallpaperFactory;
import com.ivkos.wallhaven4j.util.exceptions.ParseException;
import com.ivkos.wallhaven4j.util.htmlparser.HtmlElement;
import com.ivkos.wallhaven4j.util.htmlparser.OptionalSelector;

import static com.ivkos.wallhaven4j.models.misc.enums.Category.*;
import static com.ivkos.wallhaven4j.models.misc.enums.Purity.*;
import static com.ivkos.wallhaven4j.util.exceptions.DescriptiveParseExceptionSupplier.forResource;
import static java.lang.Long.parseLong;

@Singleton
class ThumbnailTransformer
{
   private final ResourceFactoryFactory rff;

   @Inject
   ThumbnailTransformer(ResourceFactoryFactory rff)
   {
      this.rff = rff;
   }

   public Wallpaper transform(HtmlElement figureElement)
   {
      String idText = figureElement.getDataAttributes().get("wallpaper-id");

      //region id
      long id;
      try {
         id = parseLong(idText);
      } catch (NumberFormatException e) {
         throw new ParseException("Could not parse wallpaper id of wallpaper thumbnail");
      }
      //endregion

      //region category
      Category category = figureElement.hasClass("thumb-general") ? GENERAL
            : figureElement.hasClass("thumb-anime") ? ANIME
            : figureElement.hasClass("thumb-people") ? PEOPLE
            : null;

      if (category == null) throw new ParseException("Could not parse category of wallpaper thumbnail");
      //endregion

      //region purity
      Purity purity = figureElement.hasClass("thumb-sfw") ? SFW
            : figureElement.hasClass("thumb-sketchy") ? SKETCHY
            : figureElement.hasClass("thumb-nsfw") ? NSFW
            : null;

      if (purity == null) throw new ParseException("Could not parse purity of wallpaper thumbnail");
      //endregion

      //region resolution
      HtmlElement resolutionElement = OptionalSelector.of(figureElement, "span.wall-res")
            .orElseThrow(forResource(Wallpaper.class, "resolution in thumbnail"));

      String resolutionText = resolutionElement.getText().replace(" ", "");
      Resolution resolution = Resolution.parse(resolutionText);
      //endregion

      //region favorites count
      String favoritesCountText = OptionalSelector.of(figureElement, "a.wall-favs, span.wall-favs")
            .orElseThrow(forResource(Wallpaper.class, "favorites count in thumbnail"))
            .getText()
            .trim()
            .replace(",", "");

      long favoritesCount = parseLong(favoritesCountText);
      //endregion

      WallpaperFactory wallpaperFactory = rff.getFactoryFor(Wallpaper.class);
      wallpaperFactory.create(false, id, category, purity, resolution, favoritesCount);

      return wallpaperFactory.create(false, id);
   }
}


<|start_filename|>src/test/java/com/ivkos/wallhaven4j/models/misc/enums/util/BitfieldSumTest.java<|end_filename|>
package com.ivkos.wallhaven4j.models.misc.enums.util;

import org.junit.Test;

import static com.ivkos.wallhaven4j.models.misc.enums.Purity.*;
import static org.junit.Assert.assertEquals;

public class BitfieldSumTest
{
   @Test
   public void bitfieldSum() throws Exception
   {
      assertEquals(0, BitfieldSum.of());

      assertEquals(SFW.getBitfieldValue(),
            BitfieldSum.of(SFW));

      assertEquals(SFW.getBitfieldValue() + NSFW.getBitfieldValue(),
            BitfieldSum.of(SFW, NSFW));

      assertEquals(SFW.getBitfieldValue() + SKETCHY.getBitfieldValue() + NSFW.getBitfieldValue(),
            BitfieldSum.of(SFW, SKETCHY, NSFW));
   }

   @Test
   public void sumOfDuplicates() throws Exception
   {
      assertEquals(SFW.getBitfieldValue(), BitfieldSum.of(SFW, SFW));
   }

   @Test
   public void asBinaryString() throws Exception
   {
      assertEquals("000", BitfieldSum.asThreeBitBinaryString());
      assertEquals("001", BitfieldSum.asThreeBitBinaryString(NSFW));
      assertEquals("101", BitfieldSum.asThreeBitBinaryString(SFW, NSFW));
   }
}


<|start_filename|>src/main/java/com/ivkos/wallhaven4j/util/pagecrawler/PageCrawler.java<|end_filename|>
package com.ivkos.wallhaven4j.util.pagecrawler;

import com.ivkos.wallhaven4j.models.AbstractResource;

import java.util.List;
import java.util.Map;

public interface PageCrawler<R extends AbstractResource<?>>
{
   String getUrl();

   List<R> getPage(long pageNumber, Map<String, String> queryParams);

   List<R> getPageSequence(long pageCount, Map<String, String> queryParams);
}


<|start_filename|>src/main/java/com/ivkos/wallhaven4j/models/misc/Ratio.java<|end_filename|>
package com.ivkos.wallhaven4j.models.misc;

import java.util.Objects;

import static com.google.common.base.Preconditions.checkArgument;

public class Ratio
{
   private final long width;
   private final long height;

   /**
    * Creates a new <code>Ratio</code>.
    *
    * @param width  the width
    * @param height the height
    * @throws IllegalArgumentException if the width or height are not greater than zero
    */
   public Ratio(long width, long height)
   {
      checkArgument(width > 0, "width must be greater than zero");
      checkArgument(height > 0, "height must be greater than zero");

      this.width = width;
      this.height = height;
   }

   /**
    * Returns the width.
    *
    * @return the width
    */
   public long getWidth()
   {
      return width;
   }

   /**
    * Returns the height.
    *
    * @return the height
    */
   public long getHeight()
   {
      return height;
   }

   /**
    * Returns a string representation in the format of <code>WxH</code>.
    *
    * @return a string representation in the format of <code>WxH</code>
    */
   @Override
   public String toString()
   {
      return width + "x" + height;
   }

   @Override
   public boolean equals(Object o)
   {
      if (this == o) return true;
      if (!(o instanceof Ratio)) return false;

      Ratio that = (Ratio) o;

      return Objects.equals((double) width / height, (double) that.width / that.height);
   }

   @Override
   public int hashCode()
   {
      return Objects.hash((double) width / height);
   }
}


<|start_filename|>src/main/java/com/ivkos/wallhaven4j/models/ResourceFactoryFactory.java<|end_filename|>
package com.ivkos.wallhaven4j.models;

import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.ivkos.wallhaven4j.models.tag.Tag;
import com.ivkos.wallhaven4j.models.tag.TagFactory;
import com.ivkos.wallhaven4j.models.tagcategory.TagCategory;
import com.ivkos.wallhaven4j.models.tagcategory.TagCategoryFactory;
import com.ivkos.wallhaven4j.models.user.User;
import com.ivkos.wallhaven4j.models.user.UserFactory;
import com.ivkos.wallhaven4j.models.wallpaper.Wallpaper;
import com.ivkos.wallhaven4j.models.wallpaper.WallpaperFactory;
import com.ivkos.wallhaven4j.models.wallpapercollection.WallpaperCollection;
import com.ivkos.wallhaven4j.models.wallpapercollection.WallpaperCollectionFactory;

@Singleton
public class ResourceFactoryFactory
{
   private final TagFactory tagFactory;
   private final TagCategoryFactory tagCategoryFactory;
   private final UserFactory userFactory;
   private final WallpaperFactory wallpaperFactory;
   private final WallpaperCollectionFactory wallpaperCollectionFactory;

   @Inject
   ResourceFactoryFactory(TagFactory tagFactory, TagCategoryFactory tagCategoryFactory, UserFactory userFactory,
                          WallpaperFactory wallpaperFactory, WallpaperCollectionFactory wallpaperCollectionFactory)
   {
      this.tagFactory = tagFactory;
      this.tagCategoryFactory = tagCategoryFactory;
      this.userFactory = userFactory;
      this.wallpaperFactory = wallpaperFactory;
      this.wallpaperCollectionFactory = wallpaperCollectionFactory;
   }

   @SuppressWarnings("unchecked")
   public <I, F extends ResourceFactory<R, I>, R extends AbstractResource<I>> F getFactoryFor(Class<R> resourceClass)
   {
      if (resourceClass == Tag.class) return (F) tagFactory;
      if (resourceClass == TagCategory.class) return (F) tagCategoryFactory;
      if (resourceClass == User.class) return (F) userFactory;
      if (resourceClass == Wallpaper.class) return (F) wallpaperFactory;
      if (resourceClass == WallpaperCollection.class) return (F) wallpaperCollectionFactory;

      throw new IllegalArgumentException("There is no factory for the resource type " +
            resourceClass.getSimpleName());
   }
}


<|start_filename|>src/test/java/com/ivkos/wallhaven4j/util/exceptions/DescriptiveParseExceptionSupplierTest.java<|end_filename|>
package com.ivkos.wallhaven4j.util.exceptions;

import com.ivkos.wallhaven4j.Wallhaven;
import com.ivkos.wallhaven4j.models.wallpaper.Wallpaper;
import org.junit.Test;

import static com.ivkos.wallhaven4j.util.exceptions.DescriptiveParseExceptionSupplier.forResource;
import static java.lang.String.format;
import static org.junit.Assert.assertEquals;

public class DescriptiveParseExceptionSupplierTest
{
   @Test
   public void supplyForClass() throws Exception
   {
      DescriptiveParseExceptionSupplier supplier = forResource(Wallpaper.class, "name");

      assertEquals(ParseException.class, supplier.get().getClass());
      assertEquals("Could not parse name of Wallpaper", supplier.get().getMessage());
   }

   @Test
   public void supplyForObject() throws Exception
   {
      long id = 3;
      String thing = "awesomeness";

      Wallpaper wallpaper = new Wallhaven().getWallpaper(id);
      DescriptiveParseExceptionSupplier supplier = forResource(wallpaper, thing);

      assertEquals(ParseException.class, supplier.get().getClass());
      assertEquals(format("Could not parse %s of Wallpaper with id %d", thing, id), supplier.get().getMessage());
   }
}


<|start_filename|>src/test/java/com/ivkos/wallhaven4j/util/searchquery/SearchQueryBuilderTest.java<|end_filename|>
package com.ivkos.wallhaven4j.util.searchquery;

import com.google.common.collect.ImmutableSet;
import com.ivkos.wallhaven4j.models.misc.Ratio;
import com.ivkos.wallhaven4j.models.misc.Resolution;
import com.ivkos.wallhaven4j.models.misc.enums.Category;
import com.ivkos.wallhaven4j.models.misc.enums.Purity;
import org.junit.Before;
import org.junit.Test;

import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;

import static com.ivkos.wallhaven4j.models.misc.enums.Category.GENERAL;
import static com.ivkos.wallhaven4j.models.misc.enums.Category.PEOPLE;
import static com.ivkos.wallhaven4j.models.misc.enums.Order.DESC;
import static com.ivkos.wallhaven4j.models.misc.enums.Purity.SFW;
import static com.ivkos.wallhaven4j.models.misc.enums.Purity.SKETCHY;
import static com.ivkos.wallhaven4j.models.misc.enums.Sorting.DATE_ADDED;
import static com.ivkos.wallhaven4j.models.misc.enums.ToplistRange.LAST_1_DAY;
import static com.ivkos.wallhaven4j.util.searchquery.SearchQuery.joinRatioSet;
import static com.ivkos.wallhaven4j.util.searchquery.SearchQueryDefaults.*;
import static java.util.Arrays.asList;
import static org.junit.Assert.*;

public class SearchQueryBuilderTest
{
   private SearchQueryBuilder sqb;

   @Before
   public void setUp() throws Exception
   {
      sqb = new SearchQueryBuilder();
   }

   @Test
   public void keywordsAreJoinedCorrectly() throws Exception
   {
      sqb.keywords("kw1");
      assertEquals("kw1", sqb.build().getKeywords());

      sqb.keywords("kw1 kw2");
      assertEquals("kw1 kw2", sqb.build().getKeywords());

      sqb.keywords("kw1", "kw2");
      assertEquals("kw1 kw2", sqb.build().getKeywords());

      sqb.keywords(asList("kw1", "kw2"));
      assertEquals("kw1 kw2", sqb.build().getKeywords());
   }

   @Test(expected = NullPointerException.class)
   public void keywordsWontAcceptNull() throws Exception
   {
      sqb.keywords((Collection<String>) null);
   }

   @Test
   public void defaultKeywords() throws Exception
   {
      sqb.keywords();
      assertEquals(DEFAULT_KEYWORDS, sqb.build().getKeywords());
   }

   @Test
   public void categories() throws Exception
   {
      sqb.categories();
      assertTrue(!sqb.build().getCategories().isEmpty()); // should contain the default categories

      sqb.categories(GENERAL);
      assertTrue(sqb.build().getCategories().contains(GENERAL));

      sqb.categories(GENERAL, PEOPLE);
      EnumSet<Category> categories = sqb.build().getCategories();
      assertTrue(categories.contains(GENERAL));
      assertTrue(categories.contains(PEOPLE));
   }

   @Test(expected = NullPointerException.class)
   public void categoriesWontAcceptNull() throws Exception
   {
      sqb.categories(((Collection<Category>) null));
   }

   @Test
   public void defaultCategories() throws Exception
   {
      sqb.categories();
      assertEquals(DEFAULT_CATEGORIES, sqb.build().getCategories());
   }

   @Test
   public void purity() throws Exception
   {
      sqb.purity(SFW);
      assertEquals(EnumSet.of(SFW), sqb.build().getPurity());

      sqb.purity(SFW, SKETCHY);
      assertEquals(EnumSet.of(SFW, SKETCHY), sqb.build().getPurity());

      sqb.purity(EnumSet.of(SFW));
      assertEquals(EnumSet.of(SFW), sqb.build().getPurity());

      sqb.purity(EnumSet.of(SFW, SKETCHY));
      assertEquals(EnumSet.of(SFW, SKETCHY), sqb.build().getPurity());
   }

   @Test(expected = NullPointerException.class)
   public void purityWontAcceptNull() throws Exception
   {
      sqb.purity(((Collection<Purity>) null));
   }

   @Test
   public void defaultPurity() throws Exception
   {
      sqb.purity();
      assertEquals(DEFAULT_PURITY, sqb.build().getPurity());
   }

   @Test
   public void sorting() throws Exception
   {
      sqb.sorting(DATE_ADDED);
      assertEquals(DATE_ADDED, sqb.build().getSorting());
   }

   @Test(expected = NullPointerException.class)
   public void sortingWontAcceptNull() throws Exception
   {
      sqb.sorting(null);
   }

   @Test
   public void order() throws Exception
   {
      sqb.order(DESC);
      assertEquals(DESC, sqb.build().getOrder());
   }

   @Test(expected = NullPointerException.class)
   public void orderWontAcceptNull() throws Exception
   {
      sqb.order(null);
   }

   @Test
   public void toplistRange() throws Exception
   {
      sqb.toplistRange(LAST_1_DAY);
      assertEquals(LAST_1_DAY, sqb.build().getToplistRange());
   }

   @Test(expected = NullPointerException.class)
   public void toplsitRangeWontAcceptNull() throws Exception
   {
      sqb.toplistRange(null);
   }

   @Test
   public void resolutions() throws Exception
   {
      Resolution r1080p = new Resolution(1920, 1080);
      Resolution r720p = new Resolution(1280, 720);

      sqb.resolutions(r1080p);
      assertEquals(ImmutableSet.of(r1080p), sqb.build().getResolutions());

      sqb.resolutions(Collections.singletonList(r1080p));
      assertEquals(ImmutableSet.of(r1080p), sqb.build().getResolutions());

      sqb.resolutions(r1080p, r720p);
      assertEquals(ImmutableSet.of(r1080p, r720p), sqb.build().getResolutions());

      sqb.resolutions(asList(r1080p, r720p));
      assertEquals(ImmutableSet.of(r1080p, r720p), sqb.build().getResolutions());
   }

   @Test(expected = NullPointerException.class)
   public void resolutionsWontAcceptNull() throws Exception
   {
      sqb.resolutions(((Collection<Resolution>) null));
   }

   @Test
   public void defaultResolutions() throws Exception
   {
      sqb.resolutions();
      assertEquals(DEFAULT_RESOLUTIONS, sqb.build().getResolutions());
   }

   @Test
   public void ratios() throws Exception
   {
      Resolution r16x9 = new Resolution(16, 9);
      Resolution r4x3 = new Resolution(4, 3);

      sqb.ratios(r16x9);
      assertEquals(ImmutableSet.of(r16x9), sqb.build().getRatios());

      sqb.ratios(Collections.singletonList(r16x9));
      assertEquals(ImmutableSet.of(r16x9), sqb.build().getRatios());

      sqb.ratios(r16x9, r4x3);
      assertEquals(ImmutableSet.of(r16x9, r4x3), sqb.build().getRatios());

      sqb.ratios(asList(r16x9, r4x3));
      assertEquals(ImmutableSet.of(r16x9, r4x3), sqb.build().getRatios());
   }

   @Test(expected = NullPointerException.class)
   public void ratiosWontAcceptNull() throws Exception
   {
      sqb.ratios(((Collection<Ratio>) null));
   }

   @Test
   public void defaultRatios() throws Exception
   {
      sqb.ratios();
      assertEquals(DEFAULT_RATIOS, sqb.build().getRatios());
   }

   @Test
   public void pages() throws Exception
   {
      sqb.pages(7);
      assertEquals(7, sqb.build().getPages());
   }

   @Test(expected = IllegalArgumentException.class)
   public void pagesWontAcceptZero() throws Exception
   {
      sqb.pages(0);
   }

   @Test(expected = IllegalArgumentException.class)
   public void pagesWontAcceptNegatives() throws Exception
   {
      sqb.pages(-1);
   }

   @Test
   public void build() throws Exception
   {
      assertNotNull(sqb.build());
   }

   @Test
   public void joinRatios() throws Exception
   {
      String s1 = joinRatioSet(ImmutableSet.of(new Ratio(16, 9), new Resolution(1920, 1080)));
      assertEquals("16x9,1920x1080", s1);

      String s2 = joinRatioSet(ImmutableSet.of(new Ratio(16, 9)));
      assertEquals("16x9", s2);

      String s3 = joinRatioSet(Collections.emptySet());
      assertEquals("", s3);
   }

   @Test
   public void getUrl() throws Exception
   {
      String url = sqb.build().getUrl();
      assertEquals("https://alpha.wallhaven.cc/search?q=&categories=111&purity=100&resolutions=&ratios=" +
            "&sorting=relevance&order=desc&topRange=1M", url);

      String url2 = sqb.keywords("cats").build().getUrl();
      assertEquals("https://alpha.wallhaven.cc/search?q=cats&categories=111&purity=100&resolutions=&ratios=" +
            "&sorting=relevance&order=desc&topRange=1M", url2);
   }
}


<|start_filename|>src/main/java/com/ivkos/wallhaven4j/models/wallpapercollection/WallpaperCollectionIdentifier.java<|end_filename|>
package com.ivkos.wallhaven4j.models.wallpapercollection;

import com.ivkos.wallhaven4j.models.user.User;

import java.util.Objects;

public class WallpaperCollectionIdentifier
{
   private final long longId;
   private final User user;

   public WallpaperCollectionIdentifier(long longId, User user)
   {
      this.longId = longId;
      this.user = user;
   }

   public long getLongId()
   {
      return longId;
   }

   public User getUser()
   {
      return user;
   }

   @Override
   public boolean equals(Object o)
   {
      if (this == o) return true;
      if (!(o instanceof WallpaperCollectionIdentifier)) return false;
      WallpaperCollectionIdentifier that = (WallpaperCollectionIdentifier) o;
      return getLongId() == that.getLongId() &&
            Objects.equals(getUser(), that.getUser());
   }

   @Override
   public int hashCode()
   {
      return Objects.hash(getLongId(), getUser());
   }
}


<|start_filename|>src/main/java/com/ivkos/wallhaven4j/models/wallpaper/WallpaperFactory.java<|end_filename|>
package com.ivkos.wallhaven4j.models.wallpaper;

import com.google.inject.assistedinject.Assisted;
import com.ivkos.wallhaven4j.models.ResourceFactory;
import com.ivkos.wallhaven4j.models.misc.Resolution;
import com.ivkos.wallhaven4j.models.misc.enums.Category;
import com.ivkos.wallhaven4j.models.misc.enums.Purity;

public interface WallpaperFactory extends ResourceFactory<Wallpaper, Long>
{
   Wallpaper create(boolean preloadDom,
                    @Assisted("id") Long id,
                    Category category,
                    Purity purity,
                    Resolution resolution,
                    @Assisted("favoritesCount") long favoritesCount);
}


<|start_filename|>src/main/java/com/ivkos/wallhaven4j/models/tagcategory/TagCategoryFactory.java<|end_filename|>
package com.ivkos.wallhaven4j.models.tagcategory;

import com.ivkos.wallhaven4j.models.ResourceFactory;

public interface TagCategoryFactory extends ResourceFactory<TagCategory, Long>
{
   TagCategory create(boolean preloadDom, Long id, String name);

   TagCategory create(boolean preloadDom, Long id, String name, TagCategory parentCategory);
}


<|start_filename|>src/main/java/com/ivkos/wallhaven4j/util/exceptions/WallhavenException.java<|end_filename|>
package com.ivkos.wallhaven4j.util.exceptions;

/**
 * Thrown to indicate a problem with the library.
 */
public class WallhavenException extends RuntimeException
{
   public WallhavenException()
   {
   }

   public WallhavenException(String message)
   {
      super(message);
   }

   public WallhavenException(String message, Throwable cause)
   {
      super(message, cause);
   }

   public WallhavenException(Throwable cause)
   {
      super(cause);
   }

   public WallhavenException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace)
   {
      super(message, cause, enableSuppression, writableStackTrace);
   }
}


<|start_filename|>src/main/java/com/ivkos/wallhaven4j/util/jsonserializer/JsonSerializer.java<|end_filename|>
package com.ivkos.wallhaven4j.util.jsonserializer;

import java.lang.reflect.Type;

public interface JsonSerializer
{
   String toJson(Object src);

   <T> T fromJson(String json, Class<T> type);

   <T> T fromJson(String json, Type typeOfT);
}


<|start_filename|>src/main/java/com/ivkos/wallhaven4j/models/misc/enums/Purity.java<|end_filename|>
package com.ivkos.wallhaven4j.models.misc.enums;

import com.ivkos.wallhaven4j.models.misc.enums.util.BitfieldCompatible;

public enum Purity implements BitfieldCompatible
{
   NSFW, SKETCHY, SFW
}


<|start_filename|>src/main/java/com/ivkos/wallhaven4j/models/misc/enums/util/BitfieldCompatible.java<|end_filename|>
package com.ivkos.wallhaven4j.models.misc.enums.util;

public interface BitfieldCompatible
{
   // Enums implicitly extend java.lang.Enum which implements ordinal()
   int ordinal();

   default long getBitfieldValue()
   {
      return 1 << ordinal();
   }
}


<|start_filename|>src/main/java/com/ivkos/wallhaven4j/util/exceptions/ConnectionException.java<|end_filename|>
package com.ivkos.wallhaven4j.util.exceptions;

/**
 * Thrown to indicate a problem with the HTTP connection to Wallhaven.
 */
public class ConnectionException extends WallhavenException
{
   public ConnectionException()
   {
   }

   public ConnectionException(String message)
   {
      super(message);
   }

   public ConnectionException(String message, Throwable cause)
   {
      super(message, cause);
   }

   public ConnectionException(Throwable cause)
   {
      super(cause);
   }

   public ConnectionException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace)
   {
      super(message, cause, enableSuppression, writableStackTrace);
   }
}


<|start_filename|>src/main/java/com/ivkos/wallhaven4j/models/wallpapercollection/WallpaperCollection.java<|end_filename|>
package com.ivkos.wallhaven4j.models.wallpapercollection;

import com.google.common.collect.ImmutableMap;
import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject;
import com.ivkos.wallhaven4j.models.AbstractResource;
import com.ivkos.wallhaven4j.models.misc.enums.Purity;
import com.ivkos.wallhaven4j.models.misc.enums.util.BitfieldSum;
import com.ivkos.wallhaven4j.models.user.User;
import com.ivkos.wallhaven4j.models.wallpaper.Wallpaper;
import com.ivkos.wallhaven4j.util.ResourceFieldGetter;
import com.ivkos.wallhaven4j.util.UrlPrefixes;
import com.ivkos.wallhaven4j.util.WallhavenSession;
import com.ivkos.wallhaven4j.util.exceptions.ResourceNotAccessibleException;
import com.ivkos.wallhaven4j.util.htmlparser.HtmlElement;
import com.ivkos.wallhaven4j.util.htmlparser.OptionalSelector;
import com.ivkos.wallhaven4j.util.jsonserializer.JsonSerializer;
import com.ivkos.wallhaven4j.util.pagecrawler.thumbnailpage.ThumbnailPageCrawlerFactory;

import java.util.List;

import static com.google.common.collect.Lists.newArrayList;
import static com.google.common.collect.Lists.transform;
import static com.google.common.net.HttpHeaders.CONTENT_TYPE;
import static com.google.common.net.HttpHeaders.X_REQUESTED_WITH;
import static com.google.common.net.MediaType.JSON_UTF_8;
import static com.ivkos.wallhaven4j.util.exceptions.DescriptiveParseExceptionSupplier.forResource;
import static java.lang.Long.parseLong;
import static java.util.Collections.unmodifiableList;

public class WallpaperCollection extends AbstractResource<WallpaperCollectionIdentifier>
{
   private final JsonSerializer jsonSerializer;
   private final WallpaperCollectionTransformers collectionTransformers;
   private final ThumbnailPageCrawlerFactory thumbnailPageCrawlerFactory;

   private String name;

   private Long wallpapersCount;
   private Long viewsCount;
   private Long subscribersCount;
   private List<User> subscribers;

   @AssistedInject
   WallpaperCollection(WallhavenSession session,
                       JsonSerializer jsonSerializer,
                       WallpaperCollectionTransformers collectionTransformers,
                       ThumbnailPageCrawlerFactory thumbnailPageCrawlerFactory,
                       @Assisted boolean preloadDom,
                       @Assisted WallpaperCollectionIdentifier id)
   {
      super(session, preloadDom, id);
      this.jsonSerializer = jsonSerializer;
      this.collectionTransformers = collectionTransformers;
      this.thumbnailPageCrawlerFactory = thumbnailPageCrawlerFactory;

      if (preloadDom) populateFields();
   }

   @AssistedInject
   WallpaperCollection(WallhavenSession session,
                       JsonSerializer jsonSerializer,
                       WallpaperCollectionTransformers collectionTransformers,
                       ThumbnailPageCrawlerFactory thumbnailPageCrawlerFactory,
                       @Assisted boolean preloadDom,
                       @Assisted WallpaperCollectionIdentifier id,
                       @Assisted String name)
   {
      super(session, preloadDom, id);
      this.jsonSerializer = jsonSerializer;
      this.collectionTransformers = collectionTransformers;
      this.thumbnailPageCrawlerFactory = thumbnailPageCrawlerFactory;

      this.name = name;

      if (preloadDom) populateFields();
   }

   /**
    * Returns the user who has created this collection
    *
    * @return the user who has created this collection
    */
   public User getUser()
   {
      return getId().getUser();
   }

   @Override
   public String getUrl()
   {
      return getUser().getUrl() + "/favorites/" + id.getLongId();
   }

   /**
    * Returns the name of the collection
    *
    * @return the name of the collection
    */
   @Override
   public String toString()
   {
      return name;
   }

   /**
    * Returns the name of the collection
    *
    * @return the name of the collection
    */
   @ResourceFieldGetter
   public String getName()
   {
      if (name != null) return name;

      HtmlElement nameElement = OptionalSelector.of(getDom(), "#profile-content > header > h1")
            .orElseThrow(forResource(this, "name"));

      this.name = nameElement.getText();

      return this.name;
   }

   /**
    * Returns the number of wallpapers in the collection
    *
    * @return the number of wallpapers in the collection
    */
   @ResourceFieldGetter
   public Long getWallpapersCount()
   {
      if (wallpapersCount != null) return wallpapersCount;

      HtmlElement countElement = OptionalSelector.of(getDom(), "#profile-content > header > div > span:nth-child(1)")
            .orElseThrow(forResource(this, "wallpapers count"));

      String countText = countElement.getText().trim().replace(",", "");

      try {
         wallpapersCount = parseLong(countText);
      } catch (NumberFormatException e) {
         throw forResource(this, "wallpapers count").get(e);
      }

      return wallpapersCount;
   }

   /**
    * Returns the number of times the collection has been viewed
    *
    * @return the number of times the collection has been viewed
    */
   @ResourceFieldGetter
   public Long getViewsCount()
   {
      if (viewsCount != null) return viewsCount;

      HtmlElement countElement = OptionalSelector.of(getDom(), "#profile-content > header > div > span:nth-child(2)")
            .orElseThrow(forResource(this, "views count"));

      String countText = countElement.getText().trim().replace(",", "");

      try {
         viewsCount = parseLong(countText);
      } catch (NumberFormatException e) {
         throw forResource(this, "views count").get(e);
      }

      return viewsCount;
   }

   /**
    * Returns the number of users subscribed to this collection
    *
    * @return the number of users subscribed to this collection
    */
   @ResourceFieldGetter
   public Long getSubscribersCount()
   {
      if (subscribersCount != null) return subscribersCount;

      HtmlElement countElement = OptionalSelector.of(getDom(), "#profile-content > header > div > span:nth-child(3) > a")
            .orElseThrow(forResource(this, "subscribers count"));

      String countText = countElement.getText().trim().replace(",", "");

      try {
         subscribersCount = parseLong(countText);
      } catch (NumberFormatException e) {
         throw forResource(this, "subscribers count").get(e);
      }

      return subscribersCount;
   }

   /**
    * Returns a list of the users subscribed to this collection. Note that the list can be obtained only for collections
    * owned by the currently logged in user. This method will always throw {@link ResourceNotAccessibleException} if
    * this requirement is not met.
    *
    * @return a list of the users subscribed to this collection
    * @throws ResourceNotAccessibleException if the collection is not owned by the currently logged in user,
    *                                        or the current wallhaven session is anonymous
    */
   public List<User> getSubscribers()
   {
      if (subscribers != null) return subscribers;

      if (getSession().getCurrentUser() == null) {
         throw new ResourceNotAccessibleException("Subscribers list of collection is not accessible when not logged in");
      }

      if (!getId().getUser().equals(getSession().getCurrentUser())) {
         throw new ResourceNotAccessibleException(
               "Subscribers list of collection is not accessible because the current user does not own the collection");
      }

      String url = UrlPrefixes.URL_USER + "/favorites/" + this.getId().getLongId() + "/subscribers";

      String response = getSession().getHttpClient().get(url, ImmutableMap.of(
            X_REQUESTED_WITH, "XMLHttpRequest",
            CONTENT_TYPE, JSON_UTF_8.toString()
      )).getBody();

      XhrViewResponse xhrViewResponse = jsonSerializer.fromJson(response, XhrViewResponse.class);
      HtmlElement document = getSession().getHtmlParser().parse(xhrViewResponse.view, getUrl());
      List<HtmlElement> userlist = document.find("ul.userlist > li");

      subscribers = unmodifiableList(newArrayList(transform(userlist, collectionTransformers::transformSubscribers)));

      return subscribers;
   }

   /**
    * Fetches a list of the wallpapers that are in the collection
    *
    * @param pageCount Number of pages to fetch. One page could contain 24 (default), 32 or 64 wallpapers,
    *                  depending on the preference of the logged in user, set in Wallhaven's settings page.
    * @param purities  The purity of wallpapers to include in result. If no purity is specified,
    *                  the account defaults will be used when logged in.
    * @return a list of wallpapers in the collection, newly added first
    */
   public List<Wallpaper> getWallpapers(long pageCount, Purity... purities)
   {
      return thumbnailPageCrawlerFactory
            .create(getUrl())
            .getPageSequence(pageCount, ImmutableMap.of(
                  "purity", BitfieldSum.asThreeBitBinaryString(purities)
            ));
   }

   private static class XhrViewResponse
   {
      public String view;
   }
}


<|start_filename|>src/test/java/com/ivkos/wallhaven4j/models/misc/AbstractResourceTest.java<|end_filename|>
package com.ivkos.wallhaven4j.models.misc;

import com.google.inject.Guice;
import com.google.inject.Injector;
import com.ivkos.wallhaven4j.models.misc.enums.Purity;
import com.ivkos.wallhaven4j.models.tag.Tag;
import com.ivkos.wallhaven4j.models.tag.TagFactory;
import com.ivkos.wallhaven4j.models.wallpaper.Wallpaper;
import com.ivkos.wallhaven4j.models.wallpaper.WallpaperFactory;
import com.ivkos.wallhaven4j.util.WallhavenGuiceModule;
import org.junit.Test;

import static org.junit.Assert.*;

public class AbstractResourceTest
{
   private final Injector injector = Guice.createInjector(new WallhavenGuiceModule());
   private final TagFactory tagFactory = injector.getInstance(TagFactory.class);
   private final WallpaperFactory wallpaperFactory = injector.getInstance(WallpaperFactory.class);

   @Test
   public void equalObjectsAreEqual()
   {
      Tag tag1 = tagFactory.create(false, 1L, "somename", Purity.SFW);
      Tag tag2 = tagFactory.create(false, 1L, "somename", Purity.SFW);

      assertTrue(tag1.equals(tag2));
      assertTrue(tag1.hashCode() == tag2.hashCode());
   }

   @Test
   public void inequalObjectsAreInequal()
   {
      Tag tag1 = tagFactory.create(false, 1L, "somename", Purity.SFW);
      Wallpaper wallpaper = wallpaperFactory.create(false, 1L);

      assertFalse(tag1.equals(wallpaper));
      assertFalse(tag1.hashCode() == wallpaper.hashCode());
   }

   @Test
   public void toStringDefault() throws Exception
   {
      Wallpaper wallpaper = wallpaperFactory.create(false, 42L);

      assertEquals("42", wallpaper.toString());
   }

   @Test
   public void toStringWithoutResourceType() throws Exception
   {
      Wallpaper wallpaper = wallpaperFactory.create(false, 42L);

      assertEquals("42", wallpaper.toString(false));
   }

   @Test
   public void toStringWithResourceType() throws Exception
   {
      Wallpaper wallpaper = wallpaperFactory.create(false, 42L);

      assertEquals("Wallpaper 42", wallpaper.toString(true));
   }
}


<|start_filename|>src/main/java/com/ivkos/wallhaven4j/util/jsonserializer/GsonJsonSerializer.java<|end_filename|>
package com.ivkos.wallhaven4j.util.jsonserializer;

import com.google.gson.Gson;
import com.google.inject.Inject;

import java.lang.reflect.Type;

public class GsonJsonSerializer implements JsonSerializer
{
   private final Gson gson;

   @Inject
   GsonJsonSerializer(Gson gson)
   {
      this.gson = gson;
   }

   @Override
   public String toJson(Object src)
   {
      return gson.toJson(src);
   }

   @Override
   public <T> T fromJson(String json, Class<T> type)
   {
      return gson.fromJson(json, type);
   }

   @Override
   public <T> T fromJson(String json, Type typeOfT)
   {
      return gson.fromJson(json, typeOfT);
   }
}


<|start_filename|>src/main/java/com/ivkos/wallhaven4j/models/misc/enums/ToplistRange.java<|end_filename|>
package com.ivkos.wallhaven4j.models.misc.enums;

public enum ToplistRange
{
   LAST_1_DAY("1d"),
   LAST_3_DAYS("3d"),

   LAST_1_WEEK("1w"),

   LAST_1_MONTH("1M"),
   LAST_3_MONTHS("3M"),
   LAST_6_MONTHS("6M"),

   LAST_1_YEAR("1y");

   private final String str;

   ToplistRange(String str)
   {
      this.str = str;
   }

   @Override
   public String toString()
   {
      return str;
   }
}


<|start_filename|>src/main/java/com/ivkos/wallhaven4j/models/wallpaper/Wallpaper.java<|end_filename|>
package com.ivkos.wallhaven4j.models.wallpaper;

import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableMap;
import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject;
import com.ivkos.wallhaven4j.models.AbstractResource;
import com.ivkos.wallhaven4j.models.ResourceFactoryFactory;
import com.ivkos.wallhaven4j.models.misc.Color;
import com.ivkos.wallhaven4j.models.misc.Resolution;
import com.ivkos.wallhaven4j.models.misc.enums.Category;
import com.ivkos.wallhaven4j.models.misc.enums.Purity;
import com.ivkos.wallhaven4j.models.tag.Tag;
import com.ivkos.wallhaven4j.models.user.User;
import com.ivkos.wallhaven4j.models.wallpapercollection.WallpaperCollection;
import com.ivkos.wallhaven4j.util.ResourceFieldGetter;
import com.ivkos.wallhaven4j.util.UrlPrefixes;
import com.ivkos.wallhaven4j.util.WallhavenSession;
import com.ivkos.wallhaven4j.util.exceptions.DescriptiveParseExceptionSupplier;
import com.ivkos.wallhaven4j.util.exceptions.ParseException;
import com.ivkos.wallhaven4j.util.htmlparser.HtmlElement;
import com.ivkos.wallhaven4j.util.htmlparser.OptionalSelector;
import com.ivkos.wallhaven4j.util.htmlparser.TimeElementParser;
import com.ivkos.wallhaven4j.util.jsonserializer.JsonSerializer;
import org.joda.time.DateTime;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.collect.Lists.newArrayList;
import static com.google.common.collect.Lists.transform;
import static com.google.common.net.HttpHeaders.CONTENT_TYPE;
import static com.google.common.net.HttpHeaders.X_REQUESTED_WITH;
import static com.google.common.net.MediaType.JSON_UTF_8;
import static com.ivkos.wallhaven4j.models.misc.enums.Category.*;
import static com.ivkos.wallhaven4j.models.misc.enums.Purity.*;
import static com.ivkos.wallhaven4j.util.htmlparser.OptionalSelector.of;
import static java.lang.Long.parseLong;
import static java.util.Collections.unmodifiableList;

public class Wallpaper extends AbstractResource<Long>
{
   private final JsonSerializer jsonSerializer;
   private final ResourceFactoryFactory rff;
   private final WallpaperTransformers transformers;

   private Resolution resolution;
   private List<Color> colors;
   private List<Tag> tags;
   private Purity purity;
   private User user;
   private DateTime dateCreated;
   private Category category;
   private Double size;
   private Long viewsCount;

   private Long favoritesCount;
   private List<WallpaperCollection> collections;

   private String imageUrl;

   @AssistedInject
   Wallpaper(WallhavenSession session,
             JsonSerializer jsonSerializer,
             ResourceFactoryFactory resourceFactoryFactory,
             WallpaperTransformers wallpaperTransformers,
             @Assisted boolean preloadDom,
             @Assisted long id)
   {
      super(session, preloadDom, id);

      this.jsonSerializer = jsonSerializer;
      this.rff = resourceFactoryFactory;
      this.transformers = wallpaperTransformers;

      if (preloadDom) populateFields();
   }

   @AssistedInject
   Wallpaper(WallhavenSession session,
             JsonSerializer jsonSerializer,
             ResourceFactoryFactory resourceFactoryFactory,
             WallpaperTransformers wallpaperTransformers,
             @Assisted boolean preloadDom,
             @Assisted("id") long id,
             @Assisted Category category,
             @Assisted Purity purity,
             @Assisted Resolution resolution,
             @Assisted("favoritesCount") long favoritesCount)
   {
      super(session, preloadDom, id);

      this.jsonSerializer = jsonSerializer;
      this.rff = resourceFactoryFactory;
      this.transformers = wallpaperTransformers;

      this.category = category;
      this.purity = purity;
      this.resolution = resolution;
      this.favoritesCount = favoritesCount;

      if (preloadDom) populateFields();
   }

   @Override
   public String getUrl()
   {
      return UrlPrefixes.URL_WALLPAPER + "/" + id;
   }

   /**
    * Returns the short link of the wallpaper
    *
    * @return the short link of the wallpaper
    */
   public String getShortLink()
   {
      return UrlPrefixes.URL_WALLPAPER_SHORT + "/" + id;
   }

   /**
    * Returns the resolution of the wallpaper
    *
    * @return the resolution of the wallpaper
    */
   @ResourceFieldGetter
   public Resolution getResolution()
   {
      if (resolution != null) {
         return resolution;
      }

      HtmlElement wallpaperElement = of(getDom(), "#wallpaper").get();
      Map<String, String> dataset = wallpaperElement.getDataAttributes();

      String width = checkNotNull(dataset.get("wallpaper-width"), "could not parse resolution");
      String height = checkNotNull(dataset.get("wallpaper-height"), "could not parse resolution");

      resolution = new Resolution(parseLong(width), parseLong(height));

      return resolution;
   }

   /**
    * Returns a list of the dominant colors in the wallpaper
    *
    * @return a list of the dominant colors in the wallpaper
    */
   @ResourceFieldGetter
   public List<Color> getColors()
   {
      if (colors != null) {
         return colors;
      }

      List<HtmlElement> colorElements = getDom().find("ul.color-palette > li.color");

      colors = unmodifiableList(newArrayList(transform(colorElements, transformers::transformToColor)));

      return colors;
   }

   /**
    * Returns a list of the tags the wallpaper has been tagged with
    *
    * @return a list of the tags the wallpaper has been tagged with
    */
   @ResourceFieldGetter
   public List<Tag> getTags()
   {
      if (tags != null) return tags;

      List<HtmlElement> tagElements = getDom().find("ul#tags > li.tag");

      tags = unmodifiableList(newArrayList(transform(tagElements, transformers::transformToTag)));

      return tags;
   }

   /**
    * Returns the purity of the wallpaper
    *
    * @return the purity of the wallpaper
    */
   @ResourceFieldGetter
   public Purity getPurity()
   {
      if (purity != null) return purity;

      HtmlElement purityRadio = of(getDom(), "#wallpaper-purity-form > input[type=radio][checked]").get();
      HtmlElement purityLabel = purityRadio.getNextElementSibling();

      if (purityLabel == null) {
         throw new ParseException("Could not parse purity of wallpaper (could not find sibling of purity radio element)");
      }

      purity = purityLabel.hasClass("sfw") ? SFW
            : purityLabel.hasClass("sketchy") ? SKETCHY
            : purityLabel.hasClass("nsfw") ? NSFW
            : null;

      if (purity == null) throw new ParseException("Could not parse purity of wallpaper");

      return purity;
   }

   /**
    * Returns the user who has uploaded the wallpaper
    *
    * @return the user who has uploaded the wallpaper
    */
   @ResourceFieldGetter
   public User getUser()
   {
      if (user != null) return user;

      HtmlElement uploaderElement = of(getDom().findElementById("showcase-sidebar"), "dd.showcase-uploader > a.username").get();

      String username = uploaderElement.getText();

      user = rff.getFactoryFor(User.class).create(false, username);

      return user;
   }

   /**
    * Returns the date and time the wallpaper has been uploaded
    *
    * @return the date and time the wallpaper has been uploaded
    */
   @ResourceFieldGetter
   public DateTime getDateCreated()
   {
      if (dateCreated != null) return dateCreated;

      HtmlElement timeElement = of(getDom().findElementById("showcase-sidebar"), "dd.showcase-uploader > time").get();

      dateCreated = TimeElementParser.parse(timeElement);

      return dateCreated;
   }

   /**
    * Returns the category of the wallpaper
    *
    * @return the category of the wallpaper
    */
   @ResourceFieldGetter
   public Category getCategory()
   {
      if (category != null) return category;

      String categoryText = of(getElementNextToLabel("Category"), "dd")
            .get()
            .getText();

      switch (categoryText) {
         case "General":
            category = GENERAL;
            break;
         case "Anime":
            category = ANIME;
            break;
         case "People":
            category = PEOPLE;
            break;
         default:
            throw new ParseException("Could not parse wallpaper category");
      }

      return category;
   }

   /**
    * Returns the size of the wallpaper in KiB
    *
    * @return the size of the wallpaper in KiB
    */
   @ResourceFieldGetter
   public Double getSize()
   {
      if (size != null) return size;

      String sizeText = of(getElementNextToLabel("Size"), "dd")
            .get()
            .getText();

      Pattern pattern = Pattern.compile("([\\d.,]+)\\s(KiB|MiB)");
      Matcher matcher = pattern.matcher(sizeText);

      if (!matcher.find()) {
         throw new ParseException("Could not parse size: " + sizeText);
      }

      String sizeString = matcher.group(1).trim().replace(",", "");
      String unit = matcher.group(2);

      long multiplier = unit.equals("KiB") ? 1
            : unit.equals("MiB") ? 1024
            : 0;

      size = Double.parseDouble(sizeString) * multiplier;

      return size;
   }

   /**
    * Returns the number of views the wallpaper has gotten
    *
    * @return the number of views the wallpaper has gotten
    */
   @ResourceFieldGetter
   public Long getViewsCount()
   {
      if (viewsCount != null) return viewsCount;

      String viewsString = of(getElementNextToLabel("Views"), "dd")
            .get()
            .getText()
            .trim()
            .replace(",", "");


      try {
         viewsCount = parseLong(viewsString);
      } catch (NumberFormatException e) {
         throw new ParseException("Could not parse views count", e);
      }

      return viewsCount;
   }

   /**
    * Returns the number of collections the wallpaper has been added to
    *
    * @return the number of collections the wallpaper has been added to
    */
   @ResourceFieldGetter
   public Long getFavoritesCount()
   {
      if (favoritesCount != null) return favoritesCount;

      String favoritesCountString = of(getElementNextToLabel("Favorites"), "dd")
            .get()
            .getText()
            .trim()
            .replace(",", "");


      try {
         favoritesCount = parseLong(favoritesCountString);
      } catch (NumberFormatException e) {
         throw new ParseException("Could not parse favorites count", e);
      }

      return favoritesCount;
   }

   /**
    * Returns a list of collections the wallpaper has been added to
    *
    * @return a list of collections the wallpaper has been added to
    */
   public List<WallpaperCollection> getCollections()
   {
      if (collections != null) return collections;

      String response = getSession().getHttpClient().get(getUrl() + "/favorites", ImmutableMap.of(
            X_REQUESTED_WITH, "XMLHttpRequest",
            CONTENT_TYPE, JSON_UTF_8.toString()
      )).getBody();

      XhrViewResponse xhrViewResponse = jsonSerializer.fromJson(response, XhrViewResponse.class);
      HtmlElement document = getSession().getHtmlParser().parse(xhrViewResponse.view, getUrl());
      List<HtmlElement> userlist = document.find("ul.userlist > li");

      collections = unmodifiableList(newArrayList(transform(userlist,
            transformers::transformToWallpaperCollection)));

      return collections;
   }

   public String getThumbnailUrl()
   {
      return UrlPrefixes.URL_IMAGE_THUMB + getId() + ".jpg";
   }

   @ResourceFieldGetter
   public String getImageUrl()
   {
      if (imageUrl != null) return imageUrl;

      String schemalessUrl = OptionalSelector.of(getDom(), "img#wallpaper")
            .orElseThrow(DescriptiveParseExceptionSupplier.forResource(this, "image URL"))
            .getAttribute("src");

      URI uri;
      try {
         uri = new URI("https", schemalessUrl, null);
      } catch (URISyntaxException e) {
         throw DescriptiveParseExceptionSupplier.forResource(this, "image URL").get(e);
      }

      imageUrl = uri.toString();

      return imageUrl;
   }

   private HtmlElement getElementNextToLabel(String labelContent)
   {
      List<HtmlElement> dts = getDom().find("div.sidebar-content dl > dt");

      ArrayList<HtmlElement> list = newArrayList(Collections2.filter(dts, e -> labelContent.equals(e.getText())));

      if (list.isEmpty()) return null;

      return list.get(0).getNextElementSibling();
   }

   private static class XhrViewResponse
   {
      public String view;
   }
}


<|start_filename|>src/main/java/com/ivkos/wallhaven4j/util/htmlparser/jsoup/JsoupHtmlParser.java<|end_filename|>
package com.ivkos.wallhaven4j.util.htmlparser.jsoup;

import com.ivkos.wallhaven4j.util.htmlparser.HtmlElement;
import com.ivkos.wallhaven4j.util.htmlparser.HtmlParser;
import org.jsoup.Jsoup;

public class JsoupHtmlParser implements HtmlParser
{
   @Override
   public HtmlElement parse(String html)
   {
      return new JsoupHtmlElement(Jsoup.parse(html));
   }

   @Override
   public HtmlElement parse(String html, String baseUrl)
   {
      return new JsoupHtmlElement(Jsoup.parse(html, baseUrl));
   }
}
