Initial public release

This commit is contained in:
Jedediah Smith 2017-01-29 19:43:34 -05:00
commit 7755843923
2109 changed files with 152113 additions and 0 deletions

9
.gitattributes vendored Normal file
View File

@ -0,0 +1,9 @@
# Set default behaviour, in case users don't have core.autocrlf set.
* text=auto
# Explicitly declare text files we want to always be normalized and converted
# to native line endings on checkout.
*.java text
*.xml text
*.yml text
README.md text

34
.gitignore vendored Normal file
View File

@ -0,0 +1,34 @@
# Maven generated files
target
*.jar
dependency-reduced-pom.xml
# Mac OSX generated files
.DS_Store
# Eclipse generated files
.classpath
.project
.settings
# IntelliJ IDEA
.idea
*.iml
# Vim generated files
*~
*.swp
# XCode 3 and 4 generated files
*.mode1
*.mode1v3
*.mode2v3
*.perspective
*.perspectivev3
*.pbxuser
*.xcworkspace
xcuserdata
*class
# CrowdIn
Commons/core/src/main/i18n/translations/*

95
API/api/pom.xml Normal file
View File

@ -0,0 +1,95 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>tc.oc</groupId>
<artifactId>api-parent</artifactId>
<relativePath>../pom.xml</relativePath>
<version>1.11-SNAPSHOT</version>
</parent>
<artifactId>api</artifactId>
<packaging>jar</packaging>
<name>API</name>
<description>ProjectAres API layer</description>
<dependencies>
<!-- Our stuff -->
<dependency>
<groupId>tc.oc</groupId>
<artifactId>util-core</artifactId>
<version>${project.version}</version>
</dependency>
<!-- Network stuff -->
<dependency>
<groupId>com.google.http-client</groupId>
<artifactId>google-http-client</artifactId>
<version>1.18.0-rc</version>
</dependency>
<dependency>
<groupId>com.google.http-client</groupId>
<artifactId>google-http-client-gson</artifactId>
<version>1.18.0-rc</version>
</dependency>
<dependency>
<groupId>com.damnhandy</groupId>
<artifactId>handy-uri-templates</artifactId>
<version>2.0.2</version>
</dependency>
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>3.3.5</version>
</dependency>
<!-- Testing -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<targetPath>.</targetPath>
<filtering>true</filtering>
<directory>${basedir}/src/main/resources/</directory>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>2.1</version>
<configuration>
<artifactSet>
<includes>
<include>tc.oc:util-core</include>
<include>com.google.http-client:google-http-client</include>
<include>com.google.http-client:google-http-client-gson</include>
<include>com.damnhandy:handy-uri-templates</include>
<include>com.rabbitmq:amqp-client</include>
</includes>
</artifactSet>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,48 @@
package tc.oc.api;
import tc.oc.api.connectable.ConnectablesManifest;
import tc.oc.api.document.DocumentsManifest;
import tc.oc.api.engagement.EngagementModelManifest;
import tc.oc.api.games.GameModelManifest;
import tc.oc.api.maps.MapModelManifest;
import tc.oc.api.match.MatchModelManifest;
import tc.oc.api.message.MessagesManifest;
import tc.oc.api.model.ModelsManifest;
import tc.oc.api.punishments.PunishmentModelManifest;
import tc.oc.api.reports.ReportModelManifest;
import tc.oc.api.serialization.SerializationManifest;
import tc.oc.api.servers.ServerModelManifest;
import tc.oc.api.sessions.SessionModelManifest;
import tc.oc.api.tourney.TournamentModelManifest;
import tc.oc.api.trophies.TrophyModelManifest;
import tc.oc.api.users.UserModelManifest;
import tc.oc.api.whispers.WhisperModelManifest;
import tc.oc.commons.core.inject.HybridManifest;
import tc.oc.commons.core.logging.LoggingManifest;
public final class ApiManifest extends HybridManifest {
@Override
protected void configure() {
install(new LoggingManifest()); // Load this right away, so we don't get log spam
publicBinder().install(new SerializationManifest());
install(new DocumentsManifest());
install(new MessagesManifest());
install(new ModelsManifest());
install(new ConnectablesManifest());
install(new ServerModelManifest());
install(new UserModelManifest());
install(new SessionModelManifest());
install(new GameModelManifest());
install(new ReportModelManifest());
install(new PunishmentModelManifest());
install(new MapModelManifest());
install(new MatchModelManifest());
install(new EngagementModelManifest());
install(new WhisperModelManifest());
install(new TrophyModelManifest());
install(new TournamentModelManifest());
}
}

View File

@ -0,0 +1,12 @@
package tc.oc.api.annotations;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Generic annotation for something that requires an API connection,
* and should be ommitted or cause an error if not connected.
*
* (maybe we should make that distinction explicit?)
*/
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiRequired {}

View File

@ -0,0 +1,19 @@
package tc.oc.api.annotations;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import tc.oc.api.docs.virtual.Document;
/**
* Anything descended from {@link Document} can use this annotation to indicate that a method,
* field, or entire class/interface should be included in serialization.
* Applying it to a class is equivalent to applying it to every method declared in that class.
*
* Serialized methods must return a value and take no parameters. The returned value will be
* serialized using the method name.
*/
@Retention(RetentionPolicy.RUNTIME)
public @interface Serialize {
boolean value() default true;
}

View File

@ -0,0 +1,5 @@
package tc.oc.api.config;
public interface ApiConfiguration {
String primaryQueueName();
}

View File

@ -0,0 +1,7 @@
package tc.oc.api.config;
public final class ApiConstants {
private ApiConstants() {}
public static final int PROTOCOL_VERSION = 4;
}

View File

@ -0,0 +1,19 @@
package tc.oc.api.connectable;
import java.io.IOException;
import tc.oc.minecraft.api.event.Activatable;
import tc.oc.commons.core.plugin.PluginFacet;
/**
* Service that needs to be connected and disconnected along with the API.
*
* Use a {@link ConnectableBinder} to register these.
*
* TODO: This should probably extend {@link PluginFacet},
* but to do that, API needs to be able to find the services bound in other plugins.
*/
public interface Connectable extends Activatable {
default void connect() throws IOException {};
default void disconnect() throws IOException {};
}

View File

@ -0,0 +1,10 @@
package tc.oc.api.connectable;
import com.google.inject.Binder;
import tc.oc.commons.core.inject.SetBinder;
public class ConnectableBinder extends SetBinder<Connectable> {
public ConnectableBinder(Binder binder) {
super(binder);
}
}

View File

@ -0,0 +1,14 @@
package tc.oc.api.connectable;
import tc.oc.commons.core.inject.HybridManifest;
import tc.oc.commons.core.plugin.PluginFacetBinder;
public class ConnectablesManifest extends HybridManifest {
@Override
protected void configure() {
bindAndExpose(Connector.class);
new PluginFacetBinder(binder())
.add(Connector.class);
}
}

View File

@ -0,0 +1,66 @@
package tc.oc.api.connectable;
import java.io.IOException;
import java.util.Set;
import java.util.logging.Logger;
import javax.inject.Inject;
import javax.inject.Singleton;
import tc.oc.commons.core.exception.ExceptionHandler;
import tc.oc.commons.core.logging.Loggers;
import tc.oc.commons.core.plugin.PluginFacet;
import tc.oc.commons.core.util.ExceptionUtils;
import static com.google.common.base.Preconditions.checkState;
import static tc.oc.commons.core.IterableUtils.reverseForEach;
import static tc.oc.commons.core.exception.LambdaExceptionUtils.rethrowConsumer;
@Singleton
public class Connector implements PluginFacet {
protected final Logger logger;
private final ExceptionHandler exceptionHandler;
private final Set<Connectable> services;
private boolean connected;
@Inject
Connector(Loggers loggers, ExceptionHandler exceptionHandler, Set<Connectable> services) {
this.exceptionHandler = exceptionHandler;
this.services = services;
this.logger = loggers.get(getClass());
}
private void connect(Connectable service) throws IOException {
if(service.isActive()) {
logger.fine(() -> "Connecting " + service.getClass().getName());
service.connect();
}
}
private void disconnect(Connectable service) throws IOException {
if(service.isActive()) {
logger.fine(() -> "Disconnecting " + service.getClass().getName());
service.disconnect();
}
}
public boolean isConnected() {
return connected;
}
@Override
public void enable() {
checkState(!connected, "already connected");
connected = true;
logger.fine(() -> "Connecting all services");
ExceptionUtils.propagate(() -> services.forEach(rethrowConsumer(this::connect)));
}
@Override
public void disable() {
checkState(connected, "not connected");
connected = false;
logger.fine(() -> "Disconnecting all services");
reverseForEach(services, service -> exceptionHandler.run(() -> disconnect(service)));
}
}

View File

@ -0,0 +1,41 @@
package tc.oc.api.docs;
import javax.inject.Inject;
import com.google.gson.Gson;
import tc.oc.api.docs.virtual.Model;
import tc.oc.api.serialization.Pretty;
/**
* Implements some boilerplate stuff for {@link Model}
*/
public abstract class AbstractModel implements Model {
protected @Inject @Pretty Gson prettyGson;
@Override
public boolean equals(Object o) {
if(this == o)
return true;
if(!(o instanceof Model))
return false;
return _id().equals(((Model) o)._id());
}
@Override
public int hashCode() {
return _id().hashCode();
}
@Override
public String toString() {
if(prettyGson == null) return super.toString();
try {
return prettyGson.toJson(this);
} catch(Exception e) {
return super.toString() + " (exception trying to inspect fields: " + e + ")";
}
}
}

View File

@ -0,0 +1,21 @@
package tc.oc.api.docs;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import tc.oc.api.annotations.Serialize;
import tc.oc.api.docs.virtual.Model;
@Serialize
public interface Arena extends Model {
@Nonnull String game_id();
@Nonnull String datacenter();
int num_playing();
int num_queued();
@Nullable String next_server_id();
@Override @Serialize(false)
default String toShortString() {
return game_id() + "[" + datacenter() + "]";
}
}

View File

@ -0,0 +1,26 @@
package tc.oc.api.docs;
import java.time.Instant;
import javax.annotation.Nullable;
import tc.oc.api.annotations.Serialize;
import tc.oc.api.docs.virtual.DeletableModel;
public class BasicDeletableModel extends BasicModel implements DeletableModel {
@Serialize private Instant died_at;
@Override public @Nullable Instant died_at() {
return died_at;
}
@Override
public boolean dead() {
return died_at() != null;
}
@Override
public boolean alive() {
return died_at() == null;
}
}

View File

@ -0,0 +1,28 @@
package tc.oc.api.docs;
import tc.oc.api.annotations.Serialize;
import tc.oc.api.docs.virtual.Model;
/**
* Implements {@link Model#_id()} as a public field.
*/
public class BasicModel extends AbstractModel {
@Serialize public String _id;
public BasicModel() {
}
public BasicModel(String _id) {
this._id = _id;
}
@Override
public String _id() {
return _id;
}
public String getId() {
return _id;
}
}

View File

@ -0,0 +1,5 @@
package tc.oc.api.docs;
import tc.oc.api.docs.virtual.DeathDoc;
public interface Death extends DeathDoc.Complete {}

View File

@ -0,0 +1,16 @@
package tc.oc.api.docs;
import java.util.List;
import javax.annotation.Nonnull;
import tc.oc.api.annotations.Serialize;
import tc.oc.api.docs.team.Team;
import tc.oc.api.docs.virtual.MatchDoc;
import tc.oc.api.docs.virtual.Model;
@Serialize
public interface Entrant extends Model {
@Nonnull Team team();
@Nonnull List<PlayerId> members();
@Nonnull List<MatchDoc> matches();
}

View File

@ -0,0 +1,19 @@
package tc.oc.api.docs;
import javax.annotation.Nonnull;
import tc.oc.api.annotations.Serialize;
import tc.oc.api.docs.virtual.Model;
import tc.oc.api.docs.virtual.ServerDoc;
@Serialize
public interface Game extends Model {
@Nonnull String name();
int priority();
@Nonnull ServerDoc.Visibility visibility();
@Override @Serialize(false)
default String toShortString() {
return name();
}
}

View File

@ -0,0 +1,25 @@
package tc.oc.api.docs;
import tc.oc.api.annotations.Serialize;
import tc.oc.api.docs.virtual.MapDoc;
import javax.annotation.Nullable;
@Serialize
public class MapRating {
public final String player_id;
public final @Nullable String map_id;
public final String map_name;
public final String map_version;
public final int score;
public final String comment;
public MapRating(UserId player, MapDoc map, int score, String comment) {
this.player_id = player.player_id();
this.map_id = map._id();
this.map_name = map.name();
this.map_version = map.version().toString();
this.score = score;
this.comment = comment;
}
}

View File

