fr.faylixe.googlecodejam.client.webservice.Problem Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of googlecodejam-client Show documentation
Show all versions of googlecodejam-client Show documentation
Java client API for Google Code Jam contest
package fr.faylixe.googlecodejam.client.webservice;
import fr.faylixe.googlecodejam.client.common.HTMLConstant;
import fr.faylixe.googlecodejam.client.common.NamedObject;
import fr.faylixe.googlecodejam.client.common.Resources;
import java.io.IOException;
import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.ObjectInputValidation;
import java.io.OptionalDataException;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import com.google.gson.Gson;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import com.google.gson.annotations.SerializedName;
/**
* POJO that aims to be bind to the /ContestInfo
* request, using Gson API. {@link Problem} instance belong
* to a {@link ContestInfo} object, and consists in the problem
* metadata such a name, description, and IO details.
*
* @author fv
*/
public final class Problem extends NamedObject implements ObjectInputValidation {
/** Serialization index. **/
private static final long serialVersionUID = 1L;
/** Full HTML text that describes this problem. **/
@SerializedName("body")
private String body;
/** Problem unique identifier. **/
@SerializedName("id")
private String id;
/** TODO : Figure out what is key for. **/
@SerializedName("key")
private String key;
/** TODO : Figure out what is type for. **/
@SerializedName("type")
private String type;
/** List of inputs that are available for solving in this problem. **/
@SerializedName("io")
private ProblemInput [] inputs;
/** Parent contest of this problem. **/
private transient ContestInfo parent;
/** Normalized name generated by {@link Resources#normalize(String)} method. **/
private String normalizedName;
/**
* Custom deserializer that normalizes problem body content.
*
* @author fv
*/
public static class Deserializer implements JsonDeserializer {
/** Target hostname problem are extracted from. **/
private final String hostname;
/**
* Default constructor.
*
* @param hostname Target hostname problem are extracted from.
*/
protected Deserializer(final String hostname) {
this.hostname = hostname;
}
/** {@inheritDoc} **/
@Override
public Problem deserialize(
final JsonElement element,
final Type type,
final JsonDeserializationContext context) throws JsonParseException {
final Gson parser = new Gson();
final Problem problem = parser.fromJson(element, Problem.class);
final String normalized = normalize(problem.body);
try {
problem.body = String.format(Resources.getHTMLTemplate(), normalized);
}
catch (final IOException e) {
throw new JsonParseException(e);
}
problem.normalizedName = Resources.normalize(problem.getName());
for (final ProblemInput input : problem.getProblemInputs()) {
input.setParent(problem);
}
return problem;
}
/**
* Normalizes the given HTML body text, by replacing
* images URI by absolute URI using preference hostname.
*
* @param body HTML body to normalize.
* @return Normalized HTML content.
*/
private String normalize(final String body) {
final Document document = Jsoup.parse(body);
final Elements images = document.getElementsByTag(HTMLConstant.IMG);
final StringBuilder builder = new StringBuilder();
for (final Element image : images) {
final String original = image.attr(HTMLConstant.SRC);
if (!original.startsWith("https://")) {
builder
.append(hostname.charAt(0) == '/' ? hostname.substring(0, -1) : hostname)
.append('/')
.append(original.charAt(0) == '/' ? original.substring(1) : original);
image.attr(HTMLConstant.SRC, builder.toString());
builder.delete(0, builder.length());
}
}
return document.html();
}
}
/**
* Contest setter that aims to be called by {@link ContestInfo} static factory.
*
* @param parent Parent contest of this problem.
*/
protected void setParent(final ContestInfo parent) {
this.parent = parent;
}
/**
* Getter for the parent contest of this problem.
*
* @return Parent contest of this problem.
* @see #parent
*/
public ContestInfo getParent() {
return parent;
}
/**
* Getter for the problem normalized name.
*
* @return Normalized name generated by {@link Resources#normalize(String)} method.
* @see #normalizedName
*/
public String getNormalizedName() {
return normalizedName;
}
/**
* Getter for the problem body description.
*
* @return Full HTML text that describes this problem.
* @see #body
*/
public String getBody() {
return body;
}
/**
* Getter for the problem id.
*
* @return Problem unique identifier.
* @see #id
*/
public String getId() {
return id;
}
/**
* Getter for the problem key.
* @return TODO : Figure out what is key for.
* @see #key
*/
public String getKey() {
return key;
}
/**
* Getter for the problem type.
*
* @return TODO : Figure out what is type for.
* @see #type
*/
public String getType() {
return type;
}
/**
* Getter for the problem inputs.
*
* @return List of inputs that are available for solving in this problem.
* @see #inputs
*/
public List getProblemInputs() {
return (inputs == null ? Collections.emptyList() : Arrays.asList(inputs));
}
/**
* Shortcut method for reducing law of Demeters issues.
*
* @param index Index of the problem input to retrieve.
* @return Problem input instance required.
* @throws ArrayIndexOutOfBoundsException If the given index is not valid.
*/
public ProblemInput getProblemInput(final int index) {
final List inputs = getProblemInputs();
if (index < 0 || inputs.size() <= index) {
throw new ArrayIndexOutOfBoundsException();
}
return inputs.get(index);
}
/**
* Filters and returns first problem input which name
* match the given type
*
* @param type Type of the input to retrieve (usually small or large).
* @return Corresponding input if any, null otherwise.
*/
public ProblemInput getProblemInput(final String type) {
final String name = type.toLowerCase();
for (final ProblemInput input : getProblemInputs()) {
if (input.getName().equals(name)) {
return input;
}
}
return null;
}
/** {@inheritDoc} **/
@Override
public void validateObject() throws InvalidObjectException {
for (final ProblemInput input : getProblemInputs()) {
input.setParent(this);
}
}
/**
* Custom readObject method that registers this object as a deserialization validator.
*
* @param stream {@link ObjectInputStream} to register this validator to.
* @throws OptionalDataException If any error occurs while reading the object.
* @throws ClassNotFoundException If the default readObject call can not find a required class.
* @throws IOException If any error occurs while reading the object.
*/
private void readObject(final ObjectInputStream stream) throws OptionalDataException, ClassNotFoundException, IOException {
stream.registerValidation(this, 0);
stream.defaultReadObject();
}
/** {@inheritDoc} **/
@Override
public int hashCode() {
return id.hashCode();
}
/** {@inheritDoc} **/
@Override
public boolean equals(final Object object) {
if (object == this) {
return true;
}
if (object == null || object.getClass() != getClass()) {
return false;
}
final Problem other = (Problem) object;
return id.equals(other.getId());
}
}