fr.faylixe.googlecodejam.client.CodeJamSession 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;
import static fr.faylixe.googlecodejam.client.executor.Request.*;
import fr.faylixe.googlecodejam.client.common.NamedObject;
import fr.faylixe.googlecodejam.client.common.Resources;
import fr.faylixe.googlecodejam.client.executor.HttpRequestExecutor;
import fr.faylixe.googlecodejam.client.webservice.ContestInfo;
import fr.faylixe.googlecodejam.client.webservice.InitialValues;
import fr.faylixe.googlecodejam.client.webservice.Problem;
import fr.faylixe.googlecodejam.client.webservice.ProblemInput;
import fr.faylixe.googlecodejam.client.webservice.SubmitResponse;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.util.List;
import com.google.api.client.http.HttpMediaType;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpResponse;
import com.google.api.client.http.MultipartContent;
import com.google.gson.Gson;
/**
* {@link CodeJamSession} is the main API entry point, which consists
* in logging to a code jam platform hostname and then providing
* facilities such as :
*
*
* - Retrieves contest, round
* - Interacts with platform API from a given round
* - Downloads input
* - Submits solutions
*
*
* @author fv
*/
public final class CodeJamSession extends NamedObject implements Serializable {
/** Serialization index. **/
private static final long serialVersionUID = 1L;
/** Downloaded input file extension used for filename generation. **/
private static final String INPUT_EXTENSION = ".in";
/** Practice file type for unactive contest. **/
private static final String PRACTICE = "practice";
/** Character used for file name generation. **/
private static final char FILENAME_SEPARATOR = '-';
/** Logged HTTP executor for executing queries. **/
private final HttpRequestExecutor executor;
/** Current selected round this session is working on. **/
private final Round round;
/** Current contest info this session is working on. **/
private final ContestInfo info;
/** Initial values this session is working on. **/
private final InitialValues values;
/**
*
* @param round
* @return
*/
private static String buildContestName(final Round round) {
final String contestName = Resources.normalize(round.getContestName());
final String roundName = Resources.normalize(round.getName());
return new StringBuilder(contestName.toLowerCase())
.append('-')
.append(roundName.toLowerCase())
.toString();
}
/**
* Default constructor.
*
* @param executor Logged HTTP executor for executing queries.
* @param round Current selected round this session is working on.
* @param info Current contest info this session is working on.
* @param values Initial values this session is working on.
*/
private CodeJamSession(final HttpRequestExecutor executor, final Round round, final ContestInfo info, final InitialValues values) {
super(buildContestName(round));
this.executor = executor;
this.round = round;
this.info = info;
this.values = values;
}
/**
* Reloads session components in order to prevent from any change.
* Based on the version number of {@link InitialValues}.
*
* @return This session if no change has been found, a newly created session updated otherwise.
* @throws IOException If any error occurs while retrieving new values.
*/
public CodeJamSession refresh() throws IOException {
// ISSUE : https://github.com/Faylixe/googlecodejam-client/issues/4
final InitialValues refreshedValues = InitialValues.get(executor, round);
if (refreshedValues.getVersion() > values.getVersion()) {
return createSession(executor, round);
}
return this;
}
/**
* Performs and returns a GET / request
* in order to get all round detail.
*
* @return Request response as a {@link ContestInfo} POJO.
*/
public ContestInfo getContestInfo() {
return info;
}
/**
* Indicates if the currently logged user is qualified
* for the next round or not.
*
* @return true if user is qualified, false otherwise.
*/
public boolean isQualified() {
return values.isQualified();
}
/**
* Indicates if the current session is logged in or not.
*
* @return true if user is logged, false otherwise.
*/
public boolean isLogged() {
return values.isLogged();
}
/**
* Retrieves the problem associated
* to the given letter.
*
* @param letter Letter that identifies the problem.
* @return Corresponding problem if exist, null otherwise.
*/
public Problem getProblem(final String letter) {
if (letter.isEmpty() || letter.length() > 1) {
return null;
}
final char identifier = letter.toUpperCase().charAt(0);
final int index = (int) identifier - (int) 'A';
final List problems = info.getProblems();
if (index < 0 || index >= problems.size()) {
return null;
}
return problems.get(index);
}
/**
* Indiciates if the contest is currently active,
* namely if competition is occuring at the current
* time, or not.
*
* @return true if the contest is active, false otherwise.
*/
public boolean isActive() {
final long now = System.currentTimeMillis();
final long start = values.getStart();
final long end = start + values.getLeft();
// TODO : Ensure predicate consistency.
return now >= start && now <= end;
}
/**
* Retrieves and returns the analysis
* for the given problem.
*
* @param problem Problem to retrieve analysis from.
* @return Analysis if any.
* @throws IOException If any error occurs while retrieving analysis.
*/
public String getContestAnalysis(final Problem problem) throws IOException {
final StringBuilder urlBuilder = new StringBuilder();
urlBuilder.append(round.getURL())
.append(DO)
.append(COMMAND_PARAMETER)
.append(ANALYSIS_COMMAND)
.append(PROBLEM_PARAMETER)
.append(problem.getId())
.append(CSRF_PARAMETER)
.append(values.getToken())
.append(AGENT_PARAMETER)
.append(DEFAULT_AGENT);
return executor.get(urlBuilder.toString());
}
/**
* Computes input file type suffix for the
* given input.
*
* @param input Problem input to retrieve suffix from.
* @return "practice" if the contest if not active, computed suffix otherwise.
*/
private String getType(final ProblemInput input) {
if (!isActive()) {
return PRACTICE;
}
return input.getSuffix(); // TODO : Ensure suffix is valid in a real contest.
}
/**
* Builds and returns a valid file name
* for the given problem input.
*
* @param input Input to retrieve file name from.
* @return Built file name.
*/
public String buildFilename(final ProblemInput input) {
final StringBuilder builder = new StringBuilder();
final Problem problem = input.getProblem();
final ContestInfo info = problem.getParent();
final int index = info.getProblems().indexOf(problem);
final char letter = (char) ((int) 'A' + index);
builder.append(letter)
.append(FILENAME_SEPARATOR)
.append(input.getName())
.append(FILENAME_SEPARATOR)
.append(getType(input))
.append(INPUT_EXTENSION);
return builder.toString();
}
/**
* Downloads and returns the stream of the
* input file associated to the given problem
* input.
*
* @param input Input to download file from.
* @return Stream to read which contains our downloaded file data.
* @throws IOException If any error occurs while downloading the file.
*/
public InputStream download(final ProblemInput input) throws IOException {
final String filename = buildFilename(input);
final StringBuilder urlBuilder = new StringBuilder();
urlBuilder.append(round.getURL())
.append(DO)
.append(COMMAND_PARAMETER)
.append(DOWNLOAD_COMMAND)
.append(PROBLEM_PARAMETER)
.append(input.getProblem().getId())
.append(FILENAME_PARAMETER)
.append(filename)
.append(INPUT_ID_PARAMETER)
.append(input.getNumber())
.append(CSRF_PARAMETER)
.append(values.getURLEncodedToken())
.append(AGENT_PARAMETER)
.append(DEFAULT_AGENT);
final HttpRequest request = executor.getRequest(urlBuilder.toString());
final HttpResponse response = request.execute();
return response.getContent();
}
/**
* Submits the given output file and the
* given source file for the given problem
* input. This method should be call only
* after a successful call to the {@link #download(ProblemInput)}
* method on the same input, as the evaluation
* system will judge the last downloaded dataset
* based on the internal token / session.
*
* @param input Input file to submit solution for.
* @param output Output file produced by the algorithm.
* @param source Source code file of the algorithm to submit.
* @return Request response, as a {@link SubmitResponse} instance.
* @throws IOException If any error occurs while uploading data, or performing the request.
*/
public SubmitResponse submit(final ProblemInput input, final File output, final File source) throws IOException {
final MultipartContent content = createContent(input, output, source);
final StringBuilder urlBuilder = new StringBuilder();
urlBuilder.append(round.getURL());
urlBuilder.append(DO);
final String response = executor.post(urlBuilder.toString(), content);
final Gson gson = new Gson();
return gson.fromJson(response, SubmitResponse.class);
}
/**
* Created and returns a valid {@link MultipartContent} instance
* that contains data required for submission.
*
* @param input Input file to submit solution for.
* @param output Output file produced by the algorithm.
* @param source Source code file of the algorithm to submit.
* @return Created multipart content.
*/
private MultipartContent createContent(final ProblemInput input, final File output, final File source) throws IOException {
final HttpMediaType type = new HttpMediaType(MEDIA_TYPE);
type.setParameter(BOUNDARY, createBoundary());
final MultipartContent content = new MultipartContent()
.setMediaType(type)
.addPart(HttpRequestExecutor.buildDataPart(CSRF_PARAMETER_NAME, values.getToken()))
.addPart(HttpRequestExecutor.buildFilePart(ANSWER_PARAMETER, output))
.addPart(HttpRequestExecutor.buildFilePart(SOURCE_FILE_PARAMETER, source))
.addPart(HttpRequestExecutor.buildDataPart(COMMAND_PARAMETER_NAME, SUBMIT_COMMAND))
.addPart(HttpRequestExecutor.buildDataPart(PROBLEM_PARAMETER_NAME, input.getProblem().getId()))
.addPart(HttpRequestExecutor.buildDataPart(INPUT_ID_PARAMETER_NAME, String.valueOf(input.getNumber())))
.addPart(HttpRequestExecutor.buildDataPart(NUM_SOURCE_FILE_PARAMETER, DEFAULT_NUM_SOURCE_FILE))
.addPart(HttpRequestExecutor.buildDataPart(AGENT_PARAMETER_NAME, DEFAULT_AGENT));
return content;
}
/**
* Static factory method that should be used for creating a session.
* Loads associated contest info and initial values from the given
* round, using the given executor.
*
* @param executor {@link HttpRequestExecutor} instance to use.
* @param round Contextual {@link Round} instance this session is bound to.
* @return Created session.
* @throws IOException If any error occurs while retrieving contest info or initial values.
*/
public static CodeJamSession createSession(final HttpRequestExecutor executor, final Round round) throws IOException {
final ContestInfo info = ContestInfo.get(executor, round);
final InitialValues values = InitialValues.get(executor, round);
return new CodeJamSession(executor, round, info, values);
}
}