@ -0,0 +1,9 @@
package tc.oc.api.docs;
public enum MatchState {
IDLE,
STARTING,
HUDDLE,
RUNNING,
FINISHED
}

View File

@ -0,0 +1,60 @@
package tc.oc.api.docs;
import java.time.Instant;
import tc.oc.api.annotations.Serialize;
import tc.oc.api.docs.virtual.Model;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@Serialize
public interface Objective extends Model {
@Nonnull String name();
@Nonnull String type();
@Nonnull String feature_id();
@Nonnull Instant date();
@Nonnull String match_id();
@Nonnull String server_id();
@Nonnull String family();
@Nullable Double x();
@Nullable Double y();
@Nullable Double z();
@Nullable String team();
@Nullable String player();
@Serialize
interface Colored extends Objective {
@Nonnull String color();
}
@Serialize
interface DestroyableDestroy extends Objective {
default String type() { return "destroyable_destroy"; }
int blocks_broken();
double blocks_broken_percentage();
}
@Serialize
interface CoreBreak extends Objective {
default String type() { return "core_break"; }
@Nonnull String material();
}
@Serialize
interface FlagCapture extends Colored {
default String type() { return "flag_capture"; }
@Nullable String net_id();
}
@Serialize
interface WoolPlace extends Colored {
default String type() { return "wool_place"; }
}
@Override @Serialize(false)
default String toShortString() {
return name() + "[" + type() + "]";
}
}

View File

@ -0,0 +1,33 @@
package tc.oc.api.docs;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.time.Instant;
import tc.oc.api.annotations.Serialize;
import tc.oc.api.docs.virtual.Model;
import tc.oc.api.model.ModelName;
public interface Participation {
interface Partial extends Model {} // Partials always have _id()
@Serialize
interface Start extends Partial {
@Nonnull Instant start();
@Nullable String team_id();
@Nullable String league_team_id();
@Nonnull String player_id();
@Nonnull String family();
@Nonnull String match_id();
@Nonnull String server_id();
@Nonnull String session_id();
}
@Serialize
interface Finish extends Partial {
@Nullable Instant end();
}
@ModelName("Participation")
interface Complete extends Start, Finish {}
}

View File

@ -0,0 +1,25 @@
package tc.oc.api.docs;
import javax.annotation.Nonnull;
import tc.oc.api.annotations.Serialize;
import tc.oc.api.docs.virtual.CompetitorDoc;
import tc.oc.api.docs.virtual.Model;
/**
* Subclass of {@link UserId} that adds a {@link #username()} field, which contains
* the most recently seen username for the player. It also extends {@link Model},
* so the {@link #_id()} field is available.
*
* This class is used to pass an ID and username around together, so that it can both be
* displayed and used in the DB (like we used to be able to do with usernames alone).
*/
@Serialize
public interface PlayerId extends UserId, Model, CompetitorDoc {
@Nonnull String username();
@Override @Serialize(false)
default String toShortString() {
return username();
}
}

View File

@ -0,0 +1,5 @@
package tc.oc.api.docs;
import tc.oc.api.docs.virtual.PunishmentDoc;
public interface Punishment extends PunishmentDoc.Complete {}

View File

@ -0,0 +1,5 @@
package tc.oc.api.docs;
import tc.oc.api.docs.virtual.ReportDoc;
public interface Report extends ReportDoc.Complete {}

View File

@ -0,0 +1,82 @@
package tc.oc.api.docs;
public class SemanticVersion {
protected final int major;
protected final int minor;
protected final int patch;
public SemanticVersion(int major, int minor, int patch) {
this.major = major;
this.minor = minor;
this.patch = patch;
}
public int major() {
return this.major;
}
public int minor() {
return this.minor;
}
public int patch() {
return this.patch;
}
@Override
public String toString() {
if(patch == 0) {
return major + "." + minor;
} else {
return major + "." + minor + "." + patch;
}
}
/**
* Return true if the major versions match and the minor version
* and patch levels are less or equal to the given version
*/
public boolean isNoNewerThan(SemanticVersion spec) {
return this.major == spec.major &&
(this.minor < spec.minor ||
(this.minor == spec.minor &&
this.patch <= spec.patch));
}
/**
* Return true if the major versions match and the minor version
* and patch levels are greater than the given version
*/
public boolean isNewerThan(SemanticVersion spec) {
return this.major == spec.major &&
(this.minor > spec.minor ||
(this.minor == spec.minor &&
this.patch > spec.patch));
}
/**
* Return true if the major versions match and the minor version
* and patch levels are greater or equal to the given version
*/
public boolean isNoOlderThan(SemanticVersion spec) {
return this.major == spec.major &&
(this.minor > spec.minor ||
(this.minor == spec.minor &&
this.patch >= spec.patch));
}
/**
* Return true if the major versions match and the minor version
* and patch levels are less than the given version
*/
public boolean isOlderThan(SemanticVersion spec) {
return this.major == spec.major &&
(this.minor < spec.minor ||
(this.minor == spec.minor &&
this.patch < spec.patch));
}
public int[] toArray() {
return new int[] {major, minor, patch};
}
}

View File

@ -0,0 +1,5 @@
package tc.oc.api.docs;
import tc.oc.api.docs.virtual.ServerDoc;
public interface Server extends ServerDoc.Complete {}

View File

@ -0,0 +1,5 @@
package tc.oc.api.docs;
import tc.oc.api.docs.virtual.SessionDoc;
public interface Session extends SessionDoc.Complete {}

View File

@ -0,0 +1,56 @@
package tc.oc.api.docs;
import tc.oc.api.annotations.Serialize;
import static com.google.common.base.Preconditions.checkNotNull;
public class SimplePlayerId extends SimpleUserId implements PlayerId {
@Serialize private String _id;
@Serialize private String username;
public SimplePlayerId(String _id, String player_id, String username) {
super(player_id);
this._id = checkNotNull(_id);
this.username = checkNotNull(username);
}
public SimplePlayerId(PlayerId playerId) {
this(playerId._id(), playerId.player_id(), playerId.username());
}
/** For deserialization only */
protected SimplePlayerId() {
super();
this._id = this.username = null;
}
/**
* Return a {@link SimplePlayerId} equal to the given {@link PlayerId}
*/
public static SimplePlayerId copyOf(PlayerId playerId) {
return playerId.getClass().equals(SimplePlayerId.class) ? (SimplePlayerId) playerId
: new SimplePlayerId(playerId);
}
@Serialize
@Override
public String _id() {
return _id;
}
@Serialize
@Override
public String username() {
return username;
}
@Override
public String toString() {
return getClass().getSimpleName() +
"{_id=" + _id() +
" player_id=" + player_id() +
" username=" + username() +
"}";
}
}

View File

@ -0,0 +1,46 @@
package tc.oc.api.docs;
import tc.oc.api.annotations.Serialize;
import static com.google.common.base.Preconditions.checkNotNull;
public class SimpleUserId implements UserId {
@Serialize private String player_id;
public SimpleUserId(String player_id) {
this.player_id = checkNotNull(player_id);
}
/** For deserialization only */
protected SimpleUserId() {
this.player_id = null;
}
public static SimpleUserId copyOf(UserId userId) {
return userId.getClass().equals(SimpleUserId.class) ? (SimpleUserId) userId
: new SimpleUserId(userId.player_id());
}
@Serialize
@Override
public String player_id() {
return this.player_id;
}
@Override
final public boolean equals(Object obj) {
return obj instanceof UserId && ((UserId) obj).player_id().equals(this.player_id());
}
@Override
final public int hashCode() {
return this.player_id().hashCode();
}
@Override
public String toString() {
return getClass().getSimpleName() +
"{player_id=" + player_id() + "}";
}
}

View File

@ -0,0 +1,21 @@
package tc.oc.api.docs;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.time.Instant;
import tc.oc.api.annotations.Serialize;
import tc.oc.api.docs.virtual.Model;
@Serialize
public interface Ticket extends Model {
@Nonnull PlayerId user();
@Nonnull String arena_id();
@Nullable String server_id();
@Nullable Instant dispatched_at();
@Override @Serialize(false)
default String toShortString() {
return user().toShortString();
}
}

View File

@ -0,0 +1,43 @@
package tc.oc.api.docs;
import java.time.Instant;
import java.util.List;
import java.util.Set;
import com.google.common.collect.Lists;
import tc.oc.api.annotations.Serialize;
import tc.oc.api.docs.virtual.Document;
import tc.oc.api.docs.virtual.MapDoc;
import tc.oc.api.docs.virtual.Model;
@Serialize
public interface Tournament extends Model {
String name();
Instant start();
Instant end();
int min_players_per_match();
int max_players_per_match();
List<team.Id> accepted_teams();
default List<String> acceptedTeamNames() {
return Lists.transform(accepted_teams(), team.Id::name);
}
/**
* game type -> [map ids]
*
* Key is some kind of string identifying game type e.g. "core", "wool", "tdm"
*
* Value is a set of {@link MapDoc#_id()}
*/
List<MapClassification> map_classifications();
@Serialize
interface MapClassification extends Document {
String name();
Set<String> map_ids();
}
}

View File

@ -0,0 +1,19 @@
package tc.oc.api.docs;
import tc.oc.api.annotations.Serialize;
import tc.oc.api.docs.virtual.Model;
import javax.annotation.Nonnull;
@Serialize
public interface Trophy extends Model {
@Nonnull String name();
@Nonnull String description();
@Override @Serialize(false)
default String toShortString() {
return _id();
}
}

View File

@ -0,0 +1,5 @@
package tc.oc.api.docs;
import tc.oc.api.docs.virtual.UserDoc;
public interface User extends UserDoc.Login {}

View File

@ -0,0 +1,21 @@
package tc.oc.api.docs;
import javax.annotation.Nonnull;
import tc.oc.api.annotations.Serialize;
import tc.oc.api.docs.virtual.UserDoc;
/**
* Wrapper for user.player_id values. It identifies a player stored in the DB,
* but contains no username, which has a few ramifications:
*
* - You cannot display this to the user
* - You cannot directly associate this with an online player
*
* Doing either of the above requires a lookup in the DB or PlayerIdMap.
* See the subclasses of this class for more explanation.
*/
@Serialize
public interface UserId extends UserDoc.Partial {
@Nonnull String player_id();
}

View File

@ -0,0 +1,7 @@
package tc.oc.api.docs;
import tc.oc.api.annotations.Serialize;
import tc.oc.api.docs.virtual.WhisperDoc;
@Serialize
public interface Whisper extends WhisperDoc.Complete {}

View File

@ -0,0 +1,28 @@
package tc.oc.api.docs;
import java.util.List;
import javax.annotation.Nonnull;
import tc.oc.api.annotations.Serialize;
import tc.oc.api.docs.virtual.Model;
import tc.oc.api.docs.virtual.PartialModel;
public interface team {
interface Partial extends PartialModel {}
@Serialize
interface Id extends Partial, Model {
@Nonnull String name();
@Nonnull String name_normalized();
}
@Serialize
interface Members extends Partial {
@Nonnull PlayerId leader();
@Nonnull List<PlayerId> members();
}
interface Team extends Id, Members {}
}

View File

@ -0,0 +1,4 @@
package tc.oc.api.docs.virtual;
public class BasicDocument implements Document {
}

View File

@ -0,0 +1,3 @@
package tc.oc.api.docs.virtual;
public interface CompetitorDoc extends Model {}

View File

@ -0,0 +1,50 @@
package tc.oc.api.docs.virtual;
import java.time.Instant;
import tc.oc.api.annotations.Serialize;
import tc.oc.api.docs.PlayerId;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
public interface DeathDoc {
interface Partial extends PartialModel {}
@Serialize
interface Complete extends Base {
@Nonnull PlayerId victim();
@Nullable PlayerId killer();
}
@Serialize
interface Creation extends Base {
@Nonnull String victim();
@Nullable String killer();
int raindrops();
}
@Serialize
interface Base extends Model, Partial {
double x();
double y();
double z();
@Nonnull String server_id();
@Nonnull String match_id();
@Nonnull String family();
@Nonnull Instant date();
@Nullable String entity_killer();
@Nullable String block_killer();
@Nullable Boolean player_killer();
@Nullable Boolean teamkill();
@Nullable String victim_class();
@Nullable String killer_class();
@Nullable Double distance();
@Nullable Boolean enchanted();
@Nullable String weapon();
@Nullable String from();
@Nullable String action();
@Nullable String cause();
}
}

View File

@ -0,0 +1,12 @@
package tc.oc.api.docs.virtual;
import java.time.Instant;
import javax.annotation.Nullable;
import tc.oc.api.annotations.Serialize;
public interface DeletableModel extends Model {
@Serialize @Nullable Instant died_at();
boolean dead();
boolean alive();
}

View File

@ -0,0 +1,23 @@
package tc.oc.api.docs.virtual;
import java.util.Map;
import tc.oc.api.annotations.Serialize;
@Serialize
public interface DeployInfo extends Document {
@Serialize
interface Version extends Document {
String branch();
String commit();
}
@Serialize
interface Nextgen extends Document {
String path();
Version version();
}
Nextgen nextgen();
Map<String, Version> packages();
}

View File

@ -0,0 +1,28 @@
package tc.oc.api.docs.virtual;
import tc.oc.api.annotations.Serialize;
import tc.oc.api.document.DocumentMeta;
import tc.oc.api.document.DocumentRegistry;
import tc.oc.api.message.Message;
import tc.oc.api.document.DocumentSerializer;
/**
* Base interface for serializable API documents, including documents stored
* in the database ({@link Model}s and {@link PartialModel}s) and directives
* exchanged through the API ({@link Message}s).
*
* {@link Document}s are serialized differently than normal objects. No fields
* are included in serialization by default. Rather, the {@link Serialize}
* annotation is used to mark serialized fields. Methods can also be annotated
* with {@link Serialize} to mark them as getters or setters. Getter methods
* must return a value and take no parameters, while setter methods must take
* exactly one parameter. If a class or interface is annotated with
* {@link Serialize}, all fields and methods declared in that class will be
* included in serialization.
*
* {@link DocumentSerializer} is responsible for serializing and deserializing
* {@link Document}s. It uses {@link DocumentRegistry} to get a {@link DocumentMeta}
* for a class, which knows about all the getters and setters. Registration
* happens on demand and is entirely automatic.
*/
public interface Document {}

View File

@ -0,0 +1,48 @@
package tc.oc.api.docs.virtual;
import javax.annotation.Nullable;
import java.time.Duration;
import java.time.Instant;
import tc.oc.api.annotations.Serialize;
import tc.oc.api.docs.SemanticVersion;
import tc.oc.api.model.ModelName;
@Serialize
@ModelName("Engagement")
public interface EngagementDoc extends Model {
String family_id();
String server_id();
String user_id();
MapDoc.Genre genre();
String match_id();
Instant match_started_at();
Instant match_joined_at();
@Nullable Instant match_finished_at();
@Nullable Duration match_length();
@Nullable Duration match_participation();
boolean committed();
String map_id();
SemanticVersion map_version();
int player_count();
int competitor_count();
@Nullable String team_pgm_id();
@Nullable Integer team_size();
@Nullable Duration team_participation();
@Nullable Integer rank();
@Nullable Integer tied_count();
enum ForfeitReason {
ABSENCE,
PARTICIPATION_PERCENT,
CUMULATIVE_ABSENCE,
CONTINUOUS_ABSENCE
}
@Nullable ForfeitReason forfeit_reason();
}

View File

@ -0,0 +1,79 @@
package tc.oc.api.docs.virtual;
import javax.annotation.Nullable;
import java.time.Duration;
import java.time.Instant;
import tc.oc.api.docs.BasicModel;
import tc.oc.api.docs.SemanticVersion;
public abstract class EngagementDocBase extends BasicModel implements EngagementDoc {
protected final MatchDoc matchDocument;
protected EngagementDocBase(String _id, MatchDoc matchDocument) {
super(_id);
this.matchDocument = matchDocument;
}
@Override
public String server_id() {
return matchDocument.server_id();
}
@Override
public String family_id() {
return matchDocument.family_id();
}
@Override
public MapDoc.Genre genre() {
return matchDocument.map().genre();
}
@Override
public String match_id() {
return matchDocument._id();
}
@Override
public Instant match_started_at() {
return matchDocument.start();
}
@Override
public @Nullable Instant match_finished_at() {
return matchDocument.end();
}
@Override
public @Nullable Duration match_length() {
Instant start = matchDocument.start();
Instant end = matchDocument.end();
if(start != null && end != null) {
return Duration.between(start, end);
} else {
return null;
}
}
@Override
public String map_id() {
return matchDocument.map()._id();
}
@Override
public SemanticVersion map_version() {
return matchDocument.map().version();
}
@Override
public int player_count() {
return matchDocument.player_count();
}
@Override
public int competitor_count() {
return matchDocument.competitors().size();
}
}

View File

@ -0,0 +1,62 @@
package tc.oc.api.docs.virtual;
import java.nio.file.Path;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import net.md_5.bungee.api.ChatColor;
import tc.oc.api.annotations.Serialize;
import tc.oc.api.docs.SemanticVersion;
import tc.oc.api.model.ModelName;
@Serialize
@Nonnull
@ModelName("Map")
public interface MapDoc extends Model {
String slug();
String name();
@Nullable String url();
@Nullable Path path();
Collection<String> images();
SemanticVersion version();
int min_players();
int max_players();
String objective();
enum Phase {
DEVELOPMENT, PRODUCTION;
public static final Phase DEFAULT = PRODUCTION;
}
Phase phase();
enum Edition {
STANDARD, RANKED, TOURNAMENT;
public static final Edition DEFAULT = STANDARD;
}
Edition edition();
enum Genre { OBJECTIVES, DEATHMATCH, OTHER }
Genre genre();
enum Gamemode { tdm, ctw, ctf, dtc, dtm, ad, koth, blitz, rage, scorebox, arcade, gs, ffa, mixed, skywars, survival }
Set<Gamemode> gamemode();
List<Team> teams();
Collection<UUID> author_uuids();
Collection<UUID> contributor_uuids();
@Serialize
interface Team extends Model {
@Nonnull String name();
// Fields below shouldn't be nullable, but data is missing in some old match documents
@Nullable Integer min_players();
@Nullable Integer max_players();
@Nullable ChatColor color();
}
}

View File

@ -0,0 +1,86 @@
package tc.oc.api.docs.virtual;
import java.util.Collection;
import java.util.Set;
import javax.annotation.Nullable;
import java.time.Instant;
import tc.oc.api.annotations.Serialize;
import tc.oc.api.model.ModelName;
@Serialize
@ModelName(value = "Match", plural = "matches")
public interface MatchDoc extends Model {
String server_id();
String family_id();
// Can be null if doc was generated by the API and the map is not in the DB
MapDoc map();
Collection<? extends Team> competitors();
Collection<? extends Goal> objectives();
Instant load();
@Nullable Instant start();
@Nullable Instant end();
@Nullable Instant unload();
boolean join_mid_match();
int player_count();
Collection<String> winning_team_ids();
Collection<String> winning_user_ids();
enum Mutation {
BLITZ, UHC, EXPLOSIVES, NO_FALL, MOBS, STRENGTH, DOUBLE_JUMP, INVISIBILITY, LIGHTNING, RAGE, ELYTRA;
}
Set<Mutation> mutations();
@Serialize
interface Team extends MapDoc.Team, CompetitorDoc {
@Nullable Integer size(); // Shouldn't be nullable, but data is missing in some old match documents
String league_team_id();
}
@Serialize
interface Goal extends Model {
String type();
String name();
}
@Serialize
interface OwnedGoal extends Goal {
@Nullable String owner_id();
@Nullable String owner_name();
}
@Serialize
interface IncrementalGoal extends Goal {
double completion();
}
@Serialize
interface TouchableGoal extends OwnedGoal {
Collection<? extends Proximity> proximities();
@Serialize
interface Proximity extends Model {
boolean touched();
@Nullable Metric metric();
double distance();
enum Metric {
CLOSEST_PLAYER, CLOSEST_PLAYER_HORIZONTAL,
CLOSEST_BLOCK, CLOSEST_BLOCK_HORIZONTAL,
CLOSEST_KILL, CLOSEST_KILL_HORIZONTAL;
}
}
}
@Serialize
interface Destroyable extends TouchableGoal, IncrementalGoal {
int total_blocks();
int breaks_required();
int breaks();
}
}

View File

@ -0,0 +1,9 @@
package tc.oc.api.docs.virtual;
import tc.oc.api.annotations.Serialize;
@Serialize
public interface Model extends PartialModel {
String _id();
@Serialize(false) default String toShortString() { return toString(); }
}

View File

@ -0,0 +1,37 @@
package tc.oc.api.docs.virtual;
/**
* A subset of the fields in some {@link Model} serving some particular use case.
*
* ### How the model hiearchy works ###
*
* PartialModel ---------> Thing.Partial
* | |
* | +------+------+
* | | |
* | v v
* | Thing.FooInfo Thing.BarInfo
* | | |
* | +------+------+
* | |
* | v
* Model ------------> Thing.Complete
*
* The hiearchy for a model called Thing would start with an empty interface
* called Thing.Partial that extends {@link PartialModel}. For every sub-group
* of Thing fields that are sent/received together, there would be an interface
* extending Thing.Partial that declares getter methods for those fields only.
*
* At the bottom of the hiearchy would be an interface called Thing.Complete
* that extends {@link Model} and ALL of the Thing parts. Any fields that are not
* declared in any of the parts should be declared in the complete model.
*
* Fields can be shared among different parts of the same model, as long as they
* have identical signatures in all of the parts where they appear.
*
* This system ties together all the parts of a model in a type-safe way, ensuring
* that the fields stay in sync. Model classes should be created for specific cases,
* and only implement the interfaces containing the fields that they use, avoiding
* empty fields and nulls.
*/
public interface PartialModel extends Document {}

View File

@ -0,0 +1,68 @@
package tc.oc.api.docs.virtual;
import tc.oc.api.annotations.Serialize;
import tc.oc.api.docs.PlayerId;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.time.Instant;
public interface PunishmentDoc {
interface Partial extends PartialModel {}
@Serialize
interface Base extends Model, Partial {
@Nullable String match_id();
@Nullable String server_id();
@Nullable Instant expire();
@Nullable String family();
@Nonnull String reason();
@Nonnull Instant date();
boolean debatable();
boolean silent();
boolean automatic();
boolean active();
}
@Serialize
interface Creation extends Base {
@Nullable String punisher_id();
@Nullable String punished_id();
@Nullable Type type();
boolean off_record();
}
@Serialize
interface Complete extends Base, Enforce, Evidence {
@Nullable PlayerId punisher();
@Nullable PlayerId punished();
@Nonnull Type type();
boolean stale();
}
@Serialize
interface Enforce extends Partial {
boolean enforced();
}
@Serialize
interface Evidence extends Partial {
@Nullable String evidence();
}
enum Type {
WARN,
KICK,
BAN,
FORUM_WARN,
FORUM_BAN,
TOURNEY_BAN,
UNKNOWN;
public String permission() {
return name().toLowerCase();
}
}
}

View File

@ -0,0 +1,37 @@
package tc.oc.api.docs.virtual;
import java.util.List;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.time.Instant;
import tc.oc.api.annotations.Serialize;
import tc.oc.api.docs.PlayerId;
public interface ReportDoc {
interface Partial extends PartialModel {}
@Serialize
interface Base extends Model, Partial {
@Nonnull String scope();
boolean automatic();
@Nullable String family();
@Nullable String server_id();
@Nullable String match_id();
@Nonnull String reason();
@Nullable List<String> staff_online();
}
@Serialize
interface Creation extends Base {
@Nullable String reporter_id();
@Nonnull String reported_id();
}
@Serialize
interface Complete extends Base {
@Nonnull Instant created_at();
@Nullable PlayerId reporter();
@Nullable PlayerId reported();
}
}

View File

@ -0,0 +1,194 @@
package tc.oc.api.docs.virtual;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import javax.annotation.Nullable;
import java.time.Instant;
import tc.oc.api.annotations.Serialize;
import tc.oc.api.docs.team.Team;
public interface ServerDoc {
interface Complete extends DeletableModel, Listing, Startup, Configuration, Bungee, Restart {
@Override
default String toShortString() {
return bungee_name();
}
}
interface Partial extends PartialModel {}
/**
* Info displayed in server listings (signs, picker, etc)
*/
interface Listing extends Identity, Visible, Status, RestartQueuedAt, Mutation {}
enum Role {
BUNGEE, LOBBY, PGM, MAPDEV;
}
enum Network {
PUBLIC, PRIVATE, TOURNAMENT;
}
/**
* Things that are available in the config file i.e. that can't change dynamically
*/
@Serialize
interface Deployment extends Partial {
String datacenter();
String box();
Role role();
}
@Serialize
interface BungeeName extends Partial {
@Nullable String bungee_name();
}
@Serialize
interface CurrentPort extends Partial {
Integer current_port();
}
@Serialize
interface Online extends Partial {
boolean online();
}
@Serialize
interface Dns extends Partial {
boolean dns_enabled();
@Nullable Instant dns_toggled_at();
}
@Serialize
interface Identity extends DeletableModel, BungeeName, Deployment {
int priority();
@Nullable String family();
String ip();
String name();
@Nullable String description();
Network network();
Set<String> realms();
@Nullable String game_id();
@Nullable String arena_id();
default String slug() {
return role() == Role.BUNGEE ? name() : bungee_name();
}
}
enum Visibility {
UNKNOWN, PUBLIC, PRIVATE, UNLISTED;
}
@Serialize
interface Visible extends Partial {
Visibility visibility();
}
/**
* Startup info sent to the API
*/
@Serialize
interface Startup extends Online, CurrentPort {
@Nullable DeployInfo deploy_info();
Map<String, String> plugin_versions();
Set<Integer> protocol_versions();
}
/**
* Startup info received from the API
*/
@Serialize
interface Configuration extends Partial {
String settings_profile();
Map<UUID, String> operators();
@Nullable Team team();
Set<UUID> participant_uuids();
Map<String, Boolean> participant_permissions();
Map<String, Boolean> observer_permissions();
Map<String, Boolean> mapmaker_permissions();
Visibility startup_visibility();
boolean whitelist_enabled();
boolean waiting_room();
@Nullable String resource_pack_url();
@Nullable String resource_pack_sha1();
boolean resource_pack_fast_update();
}
@Serialize
interface PlayerCounts extends Partial {
default int min_players() { return 0; }
int max_players();
int num_observing();
}
@Serialize
interface MatchStatus extends Partial {
@Nullable MatchDoc current_match();
int num_participating();
@Nullable MapDoc next_map();
}
@Serialize
interface Mutation extends Partial {
Set<MatchDoc.Mutation> queued_mutations();
}
/**
* Status sent to the API from Lobby
*/
@Serialize
interface StatusUpdate extends PlayerCounts {}
/**
* Status sent to the API from PGM
*/
@Serialize
interface MatchStatusUpdate extends StatusUpdate, MatchStatus {}
/**
* Status received from the API
*/
@Serialize
interface Status extends MatchStatusUpdate {
boolean running();
boolean online();
int num_online();
}
@Serialize
interface RestartQueuedAt extends Partial {
@Nullable Instant restart_queued_at();
}
@Serialize
interface Restart extends RestartQueuedAt {
interface Priority {
int LOW = -10;
int NORMAL = 0;
int HIGH = 10;
}
@Nullable String restart_reason();
default int restart_priority() { return 0; }
}
@Serialize
interface Bungee extends Dns {
Map<UUID, String> fake_usernames();
List<Banner> banners();
}
@Serialize
interface Banner extends Document {
String rendered();
float weight();
}
}

View File

@ -0,0 +1,24 @@
package tc.oc.api.docs.virtual;
import java.time.Instant;
import javax.annotation.Nullable;
import tc.oc.api.annotations.Serialize;
import tc.oc.api.docs.PlayerId;
public interface SessionDoc {
interface Partial extends PartialModel {}
@Serialize
interface Complete extends Model, Partial {
String family_id();
String server_id();
PlayerId user();
@Nullable String nickname();
@Nullable String nickname_lower();
String ip();
Instant start();
@Nullable Instant end();
}
}

View File

@ -0,0 +1,113 @@
package tc.oc.api.docs.virtual;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.time.Instant;
import tc.oc.api.annotations.Serialize;
import tc.oc.api.docs.PlayerId;
public interface UserDoc {
interface Partial extends PartialModel {}
@Serialize
interface Nickname extends Partial {
@Nullable String nickname();
}
@Serialize
interface Locale extends Partial {
@Nullable String mc_locale();
}
class Flair {
public String realm;
public String text;
public int priority;
}
@Serialize
interface Identity extends PlayerId, Nickname {
@Nonnull UUID uuid();
@Nonnull List<Flair> minecraft_flair();
}
@Serialize
interface Trophies extends Partial {
List<String> trophy_ids();
}
interface License {
@Serialize
interface Kill extends Document {
@Nonnull String victim_id();
boolean friendly();
}
@Serialize
interface Stats extends Partial {
@Nonnull List<Kill> tnt_license_kills();
}
interface Request extends Stats {
@Serialize @Nullable Instant requested_tnt_license_at();
default boolean requestedTntLicense() {
return requested_tnt_license_at() != null;
}
}
interface Grant extends Stats {
@Serialize @Nullable Instant granted_tnt_license_at();
default boolean hasTntLicense() {
return granted_tnt_license_at() != null;
}
}
interface Complete extends Request, Grant {}
}
/**
* Stuff we get from the API on login, and keep around for plugins to use
*/
@Serialize
interface Login extends Identity, Locale, Trophies, License.Complete {
int raindrops();
String mc_last_sign_in_ip();
@Nullable Date trial_expires_at();
Map<String, Map<String, Boolean>> mc_permissions_by_realm();
Map<String, Map<String, String>> mc_settings_by_profile();
Map<String, String> classes();
Set<PlayerId> friends();
Map<String, List<Instant>> recent_match_joins_by_family_id(); // Reverse-chronological order
int enemy_kills();
}
/**
* Stuff we learn from the client on login, and report to the API
*/
@Serialize
interface ClientDetails extends Partial {
String mc_client_version();
String skin_blob(); // Base64 encoded thing returned from Skin#getData()
}
enum ResourcePackStatus {
// MUST match org.bukit.ResourcePackStatus
ACCEPTED, DECLINED, LOADED, FAILED
}
@Serialize
interface ResourcePackResponse extends Partial {
UserDoc.ResourcePackStatus resource_pack_status();
}
}

View File

@ -0,0 +1,29 @@
package tc.oc.api.docs.virtual;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.time.Instant;
import tc.oc.api.annotations.Serialize;
import tc.oc.api.docs.PlayerId;
public interface WhisperDoc {
interface Partial extends PartialModel {}
@Serialize
interface Complete extends Delivery, Model {
@Nonnull String family();
@Nonnull String server_id();
@Nonnull Instant sent();
@Nonnull PlayerId sender_uid();
@Nullable String sender_nickname();
@Nonnull PlayerId recipient_uid();
@Nullable String recipient_specified();
@Nonnull String content();
}
@Serialize
interface Delivery extends Partial, Model {
boolean delivered();
}
}

View File

@ -0,0 +1,84 @@
package tc.oc.api.document;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Member;
import java.lang.reflect.Type;
import javax.annotation.Nullable;
import com.google.common.reflect.TypeToken;
import tc.oc.api.docs.virtual.Document;
/**
* Metadata for an accessor of a {@link Document} property of type {@link T}.
* This object wraps a particular field or method declared in a particular
* class. A property can have multiple accessors e.g. if they override each other.
*/
public interface Accessor<T> {
/**
* Metadata of the {@link Document} that declares this property
*/
DocumentMeta<?> document();
/**
* Serialized name of the property
*/
String name();
/**
* Generic type of the property
*/
Type type();
boolean isPrimitive();
/**
* Type of the property in the context of the given document type.
* If this property type depends on type parameters declared on the
* document, they will be resolved using the actual type arguments
* of the given document type.
*
* For example, if a document is declared {@code Doc<T>} and it has a
* property {@code List<T> things;}, then calling this method on the
* things accessor with argument {@code Doc<String>} would return
* {@code List<String>}.
*/
Type resolvedType(Type documentType);
Type resolvedType(TypeToken documentType);
/**
* Raw type of the property
*/
Class<T> rawType();
Class<T> boxType();
/**
* Java reflection API handle for the wrapped accessor
*/
<M extends AccessibleObject & Member> M member();
/**
* Accessor for the same property from an ancestor document that this accessor overrides.
*/
@Nullable Accessor<?> override();
/**
* Is the property allowed to be null? This is determined from @Nullable and @Nonnull
* annotations on the wrapped member. Overrides inherit the nullability of their parent,
* and can override it with their own annotation. Nulls are allowed by default if no
* annotations are present in the property ancestry.
*/
boolean isNullable();
/**
* Is this accessor implemented in the given class? This is true only if
* the wrapped member exists on the given class, and is not an abstract method.
*/
boolean isImplemented(Class<?> type);
boolean hasDefault();
T validate(T value);
}

View File

@ -0,0 +1,116 @@
package tc.oc.api.document;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Member;
import java.lang.reflect.Type;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import com.google.common.reflect.TypeToken;
import tc.oc.api.docs.virtual.Document;
import tc.oc.commons.core.reflect.Types;
import static com.google.common.base.Preconditions.checkNotNull;
public abstract class BaseAccessor<T> implements Accessor<T> {
private final DocumentRegistry registry;
private DocumentMeta<?> document;
private @Nullable Accessor<?> override;
protected BaseAccessor(DocumentRegistry registry) {
this.registry = checkNotNull(registry);
}
@Override
public DocumentMeta<?> document() {
if(document == null) {
this.document = registry.getMeta((Class<? extends Document>) member().getDeclaringClass());
Accessor<?> override = null;
for(DocumentMeta<?> ancestor : document.ancestors()) {
if(ancestor != document) {
override = getOverrideIn(ancestor);
if(override != null) break;
}
}
this.override = override;
}
return document;
}
@Override
public String name() {
return member().getName();
}
@Override
public Type resolvedType(Type documentType) {
return resolvedType(TypeToken.of(documentType));
}
@Override
public Type resolvedType(TypeToken documentType) {
return documentType.resolveType(type()).getType();
}
@Override
public @Nullable Accessor<?> override() {
document();
return override;
}
protected abstract @Nullable Accessor<?> getOverrideIn(DocumentMeta<?> ancestor);
@Override
public boolean isPrimitive() {
final Type type = type();
return type instanceof Class && ((Class) type).isPrimitive();
}
@Override public Class<T> boxType() {
return isPrimitive() ? Types.box(rawType())
: rawType();
}
@Override
public boolean isNullable() {
if(isPrimitive()) return false;
{
final AccessibleObject member = member();
if(member.getAnnotation(Nullable.class) != null) return true;
if(member.getAnnotation(Nonnull.class) != null) return false;
}
{
final Member member = member();
if(member.getDeclaringClass().getAnnotation(Nullable.class) != null) return true;
if(member.getDeclaringClass().getAnnotation(Nonnull.class) != null) return false;
}
if(override() != null) return override().isNullable();
return true;
}
@Override
public boolean hasDefault() {
return isNullable() || isImplemented(document().type()) || isImplemented(document().baseType());
}
@Override
public T validate(T value) {
if(value == null) {
if(!isNullable()) {
throw new NullPointerException("null value for non-nullable property " + name());
}
} else {
if(!boxType().isInstance(value)) {
throw new ClassCastException("value of type " + value.getClass().getName() +
" is not assignable to property " + name() + " of type " + rawType().getName());
}
}
return value;
}
}

View File

@ -0,0 +1,9 @@
package tc.oc.api.document;
import java.util.Map;
import tc.oc.api.docs.virtual.Document;
public interface DocumentGenerator {
<T extends Document> T instantiate(DocumentMeta<T> meta, Document base, Map<String, Object> data);
}

View File

@ -0,0 +1,101 @@
package tc.oc.api.document;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import tc.oc.api.annotations.Serialize;
import tc.oc.api.docs.virtual.Document;
/**
* Meta-info about a {@link Document} subtype, including all property {@link Accessor}s.
*/
public class DocumentMeta<T extends Document> {
private final Class<T> type;
private final ImmutableList<DocumentMeta<? super T>> ancestors;
private final Class<? extends Document> baseType;
private final ImmutableMap<String, Getter> getters;
private final ImmutableMap<String, Setter> setters;
public DocumentMeta(Class<T> type, List<DocumentMeta<? super T>> ancestors, Class<? extends Document> baseType, Map<String, Getter> getters, Map<String, Setter> setters) {
this.type = type;
this.ancestors = ImmutableList.copyOf(Iterables.concat(Collections.singleton(this), ancestors));
this.baseType = baseType;
this.getters = ImmutableMap.copyOf(getters);
this.setters = ImmutableMap.copyOf(setters);
}
@Override
public String toString() {
return getClass().getSimpleName() + "<" + type() + ">";
}
/**
* The {@link Document} subtype described by this metadata
*/
public Class<T> type() {
return type;
}
/**
* All ancestor {@link Document}s of this document, in resolution order.
* This list includes every supertype of this document that is also a subtype
* of {@link Document}, including this document itself.
*
* This list can include both classes and interfaces, which are flattened into
* a single list using the C3 linearization algorithm. Classes always come before
* interfaces in the list.
*/
public ImmutableList<DocumentMeta<? super T>> ancestors() {
return ancestors;
}
/**
* Best concrete base class to extend when implementing this document
* (used by the document generator)
*/
public Class<? extends Document> baseType() {
return baseType;
}
/**
* All property getters visible on this document type,
* including inherited getters that are not overridden.
*/
public ImmutableMap<String, Getter> getters() {
return getters;
}
/**
* All property setters visible on this document type,
* including inherited setters that are not overridden.
*/
public ImmutableMap<String, Setter> setters() {
return setters;
}
private static boolean isSerialized(AnnotatedElement element, boolean def) {
final Serialize annotation = element.getAnnotation(Serialize.class);
return annotation != null ? annotation.value() : def;
}
public static <T extends AnnotatedElement> Iterable<T> serializedMembers(Class<? extends Document> type, Iterable<T> members) {
final boolean def = isSerialized(type, false);
return Iterables.filter(members, member -> isSerialized(member, def));
}
public static Iterable<Method> serializedMethods(Class<? extends Document> type) {
return serializedMembers(type, Arrays.asList(type.getDeclaredMethods()));
}
public static Iterable<Field> serializedFields(Class<? extends Document> type) {
return serializedMembers(type, Arrays.asList(type.getDeclaredFields()));
}
}

View File

@ -0,0 +1,271 @@
package tc.oc.api.document;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.inject.Inject;
import javax.inject.Singleton;
import com.google.api.client.repackaged.com.google.common.base.Joiner;
import com.google.common.base.Function;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.gson.annotations.SerializedName;
import com.google.inject.Injector;
import tc.oc.api.docs.BasicDeletableModel;
import tc.oc.api.docs.BasicModel;
import tc.oc.api.docs.PlayerId;
import tc.oc.api.docs.SimplePlayerId;
import tc.oc.api.docs.SimpleUserId;
import tc.oc.api.docs.UserId;
import tc.oc.api.docs.virtual.BasicDocument;
import tc.oc.api.docs.virtual.DeletableModel;
import tc.oc.api.docs.virtual.Document;
import tc.oc.api.docs.virtual.Model;
import tc.oc.api.exceptions.SerializationException;
import tc.oc.commons.core.logging.Loggers;
import tc.oc.commons.core.reflect.Types;
import tc.oc.commons.core.util.C3;
import tc.oc.commons.core.util.MapUtils;
/**
* Cache of {@link DocumentMeta} records, populated on-demand when a {@link Document}
* is serialized or deserialized.
*/
@Singleton
public class DocumentRegistry {
protected final Logger logger;
protected final DocumentGenerator generator;
protected final Injector injector;
private final LoadingCache<Class<? extends Document>, DocumentMeta> cache = CacheBuilder.newBuilder().build(
new CacheLoader<Class<? extends Document>, DocumentMeta>() {
@Override
public DocumentMeta load(Class<? extends Document> type) throws Exception {
return register(type);
}
}
);
@Inject DocumentRegistry(Loggers loggers, DocumentGenerator generator, Injector injector) {
this.generator = generator;
this.injector = injector;
this.logger = loggers.get(getClass());
}
/**
* Is the given document type directly instantiable? This is true only
* if the type is a non-abstract class with a default constructor.
*/
public boolean isInstantiable(Class<? extends Document> type) {
if(type.isInterface() || Modifier.isAbstract(type.getModifiers())) return false;
try {
type.getDeclaredConstructor();
return true;
} catch(NoSuchMethodException e) {
return false;
}
}
public <T extends Document> T instantiate(Class<T> type, Map<String, Object> data) {
return instantiate(getMeta(type), data);
}
public <T extends Document> T instantiate(DocumentMeta<T> meta, Map<String, Object> data) {
if(meta.type().isInterface()) {
// To create an interface document, choose the best base type,
// instantiate that, and then wrap it in a proxy that implements
// the rest of the properties.
return generator.instantiate(meta, instantiate(getMeta(meta.baseType()), data), data);
} else if(isInstantiable(meta.type())) {
// If document type is directly instantiable, get an instance
// from the injector and use setters to initialize it.
final T doc = injector.getInstance(meta.type());
for(Map.Entry<String, Setter> entry : meta.setters().entrySet()) {
if(data.containsKey(entry.getKey())) {
entry.getValue().setUnchecked(doc, data.get(entry.getKey()));
}
}
return doc;
} else {
throw new SerializationException("Document type " + meta.type().getName() + " is not instantiable");
}
}
public <T extends Document> T copy(T original) {
final DocumentMeta<T> meta = getMeta((Class<T>) original.getClass());
return instantiate(meta, ImmutableMap.copyOf(Maps.transformValues(meta.getters(), getter -> getter.get(original))));
}
/**
* Get (or create) the metadata for the given {@link Document} type.
*/
public <T extends Document> DocumentMeta<T> getMeta(Class<T> type) {
return cache.getUnchecked(type);
}
private <T extends Document> DocumentMeta<T> register(final Class<T> type) {
logger.fine("Registering serializable type " + type);
// Find property accessors declared directly on the given document
final Map<String, Getter> getters = new HashMap<>();
final Map<String, Setter> setters = new HashMap<>();
for(Method method : DocumentMeta.serializedMethods(type)) {
registerMethod(getters, setters, method);
}
for(Field field : DocumentMeta.serializedFields(type)) {
registerField(getters, setters, field);
}
// Find the immediate supertypes of the document
final List<DocumentMeta<? super T>> parents = new ArrayList<>();
for(Class<? super T> parent : Types.parents(type)) {
if(Document.class.isAssignableFrom(parent)) {
parents.add((DocumentMeta<? super T>) getMeta(parent.asSubclass(Document.class)));
}
}
// Merge all ancestors into a single list
final List<DocumentMeta<? super T>> ancestors = ImmutableList.copyOf(
C3.merge(
Lists.transform(
parents,
(Function<DocumentMeta<? super T>, Collection<? extends DocumentMeta<? super T>>>) DocumentMeta::ancestors
)
)
);
if(logger.isLoggable(Level.FINE)) {
logger.fine("Linearized ancestors for " + type + ": " + Joiner.on(", ").join(ancestors));
}
// Copy inherited accessors from ancestor documents
for(DocumentMeta<? super T> ancestor : ancestors) {
MapUtils.putAbsent(getters, ancestor.getters());
MapUtils.putAbsent(setters, ancestor.setters());
}
return new DocumentMeta<>(type, ancestors, bestBaseClass(type), getters, setters);
}
private static String serializedName(Member member) {
if(member instanceof AnnotatedElement) {
SerializedName nameAnnot = ((AnnotatedElement) member).getAnnotation(SerializedName.class);
if(nameAnnot != null) return nameAnnot.value();
}
return member.getName();
}
private static @Nullable Type getterType(Method method) {
if(method.getGenericParameterTypes().length == 0 && method.getGenericReturnType() != Void.TYPE) {
return method.getGenericReturnType();
}
return null;
}
private static @Nullable Type setterType(Method method) {
if(method.getGenericParameterTypes().length == 1) {
return method.getGenericParameterTypes()[0];
}
return null;
}
private void registerMethod(Map<String, Getter> getters, Map<String, Setter> setters, Method method) {
if(Modifier.isStatic(method.getModifiers())) return;
final String name = serializedName(method);
boolean accessor = false;
if(getterType(method) != null) {
accessor = true;
if(!getters.containsKey(name)) {
if(logger.isLoggable(Level.FINE)) {
logger.fine(" " + name + " -- get --> " + method);
}
getters.put(name, new GetterMethod(this, method));
}
}
if(setterType(method) != null) {
accessor = true;
if(!setters.containsKey(name)) {
if(logger.isLoggable(Level.FINE)) {
logger.fine(" " + name + " -- set --> " + method);
}
setters.put(name, new SetterMethod(this, method));
}
}
if(!accessor) {
throw new SerializationException("Serialized method " + method + " is not a valid getter or setter");
}
}
private void registerField(Map<String, Getter> getters, Map<String, Setter> setters, Field field) {
if(Modifier.isTransient(field.getModifiers()) ||
Modifier.isStatic(field.getModifiers()) ||
field.isSynthetic() ||
field.isEnumConstant()) return;
final String name = serializedName(field);
final boolean gettable = !getters.containsKey(name);
final boolean settable = !setters.containsKey(name);
if(gettable || settable) {
if(logger.isLoggable(Level.FINE)) {
String access;
if(gettable && settable) {
access = "get/set";
} else if(gettable) {
access = "get";
} else {
access = "set";
}
logger.fine(" " + name + " -- " + access + " --> " + field);
}
if(gettable) {
getters.put(name, new FieldGetter(this, field));
}
if(settable) {
setters.put(name, new FieldSetter(this, field));
}
}
}
// TODO: This could could be done in a more general way i.e. search
// the registry for the best existing implementation to inherit from.
public Class<? extends Document> bestBaseClass(Class<? extends Document> type) {
if(PlayerId.class.isAssignableFrom(type)) {
return SimplePlayerId.class;
} else if(UserId.class.isAssignableFrom(type)) {
return SimpleUserId.class;
} else if(DeletableModel.class.isAssignableFrom(type)) {
return BasicDeletableModel.class;
} else if(Model.class.isAssignableFrom(type)) {
return BasicModel.class;
} else {
return BasicDocument.class;
}
}
}

View File

@ -0,0 +1,104 @@
package tc.oc.api.document;
import java.lang.reflect.Type;
import java.util.Map;
import java.util.logging.Logger;
import javax.inject.Inject;
import javax.inject.Singleton;
import com.google.common.collect.ImmutableMap;
import com.google.common.reflect.TypeToken;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import com.google.gson.JsonSyntaxException;
import com.google.inject.CreationException;
import com.google.inject.ProvisionException;
import tc.oc.api.docs.virtual.Document;
import tc.oc.api.document.DocumentMeta;
import tc.oc.api.document.DocumentRegistry;
import tc.oc.api.document.Getter;
import tc.oc.api.exceptions.SerializationException;
import tc.oc.commons.core.logging.Loggers;
@Singleton
public class DocumentSerializer implements JsonSerializer<Document>, JsonDeserializer<Document> {
protected final Logger logger;
protected final DocumentRegistry documentRegistry;
@Inject DocumentSerializer(Loggers loggers, DocumentRegistry documentRegistry) {
this.logger = loggers.get(getClass());
this.documentRegistry = documentRegistry;
}
@Override
public JsonElement serialize(Document document, Type documentType, JsonSerializationContext context) {
final JsonObject documentJson = new JsonObject();
final DocumentMeta<?> documentMeta = documentRegistry.getMeta(document.getClass());
final TypeToken documentTypeToken = TypeToken.of(documentType);
for(Map.Entry<String, Getter> entry : documentMeta.getters().entrySet()) {
final String name = entry.getKey();
final Getter getter = entry.getValue();
final Type resolvedType = getter.resolvedType(documentTypeToken);
final Object value;
try {
value = getter.get(document);
} catch(Exception e) {
throw new SerializationException("Exception reading property " + resolvedType + " " + documentType + "." + name, e);
}
try {
documentJson.add(name, context.serialize(value, resolvedType));
} catch(Exception e) {
throw new SerializationException("Exception serializing property " + resolvedType + " " + documentType + "." + name + " = " + value, e);
}
}
return documentJson;
}
@Override
public Document deserialize(JsonElement rawJson, Type documentType, JsonDeserializationContext context) throws JsonParseException {
final TypeToken documentTypeToken = TypeToken.of(documentType);
final DocumentMeta<?> documentMeta = documentRegistry.getMeta(documentTypeToken.getRawType());
final Class<? extends Document> instantiableType = documentMeta.type();
if(!(rawJson instanceof JsonObject)) {
throw new JsonSyntaxException("Expected JSON object while deserializing " + instantiableType.getName() + ", not " + rawJson.getClass().getSimpleName());
}
final JsonObject documentJson = (JsonObject) rawJson;
final ImmutableMap.Builder<String, Object> builder = ImmutableMap.builder();
for(Map.Entry<String, Getter> entry : documentMeta.getters().entrySet()) {
final String name = entry.getKey();
final Getter getter = entry.getValue();
final Type resolvedType = getter.resolvedType(documentTypeToken);
final JsonElement propertyJson = documentJson.get(name);
try {
if(propertyJson == null || propertyJson.isJsonNull()) {
if(!getter.isNullable()) {
throw new NullPointerException("Missing value for non-nullable property " + name);
}
} else {
builder.put(name, context.deserialize(propertyJson, resolvedType));
}
} catch(Exception e) {
throw new SerializationException("Exception deserializing property " + resolvedType + " " + documentType + "." + name + " = " + propertyJson, e);
}
}
try {
return documentRegistry.instantiate(documentMeta, builder.build());
} catch(ProvisionException | CreationException e) {
throw new SerializationException("Exception instantiating document " + instantiableType.getName(), e);
}
}
}

View File

@ -0,0 +1,19 @@
package tc.oc.api.document;
import tc.oc.api.docs.virtual.Document;
import tc.oc.api.serialization.GsonBinder;
import tc.oc.commons.core.inject.HybridManifest;
public class DocumentsManifest extends HybridManifest {
@Override
protected void configure() {
bindAndExpose(DocumentSerializer.class);
bind(DocumentRegistry.class);
bind(DocumentGenerator.class).to(ProxyDocumentGenerator.class);
new GsonBinder(publicBinder())
.bindHiearchySerializer(Document.class)
.to(DocumentSerializer.class);
}
}

View File

@ -0,0 +1,35 @@
package tc.oc.api.document;
import java.lang.reflect.Field;
import java.lang.reflect.Type;
public abstract class FieldAccessor<T> extends BaseAccessor<T> {
private final Field field;
public FieldAccessor(DocumentRegistry registry, Field field) {
super(registry);
this.field = field;
field.setAccessible(true);
}
@Override
public Type type() {
return field.getGenericType();
}
@Override
public Class<T> rawType() {
return (Class<T>) field.getType();
}
@Override
public Field member() {
return field;
}
@Override
public boolean isImplemented(Class<?> type) {
return field.getDeclaringClass().isAssignableFrom(type);
}
}

View File

@ -0,0 +1,30 @@
package tc.oc.api.document;
import java.lang.reflect.Field;
import javax.annotation.Nullable;
import tc.oc.commons.core.util.ExceptionUtils;
/**
* Property getter that wraps a field
*/
public class FieldGetter<T> extends FieldAccessor<T> implements Getter<T> {
public FieldGetter(DocumentRegistry registry, Field field) {
super(registry, field);
}
@Override
protected @Nullable Accessor<?> getOverrideIn(DocumentMeta<?> ancestor) {
return ancestor.getters().get(name());
}
@Override
public T get(Object obj) {
try {
return validate((T) member().get(obj));
} catch(IllegalAccessException e) {
throw ExceptionUtils.propagate(e);
}
}
}

View File

@ -0,0 +1,40 @@
package tc.oc.api.document;
import java.lang.reflect.Field;
import java.util.concurrent.ExecutionException;
import javax.annotation.Nullable;
import com.google.common.util.concurrent.UncheckedExecutionException;
/**
* Property setter that wraps a field
*/
public class FieldSetter<T> extends FieldAccessor<T> implements Setter<T> {
public FieldSetter(DocumentRegistry registry, Field field) {
super(registry, field);
}
@Override
protected @Nullable Accessor<?> getOverrideIn(DocumentMeta<?> ancestor) {
return ancestor.setters().get(name());
}
@Override
public void setUnchecked(Object obj, T value) {
try {
set(obj, value);
} catch(ExecutionException e) {
throw new UncheckedExecutionException(e.getCause());
}
}
@Override
public void set(Object obj, T value) throws ExecutionException {
try {
member().set(obj, validate(value));
} catch(IllegalAccessException e) {
throw new ExecutionException(e);
}
}
}

View File

@ -0,0 +1,18 @@
package tc.oc.api.document;
import java.util.concurrent.ExecutionException;
import tc.oc.api.docs.virtual.Document;
/**
* Metadata for an accessor that can read the value of a {@link Document} property.
*/
public interface Getter<T> extends Accessor<T> {
/**
* Get the value of this property on the given document.
* @throws ExecutionException if a checked exception was thrown while trying to read the value,
* or if the property is not accessible on the given object.
*/
T get(Object obj);
}

View File

@ -0,0 +1,62 @@
package tc.oc.api.document;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import javax.annotation.Nullable;
import tc.oc.api.docs.virtual.Document;
import tc.oc.commons.core.reflect.Methods;
import tc.oc.commons.core.util.ExceptionUtils;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* Metadata for a getter method of a {@link Document} property.
*
* The wrapped method takes no parameters and returns the value of the property.
*/
public class GetterMethod<T> extends BaseAccessor<T> implements Getter<T> {
private final Method method;
public GetterMethod(DocumentRegistry registry, Method method) {
super(registry);
this.method = checkNotNull(method);
method.setAccessible(true);
}
@Override
public Method member() {
return method;
}
@Override
public Type type() {
return method.getGenericReturnType();
}
@Override
public Class<T> rawType() {
return (Class<T>) method.getReturnType();
}
@Override
protected @Nullable Accessor<?> getOverrideIn(DocumentMeta<?> ancestor) {
return ancestor.getters().get(name());
}
@Override
public boolean isImplemented(Class<?> type) {
return Methods.respondsTo(type, method);
}
@Override
public T get(Object obj) {
try {
return validate((T) member().invoke(obj));
} catch(IllegalAccessException | InvocationTargetException e) {
throw ExceptionUtils.propagate(e);
}
}
}

View File

@ -0,0 +1,134 @@
package tc.oc.api.document;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Stream;
import com.google.common.cache.LoadingCache;
import tc.oc.api.docs.virtual.Document;
import tc.oc.api.docs.virtual.Model;
import tc.oc.api.exceptions.SerializationException;
import tc.oc.commons.core.inspect.Inspectable;
import tc.oc.commons.core.inspect.InspectableProperty;
import tc.oc.commons.core.reflect.MethodHandleUtils;
import tc.oc.commons.core.reflect.Methods;
import tc.oc.commons.core.stream.BiStream;
import tc.oc.commons.core.util.CacheUtils;
public class ProxyDocumentGenerator implements DocumentGenerator {
@Override
public <T extends Document> T instantiate(DocumentMeta<T> meta, Document base, Map<String, Object> data) {
// Validate data before creating the document
for(Map.Entry<String, Getter> entry : meta.getters().entrySet()) {
final String name = entry.getKey();
final Getter getter = entry.getValue();
if(data.containsKey(name)) {
getter.validate(data.get(name));
} else if(!getter.hasDefault()) {
throw new IllegalArgumentException("Missing value for required property " + name);
}
}
return new Invoker<>(meta, base, data).proxy;
}
private static final Method Object_toString = Methods.declaredMethod(Object.class, "toString");
private static final Method Inspectable_inspect = Methods.declaredMethod(Inspectable.class, "inspect");
private class Invoker<T extends Document> implements InvocationHandler, Inspectable {
final DocumentMeta<T> meta;
final Map<String, Object> data;
final LoadingCache<Method, MethodHandle> handles;
final T proxy;
Invoker(DocumentMeta<T> meta, Document base, Map<String, Object> data) {
this.meta = meta;
this.data = data;
this.proxy = (T) Proxy.newProxyInstance(meta.type().getClassLoader(), new Class[]{meta.type(), Inspectable.class}, this);
this.handles = CacheUtils.newCache(method -> {
if(method.getDeclaringClass().isAssignableFrom(Inspectable.class) &&
!method.getDeclaringClass().isAssignableFrom(Object.class)) {
// Send Inspectable methods to this
return MethodHandles.lookup()
.unreflect(method)
.bindTo(this);
} else if(method.equals(Object_toString)) {
// Send toString to this.inspect()
return MethodHandles.lookup()
.unreflect(Inspectable_inspect)
.bindTo(this);
}
final String name = method.getName();
final Getter getter;
// If method is a property getter, and we have a value for the property,
// return a constant method handle that just returns the value.
if(method.getParameterTypes().length == 0) {
getter = meta.getters().get(name);
if(getter != null && data.containsKey(name)) {
return MethodHandles.constant(method.getReturnType(), data.get(name));
}
} else {
getter = null;
}
// If the base class implements the method, call that one.
if(method.getDeclaringClass().isInstance(base)) {
return MethodHandles.lookup()
.unreflect(method)
.bindTo(base);
}
// If method has a default implementation in the document interface,
// return a method handle that calls that implementation directly,
// with the proxy as target object. We can't just invoke the method
// the normal way, because the proxy would intercept it again.
if(method.isDefault()) {
return MethodHandleUtils.defaultMethodHandle(method)
.bindTo(proxy);
}
// If the method is a nullable property, we can return null as a default value.
if(getter != null && getter.isNullable()) {
return MethodHandles.constant(method.getReturnType(), null);
}
throw new SerializationException("No implementation for method '" + name + "'");
});
}
@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return handles.getUnchecked(method).invokeWithArguments(args);
}
@Override
public String inspectType() {
return meta.type().getSimpleName();
}
@Override
public Optional<String> inspectIdentity() {
if(proxy instanceof Model) {
return Optional.of(((Model) proxy)._id());
}
return Optional.empty();
}
@Override
public Stream<? extends InspectableProperty> inspectableProperties() {
return BiStream.from(data)
.merge(InspectableProperty::of);
}
}
}

View File

@ -0,0 +1,26 @@
package tc.oc.api.document;
import java.util.concurrent.ExecutionException;
import com.google.common.util.concurrent.UncheckedExecutionException;
import tc.oc.api.docs.virtual.Document;
/**
* Metadata for an accessor that can set the value of a {@link Document} property
*/
public interface Setter<T> extends Accessor<T> {
/**
* Set the value of this property on the given document.
* @throws ExecutionException if a checked exception was thrown while trying to write the value,
* or if the property is not accessible on the given object.
*/
void set(Object obj, T value) throws ExecutionException;
/**
* Set the value of this property on the given document.
* @throws UncheckedExecutionException if a checked exception was thrown while trying to write the value,
* or if the property is not accessible on the given object.
*/
void setUnchecked(Object obj, T value);
}

View File

@ -0,0 +1,76 @@
package tc.oc.api.document;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.concurrent.ExecutionException;
import javax.annotation.Nullable;
import com.google.common.util.concurrent.UncheckedExecutionException;
import tc.oc.api.docs.virtual.Document;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* Metadata for a setter method of a {@link Document} property.
*
* The wrapped method takes the new value as its single parameter.
*/
public class SetterMethod<T> extends BaseAccessor<T> implements Setter<T> {
private final Method method;
public SetterMethod(DocumentRegistry registry, Method method) {
super(registry);
this.method = checkNotNull(method);
method.setAccessible(true);
}
@Override
public Method member() {
return method;
}
@Override
public Type type() {
return method.getGenericParameterTypes()[0];
}
@Override
public Class<T> rawType() {
return (Class<T>) method.getParameterTypes()[0];
}
@Override
protected @Nullable Accessor<?> getOverrideIn(DocumentMeta<?> ancestor) {
return ancestor.setters().get(name());
}
@Override
public boolean isImplemented(Class<?> type) {
try {
return !Modifier.isAbstract(type.getMethod(method.getName(), method.getParameterTypes()[0]).getModifiers());
} catch(NoSuchMethodException e) {
return false;
}
}
@Override
public void setUnchecked(Object obj, T value) {
try {
set(obj, value);
} catch(ExecutionException e) {
throw new UncheckedExecutionException(e.getCause());
}
}
@Override
public void set(Object obj, T value) throws ExecutionException {
try {
method.invoke(obj, validate(value));
} catch(IllegalAccessException | InvocationTargetException e) {
throw new ExecutionException(e);
}
}
}

View File

@ -0,0 +1,17 @@
package tc.oc.api.engagement;
import com.google.inject.multibindings.OptionalBinder;
import tc.oc.api.docs.virtual.EngagementDoc;
import tc.oc.api.model.ModelBinders;
import tc.oc.commons.core.inject.HybridManifest;
public class EngagementModelManifest extends HybridManifest implements ModelBinders {
@Override
protected void configure() {
bindModel(EngagementDoc.class);
OptionalBinder.newOptionalBinder(publicBinder(), EngagementService.class)
.setDefault().to(LocalEngagementService.class);
}
}

View File

@ -0,0 +1,12 @@
package tc.oc.api.engagement;
import java.util.Collection;
import com.google.common.util.concurrent.ListenableFuture;
import tc.oc.api.docs.virtual.EngagementDoc;
import tc.oc.api.message.types.Reply;
public interface EngagementService {
ListenableFuture<Reply> updateMulti(Collection<? extends EngagementDoc> engagements);
}

View File

@ -0,0 +1,19 @@
package tc.oc.api.engagement;
import java.util.Collection;
import tc.oc.api.annotations.Serialize;
import tc.oc.api.docs.virtual.EngagementDoc;
import tc.oc.api.message.Message;
import tc.oc.api.queue.MessageDefaults;
@MessageDefaults.RoutingKey("engagements")
@MessageDefaults.Persistent(true)
public class EngagementUpdateRequest implements Message {
@Serialize public final Collection<? extends EngagementDoc> engagements;
public EngagementUpdateRequest(Collection<? extends EngagementDoc> engagements) {
this.engagements = engagements;
}
}

View File

@ -0,0 +1,17 @@
package tc.oc.api.engagement;
import java.util.Collection;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import tc.oc.api.docs.virtual.EngagementDoc;
import tc.oc.api.engagement.EngagementService;
import tc.oc.api.message.types.Reply;
public class LocalEngagementService implements EngagementService {
@Override
public ListenableFuture<Reply> updateMulti(Collection<? extends EngagementDoc> engagements) {
return Futures.immediateFuture(Reply.SUCCESS);
}
}

View File

@ -0,0 +1,53 @@
package tc.oc.api.exceptions;
import javax.annotation.Nullable;
import tc.oc.api.message.types.Reply;
import tc.oc.commons.core.util.ArrayUtils;
/**
* Thrown when an API call has an exceptional response. This abstracts away HTTP
* status codes so we can use other protocols for the API.
*/
public class ApiException extends Exception {
private @Nullable StackTraceElement[] originalTrace;
private @Nullable StackTraceElement[] callSite;
private final @Nullable Reply reply;
public ApiException(String message, @Nullable Reply reply) {
this(message, null, null, reply);
}
public ApiException(String message, @Nullable StackTraceElement[] callSite) {
this(message, null, callSite);
}
public ApiException(String message, @Nullable Throwable cause, @Nullable StackTraceElement[] callSite) {
this(message, cause, callSite, null);
}
public ApiException(String message, @Nullable Throwable cause, @Nullable StackTraceElement[] callSite, @Nullable Reply reply) {
super(message, cause);
this.reply = reply;
setCallSite(callSite);
}
public @Nullable Reply getReply() {
return reply;
}
public @Nullable StackTraceElement[] getCallSite() {
return callSite;
}
public void setCallSite(@Nullable StackTraceElement[] callSite) {
this.callSite = callSite;
if(callSite != null) {
if(originalTrace == null) {
originalTrace = getStackTrace();
}
setStackTrace(ArrayUtils.append(originalTrace, callSite));
}
}
}

View File

@ -0,0 +1,16 @@
package tc.oc.api.exceptions;
/**
* Thrown when you try to do something that requires an API connection,
* but the API is not currently connected.
*/
public class ApiNotConnected extends IllegalStateException {
public ApiNotConnected() {
this("No API connection");
}
public ApiNotConnected(String s) {
super(s);
}
}

View File

@ -0,0 +1,14 @@
package tc.oc.api.exceptions;
import javax.annotation.Nullable;
import tc.oc.api.message.types.Reply;
/**
* An API request is invalid based on the current state of whatever it is manipulating
*/
public class Conflict extends ApiException {
public Conflict(String message, @Nullable Reply reply) {
super(message, reply);
}
}

View File

@ -0,0 +1,14 @@
package tc.oc.api.exceptions;
import javax.annotation.Nullable;
import tc.oc.api.message.types.Reply;
/**
* An API request was not allowed
*/
public class Forbidden extends ApiException {
public Forbidden(String message, @Nullable Reply reply) {
super(message, reply);
}
}

View File

@ -0,0 +1,23 @@
package tc.oc.api.exceptions;
import javax.annotation.Nullable;
import tc.oc.api.message.types.Reply;
/**
* Something was referenced through the API that does not exist
*/
public class NotFound extends ApiException {
public NotFound() {
this(null);
}
public NotFound(@Nullable String message) {
this(message, null);
}
public NotFound(@Nullable String message, @Nullable Reply reply) {
super(message, reply);
}
}

View File

@ -0,0 +1,19 @@
package tc.oc.api.exceptions;
public class SerializationException extends RuntimeException {
public SerializationException() {
}
public SerializationException(String message) {
super(message);
}
public SerializationException(String message, Throwable cause) {
super(message, cause);
}
public SerializationException(Throwable cause) {
super(cause);
}
}

View File

@ -0,0 +1,20 @@
package tc.oc.api.exceptions;
/**
* Thrown when an unknown message type is received from an AMQP queue.
* This is usually not a problem. It could be a message on the fanout
* exchange not intended for the server, or a future message type
* sent during an upgrade.
*/
public class UnknownMessageType extends RuntimeException {
private final String type;
public UnknownMessageType(String type) {
super("Unknown queue message of type '" + type + "'");
this.type = type;
}
public String getMessageType() {
return type;
}
}

View File

@ -0,0 +1,18 @@
package tc.oc.api.exceptions;
public class UnmappedUserException extends IllegalStateException {
private final String username;
public UnmappedUserException(String username) {
this.username = username;
}
public String getUsername() {
return username;
}
@Override
public String getMessage() {
return "No UserId stored for username \"" + this.username + "\"";
}
}

View File

@ -0,0 +1,14 @@
package tc.oc.api.exceptions;
import javax.annotation.Nullable;
import tc.oc.api.message.types.Reply;
/**
* HTTP 422 i.e. validation failed
*/
public class UnprocessableEntity extends ApiException {
public UnprocessableEntity(String message, @Nullable Reply reply) {
super(message, reply);
}
}

View File

@ -0,0 +1,41 @@
package tc.oc.api.games;
import java.util.Set;
import javax.annotation.Nullable;
import javax.inject.Singleton;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.SetMultimap;
import com.google.common.collect.Table;
import tc.oc.api.docs.Arena;
import tc.oc.api.model.ModelStore;
@Singleton
public class ArenaStore extends ModelStore<Arena> {
private final SetMultimap<String, Arena> byDatacenter = HashMultimap.create();
private final Table<String, String, Arena> byDatacenterAndGameId = HashBasedTable.create();
public Set<Arena> byDatacenter(String datacenter) {
return byDatacenter.get(datacenter);
}
public @Nullable Arena tryDatacenterAndGameId(String datacenter, String gameId) {
return byDatacenterAndGameId.get(datacenter, gameId);
}
@Override
protected void unindex(Arena doc) {
super.unindex(doc);
byDatacenter.remove(doc.datacenter(), doc);
byDatacenterAndGameId.remove(doc.datacenter(), doc.game_id());
}
@Override
protected void reindex(Arena doc) {
super.reindex(doc);
byDatacenter.put(doc.datacenter(), doc);
byDatacenterAndGameId.put(doc.datacenter(), doc.game_id(), doc);
}
}

View File

@ -0,0 +1,35 @@
package tc.oc.api.games;
import tc.oc.api.docs.Arena;
import tc.oc.api.docs.Game;
import tc.oc.api.docs.Ticket;
import tc.oc.api.model.ModelBinders;
import tc.oc.commons.core.inject.HybridManifest;
public class GameModelManifest extends HybridManifest implements ModelBinders {
@Override
protected void configure() {
bindAndExpose(GameStore.class);
bindAndExpose(ArenaStore.class);
bindAndExpose(TicketStore.class);
bindModel(Game.class, model -> {
model.bindStore().to(GameStore.class);
model.queryService().setDefault().to(model.nullQueryService());
});
bindModel(Arena.class, model -> {
model.bindStore().to(ArenaStore.class);
model.queryService().setDefault().to(model.nullQueryService());
});
bindModel(Ticket.class, model -> {
model.bindStore().to(TicketStore.class);
model.queryService().setBinding().to(TicketService.class);
});
publicBinder().forOptional(TicketService.class)
.setDefault().to(NullTicketService.class);
}
}

View File

@ -0,0 +1,10 @@
package tc.oc.api.games;
import javax.inject.Singleton;
import tc.oc.api.docs.Game;
import tc.oc.api.model.ModelStore;
@Singleton
public class GameStore extends ModelStore<Game> {
}

View File

@ -0,0 +1,23 @@
package tc.oc.api.games;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import tc.oc.api.docs.Ticket;
import tc.oc.api.message.types.CycleRequest;
import tc.oc.api.message.types.CycleResponse;
import tc.oc.api.message.types.PlayGameRequest;
import tc.oc.api.message.types.Reply;
import tc.oc.api.model.NullQueryService;
class NullTicketService extends NullQueryService<Ticket> implements TicketService {
@Override
public ListenableFuture<Reply> requestPlay(PlayGameRequest request) {
return Futures.immediateFuture(Reply.FAILURE);
}
@Override
public ListenableFuture<CycleResponse> requestCycle(CycleRequest request) {
return Futures.immediateFuture(CycleResponse.EMPTY);
}
}

View File

@ -0,0 +1,16 @@
package tc.oc.api.games;
import com.google.common.util.concurrent.ListenableFuture;
import tc.oc.api.docs.Ticket;
import tc.oc.api.message.types.CycleRequest;
import tc.oc.api.message.types.CycleResponse;
import tc.oc.api.message.types.PlayGameRequest;
import tc.oc.api.message.types.Reply;
import tc.oc.api.model.QueryService;
public interface TicketService extends QueryService<Ticket> {
ListenableFuture<Reply> requestPlay(PlayGameRequest request);
ListenableFuture<CycleResponse> requestCycle(CycleRequest request);
}

View File

@ -0,0 +1,54 @@
package tc.oc.api.games;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
import javax.inject.Singleton;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.SetMultimap;
import tc.oc.api.docs.Arena;
import tc.oc.api.docs.PlayerId;
import tc.oc.api.docs.Ticket;
import tc.oc.api.model.ModelStore;
@Singleton
public class TicketStore extends ModelStore<Ticket> {
private final Map<PlayerId, Ticket> byUser = new HashMap<>();
private final SetMultimap<String, Ticket> byArenaId = HashMultimap.create();
private final SetMultimap<String, Ticket> byArenaIdQueued = HashMultimap.create();
public @Nullable Ticket tryUser(PlayerId playerId) {
return byUser.get(playerId);
}
public Set<Ticket> byArena(Arena arena) {
return byArenaId.get(arena._id());
}
public Set<Ticket> queued(Arena arena) {
return byArenaIdQueued.get(arena._id());
}
@Override
protected void reindex(Ticket doc) {
super.reindex(doc);
byUser.put(doc.user(), doc);
byArenaId.put(doc.arena_id(), doc);
if(doc.server_id() == null) {
byArenaIdQueued.put(doc.arena_id(), doc);
} else {
byArenaIdQueued.remove(doc.arena_id(), doc);
}
}
@Override
protected void unindex(Ticket doc) {
super.unindex(doc);
byUser.remove(doc.user());
byArenaId.remove(doc.arena_id(), doc);
byArenaIdQueued.remove(doc.arena_id(), doc);
}
}

View File

@ -0,0 +1,307 @@
package tc.oc.api.http;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Type;
import java.time.Duration;
import java.util.concurrent.Callable;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.logging.ConsoleHandler;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.inject.Inject;
import javax.inject.Singleton;
import com.google.api.client.http.AbstractHttpContent;
import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpBackOffIOExceptionHandler;
import com.google.api.client.http.HttpContent;
import com.google.api.client.http.HttpMethods;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpRequestFactory;
import com.google.api.client.http.HttpResponse;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.util.ExponentialBackOff;
import com.google.common.base.Charsets;
import com.google.common.reflect.TypeToken;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.google.gson.Gson;
import com.google.gson.JsonParseException;
import tc.oc.api.connectable.Connectable;
import tc.oc.api.config.ApiConstants;
import tc.oc.api.exceptions.ApiException;
import tc.oc.api.exceptions.Conflict;
import tc.oc.api.exceptions.Forbidden;
import tc.oc.api.exceptions.NotFound;
import tc.oc.api.exceptions.UnprocessableEntity;
import tc.oc.api.message.types.Reply;
import tc.oc.api.serialization.JsonUtils;
import tc.oc.commons.core.concurrent.ExecutorUtils;
import tc.oc.commons.core.logging.Loggers;
import static com.google.common.base.Preconditions.checkNotNull;
@Singleton
public class HttpClient implements Connectable {
private static final Duration SHUTDOWN_TIMEOUT = Duration.ofSeconds(10);
protected final Logger logger;
private final HttpClientConfiguration config;
private final ListeningExecutorService executorService;
private final JsonUtils jsonUtils;
private final Gson gson;
private final HttpRequestFactory requestFactory;
@Inject HttpClient(Loggers loggers, HttpClientConfiguration config, JsonUtils jsonUtils, Gson gson) {
this.logger = loggers.get(getClass());
this.config = checkNotNull(config, "config");
this.jsonUtils = jsonUtils;
this.gson = gson;
ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("API HTTP Executor").build();
if (config.getThreads() > 0) {
this.executorService = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(config.getThreads()));
} else {
this.executorService = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool(threadFactory));
}
this.requestFactory = this.createRequestFactory();
// By default the google http client is very noisy on http errors,
// spitting out a long stack trace every time one try of a request
// fails. Disabling the parent handlers (removes default console
// handling) and adding our own handler that will only show the short
// message makes the error output much more manageable.
// See HttpRequest#986 for the offending line and HttpTransport#81 for
// the logger definition (it's package private so we can't access it
// directly).
final Logger httpLogger = Logger.getLogger(HttpTransport.class.getName());
httpLogger.setUseParentHandlers(false);
httpLogger.addHandler(new ConsoleHandler() {
@Override
public void publish(LogRecord record) {
String message = record.getMessage();
if (record.getThrown() != null) message += ": " + record.getThrown().toString();
HttpClient.this.logger.log(record.getLevel(), message);
}
});
}
private HttpRequestFactory createRequestFactory() {
return new NetHttpTransport().createRequestFactory(request -> {
request.setConnectTimeout(HttpClient.this.config.getConnectTimeout());
request.setReadTimeout(HttpClient.this.config.getReadTimeout());
request.setNumberOfRetries(HttpClient.this.config.getRetries());
request.setIOExceptionHandler(new HttpBackOffIOExceptionHandler(new ExponentialBackOff.Builder().build()));
});
}
public String getBaseUrl() {
return this.config.getBaseUrl();
}
public ListenableFuture<?> get(String path, HttpOption... options) {
return get(path, (TypeToken) null, options);
}
public <T> ListenableFuture<T> get(String path, @Nullable Class<T> returnType, HttpOption...options) {
return request(HttpMethods.GET, path, null, returnType, options);
}
public ListenableFuture<?> get(String path, @Nullable Type returnType, HttpOption...options) {
return request(HttpMethods.GET, path, null, returnType, options);
}
public <T> ListenableFuture<T> get(String path, @Nullable TypeToken<T> returnType, HttpOption...options) {
return request(HttpMethods.GET, path, null, returnType, options);
}
public ListenableFuture<?> post(String path, @Nullable Object content, HttpOption...options) {
return post(path, content, (TypeToken) null, options);
}
public <T> ListenableFuture<T> post(String path, @Nullable Object content, @Nullable Class<T> returnType, HttpOption...options) {
return request(HttpMethods.POST, path, content, returnType, options);
}
public ListenableFuture<?> post(String path, @Nullable Object content, @Nullable Type returnType, HttpOption...options) {
return request(HttpMethods.POST, path, content, returnType, options);
}
public <T> ListenableFuture<T> post(String path, @Nullable Object content, @Nullable TypeToken<T> returnType, HttpOption...options) {
return request(HttpMethods.POST, path, content, returnType, options);
}
public ListenableFuture<?> put(String path, @Nullable Object content, HttpOption...options) {
return put(path, content, (TypeToken) null, options);
}
public <T> ListenableFuture<T> put(String path, @Nullable Object content, @Nullable Class<T> returnType, HttpOption...options) {
return request(HttpMethods.PUT, path, content, returnType, options);
}
public ListenableFuture<?> put(String path, @Nullable Object content, @Nullable Type returnType, HttpOption...options) {
return request(HttpMethods.PUT, path, content, returnType, options);
}
public <T> ListenableFuture<T> put(String path, @Nullable Object content, @Nullable TypeToken<T> returnType, HttpOption...options) {
return request(HttpMethods.PUT, path, content, returnType, options);
}
protected <T> ListenableFuture<T> request(String method, String path, @Nullable Object content, @Nullable Class<T> returnType, HttpOption...options) {
return request(method, path, content, returnType == null ? null : TypeToken.of(returnType), options);
}
protected ListenableFuture<?> request(String method, String path, @Nullable Object content, @Nullable Type returnType, HttpOption...options) {
return request(method, path, content, returnType == null ? null : TypeToken.of(returnType), options);
}
protected <T> ListenableFuture<T> request(String method, String path, @Nullable Object content, @Nullable TypeToken<T> returnType, HttpOption...options) {
// NOTE: Serialization must happen synchronously, because getter methods may not be thread-safe
final HttpContent httpContent = content == null ? null : new Content(gson.toJson(content));
GenericUrl url = new GenericUrl(this.getBaseUrl() + path);
HttpRequest request;
try {
request = requestFactory.buildRequest(method, url, httpContent).setThrowExceptionOnExecuteError(false);
} catch (IOException e) {
this.getLogger().log(Level.SEVERE, "Failed to build request to " + url.toString(), e);
return Futures.immediateFailedFuture(e);
}
request.getHeaders().set("X-OCN-Version", String.valueOf(ApiConstants.PROTOCOL_VERSION));
request.getHeaders().setAccept("application/json");
for(HttpOption option : options) {
switch(option) {
case INFINITE_RETRY:
request.setNumberOfRetries(Integer.MAX_VALUE);
break;
case NO_RETRY:
request.setNumberOfRetries(0);
break;
}
}
return this.executorService.submit(new RequestCallable<T>(request, returnType, options));
}
@Override
public void connect() throws IOException {
// Nothing to do
}
@Override
public void disconnect() throws IOException {
ExecutorUtils.shutdownImpatiently(executorService, logger, SHUTDOWN_TIMEOUT);
}
protected Logger getLogger() {
return logger;
}
private class Content extends AbstractHttpContent {
final String json;
protected Content(String json) {
super("application/json");
this.json = json;
}
@Override
public void writeTo(OutputStream out) throws IOException {
out.write(json.getBytes(Charsets.UTF_8));
}
}
private class RequestCallable<T> implements Callable<T> {
private final HttpRequest request;
private final @Nullable Content content;
private final TypeToken<T> returnType;
private StackTraceElement[] callSite;
public RequestCallable(HttpRequest request, @Nullable TypeToken<T> returnType, HttpOption...options) {
this.request = checkNotNull(request, "http request");
this.content = (Content) request.getContent();
this.returnType = returnType;
this.callSite = new Exception().getStackTrace();
}
@Override
public T call() throws Exception {
try {
if(logger.isLoggable(Level.FINE)) {
logger.fine("Request " + request.getRequestMethod() + " " + request.getUrl() +
(content == null ? " (no content)" : "\n" + content.json));
}
final HttpResponse response = this.request.execute();
final String json = response.parseAsString();
if(logger.isLoggable(Level.FINE)) {
logger.fine("Response " + response.getStatusCode() +
" " + response.getStatusMessage() +
"\n" + jsonUtils.prettify(json));
}
if(response.getStatusCode() < 300) {
// We need a 200 in order to return a document
if(returnType == null) return null;
try {
return gson.fromJson(json, returnType.getType());
} catch (JsonParseException e) {
throw new ApiException("Parsing of " + returnType + " failed at [" + jsonUtils.errorContext(json, returnType.getType()) + "]", e, callSite);
}
} else if(response.getStatusCode() < 400 && returnType == null) {
// 3XX is a successful response only if no response document is expected
return null;
} else {
Reply reply;
// Error response might be a Reply, even if that is not the return type
try {
reply = gson.fromJson(json, Reply.class);
} catch(JsonParseException ex) {
reply = null;
}
final String message;
if(reply != null && reply.error() != null) {
// If we have a Reply somehow, use the included error message
message = reply.error();
} else {
// Otherwise, make a generic message
message = "HTTP " + response.getStatusCode() + " " + response.getStatusMessage();
}
final ApiException apiException;
switch(response.getStatusCode()) {
case 403: apiException = new Forbidden(message, reply); break;
case 404: apiException = new NotFound(message, reply); break;
case 409: apiException = new Conflict(message, reply); break;
case 422: apiException = new UnprocessableEntity(message, reply); break;
default: apiException = new ApiException(message, reply); break;
}
throw apiException;
}
} catch(ApiException e) {
e.setCallSite(callSite);
throw e;
} catch (Throwable e) {
final String message = "Unhandled exception submitting request to " + this.request.getUrl();
logger.log(Level.SEVERE, message, e);
throw new ApiException(message, e, callSite);
}
}
}
}

View File

@ -0,0 +1,38 @@
package tc.oc.api.http;
public interface HttpClientConfiguration {
int DEFAULT_THREADS = 0;
int DEFAULT_CONNECT_TIMEOUT = 20000;
int DEFAULT_READ_TIMEOUT = 20000;
int DEFAULT_RETRIES = 10;
/**
* Base URL of the API. End points will be appended to this address.
*/
String getBaseUrl();
/**
* Number of threads to execute requests. 0 indicates an unbounded number
* of threads.
*/
int getThreads();
/**
* Timeout in milliseconds to establish a connection or 0 for an infinite
* timeout.
*/
int getConnectTimeout();
/**
* Timeout in milliseconds to read data from an established connection or 0
* for an infinite timeout.
*/
int getReadTimeout();
/**
* Number of retries to execute a request until giving up. 0 indicates no
* retrying.
*/
int getRetries();
}

View File

@ -0,0 +1,50 @@
package tc.oc.api.http;
import javax.inject.Inject;
import tc.oc.minecraft.api.configuration.Configuration;
import tc.oc.minecraft.api.configuration.ConfigurationSection;
import static com.google.common.base.Preconditions.checkNotNull;
public class HttpClientConfigurationImpl implements HttpClientConfiguration {
public static final String SECTION = "api.http";
public static final String RETRIES_PATH = "retries";
public static final String READ_TIMEOUT_PATH = "read-timeout";
public static final String CONNECT_TIMEOUT_PATH = "connect-timeout";
public static final String THREADS_PATH = "threads";
public static final String BASE_URL_PATH = "base-url";
private final ConfigurationSection config;
@Inject public HttpClientConfigurationImpl(Configuration config) {
this.config = checkNotNull(config.getSection(SECTION));
}
@Override
public String getBaseUrl() {
return config.getString(BASE_URL_PATH);
}
@Override
public int getThreads() {
return config.getInt(THREADS_PATH);
}
@Override
public int getConnectTimeout() {
return config.getInt(CONNECT_TIMEOUT_PATH);
}
@Override
public int getReadTimeout() {
return config.getInt(READ_TIMEOUT_PATH);
}
@Override
public int getRetries() {
return config.getInt(RETRIES_PATH);
}
}

View File

@ -0,0 +1,14 @@
package tc.oc.api.http;
import tc.oc.commons.core.inject.HybridManifest;
public class HttpManifest extends HybridManifest {
@Override
protected void configure() {
expose(HttpClient.class);
bind(HttpClient.class);
bind(HttpClientConfiguration.class)
.to(HttpClientConfigurationImpl.class);
}
}

View File

@ -0,0 +1,12 @@
package tc.oc.api.http;
/**
* Flags passed to HttpClient for individual requests.
*
* TODO nice-to-have: replace HttpClientConfig with this mechanism so
* any setting can be overridden per request.
*/
public enum HttpOption {
NO_RETRY, // Do not retry the request
INFINITE_RETRY // Retry the request forever
}

View File

@ -0,0 +1,40 @@
package tc.oc.api.http;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;
import com.google.api.client.repackaged.com.google.common.base.Joiner;
import com.google.common.base.Charsets;
public class QueryUri {
private static final Joiner JOINER = Joiner.on('&');
private final String prefix;
private final List<String> vars = new ArrayList<>();
public QueryUri(String prefix) {
this.prefix = prefix;
}
public QueryUri put(String name, Object value) {
try {
vars.add(URLEncoder.encode(name, Charsets.UTF_8.name()) +
"=" +
URLEncoder.encode(String.valueOf(value), Charsets.UTF_8.name()));
} catch(UnsupportedEncodingException e) {
throw new IllegalStateException(e);
}
return this;
}
public String encode() {
if(vars.isEmpty()) {
return prefix;
} else {
return prefix + '?' + JOINER.join(vars);
}
}
}

View File

@ -0,0 +1,18 @@
package tc.oc.api.maps;
import com.google.inject.multibindings.OptionalBinder;
import tc.oc.api.docs.virtual.MapDoc;
import tc.oc.api.model.ModelBinders;
import tc.oc.commons.core.inject.HybridManifest;
public class MapModelManifest extends HybridManifest implements ModelBinders {
@Override
protected void configure() {
bindModel(MapDoc.class, model -> {
model.bindService().to(MapService.class);
});
OptionalBinder.newOptionalBinder(publicBinder(), MapService.class);
}
}

View File

@ -0,0 +1,25 @@
package tc.oc.api.maps;
import tc.oc.api.docs.UserId;
import tc.oc.api.docs.virtual.MapDoc;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
public class MapRatingsRequest {
public final @Nullable String map_id;
public final String map_name;
public final String map_version; // TODO: use SemanticVersion class
public final List<String> player_ids;
public MapRatingsRequest(MapDoc map, Collection<? extends UserId> userIds) {
this.map_id = map._id();
this.map_name = map.name();
this.map_version = map.version().toString();
this.player_ids = new ArrayList<>(userIds.size());
for(UserId userId : userIds) this.player_ids.add(userId.player_id());
}
}

View File

@ -0,0 +1,12 @@
package tc.oc.api.maps;
import java.util.Map;
import tc.oc.api.annotations.Serialize;
import tc.oc.api.docs.UserId;
import tc.oc.api.docs.virtual.Document;
@Serialize
public interface MapRatingsResponse extends Document {
Map<UserId, Integer> player_ratings();
}

View File

@ -0,0 +1,17 @@
package tc.oc.api.maps;
import java.util.Collection;
import com.google.common.util.concurrent.ListenableFuture;
import tc.oc.api.docs.MapRating;
import tc.oc.api.docs.virtual.MapDoc;
import tc.oc.api.model.ModelService;
public interface MapService extends ModelService<MapDoc, MapDoc> {
ListenableFuture<?> rate(MapRating rating);
ListenableFuture<MapRatingsResponse> getRatings(MapRatingsRequest request);
ListenableFuture<MapUpdateMultiResponse> updateMapsAndLookupAuthors(Collection<? extends MapDoc> maps);
}

Some files were not shown because too many files have changed in this diff Show More