mirror of
https://github.com/OvercastNetwork/ProjectAres.git
synced 2025-04-11 22:56:08 +02:00
Initial public release
This commit is contained in:
commit
7755843923
9
.gitattributes
vendored
Normal file
9
.gitattributes
vendored
Normal 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
34
.gitignore
vendored
Normal 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
95
API/api/pom.xml
Normal 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>
|
48
API/api/src/main/java/tc/oc/api/ApiManifest.java
Normal file
48
API/api/src/main/java/tc/oc/api/ApiManifest.java
Normal 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());
|
||||
}
|
||||
}
|
12
API/api/src/main/java/tc/oc/api/annotations/ApiRequired.java
Normal file
12
API/api/src/main/java/tc/oc/api/annotations/ApiRequired.java
Normal 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 {}
|
19
API/api/src/main/java/tc/oc/api/annotations/Serialize.java
Normal file
19
API/api/src/main/java/tc/oc/api/annotations/Serialize.java
Normal 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;
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
package tc.oc.api.config;
|
||||
|
||||
public interface ApiConfiguration {
|
||||
String primaryQueueName();
|
||||
}
|
7
API/api/src/main/java/tc/oc/api/config/ApiConstants.java
Normal file
7
API/api/src/main/java/tc/oc/api/config/ApiConstants.java
Normal file
@ -0,0 +1,7 @@
|
||||
package tc.oc.api.config;
|
||||
|
||||
public final class ApiConstants {
|
||||
private ApiConstants() {}
|
||||
|
||||
public static final int PROTOCOL_VERSION = 4;
|
||||
}
|
19
API/api/src/main/java/tc/oc/api/connectable/Connectable.java
Normal file
19
API/api/src/main/java/tc/oc/api/connectable/Connectable.java
Normal 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 {};
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
66
API/api/src/main/java/tc/oc/api/connectable/Connector.java
Normal file
66
API/api/src/main/java/tc/oc/api/connectable/Connector.java
Normal 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)));
|
||||
}
|
||||
}
|
41
API/api/src/main/java/tc/oc/api/docs/AbstractModel.java
Normal file
41
API/api/src/main/java/tc/oc/api/docs/AbstractModel.java
Normal 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 + ")";
|
||||
}
|
||||
}
|
||||
}
|
21
API/api/src/main/java/tc/oc/api/docs/Arena.java
Normal file
21
API/api/src/main/java/tc/oc/api/docs/Arena.java
Normal 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() + "]";
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
28
API/api/src/main/java/tc/oc/api/docs/BasicModel.java
Normal file
28
API/api/src/main/java/tc/oc/api/docs/BasicModel.java
Normal 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;
|
||||
}
|
||||
}
|
5
API/api/src/main/java/tc/oc/api/docs/Death.java
Normal file
5
API/api/src/main/java/tc/oc/api/docs/Death.java
Normal file
@ -0,0 +1,5 @@
|
||||
package tc.oc.api.docs;
|
||||
|
||||
import tc.oc.api.docs.virtual.DeathDoc;
|
||||
|
||||
public interface Death extends DeathDoc.Complete {}
|
16
API/api/src/main/java/tc/oc/api/docs/Entrant.java
Normal file
16
API/api/src/main/java/tc/oc/api/docs/Entrant.java
Normal 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();
|
||||
}
|
19
API/api/src/main/java/tc/oc/api/docs/Game.java
Normal file
19
API/api/src/main/java/tc/oc/api/docs/Game.java
Normal 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();
|
||||
}
|
||||
}
|
25
API/api/src/main/java/tc/oc/api/docs/MapRating.java
Normal file
25
API/api/src/main/java/tc/oc/api/docs/MapRating.java
Normal 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;
|
||||
}
|
||||
}
|
9
API/api/src/main/java/tc/oc/api/docs/MatchState.java
Normal file
9
API/api/src/main/java/tc/oc/api/docs/MatchState.java
Normal file
@ -0,0 +1,9 @@
|
||||
package tc.oc.api.docs;
|
||||
|
||||
public enum MatchState {
|
||||
IDLE,
|
||||
STARTING,
|
||||
HUDDLE,
|
||||
RUNNING,
|
||||
FINISHED
|
||||
}
|
60
API/api/src/main/java/tc/oc/api/docs/Objective.java
Normal file
60
API/api/src/main/java/tc/oc/api/docs/Objective.java
Normal 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() + "]";
|
||||
}
|
||||
|
||||
}
|
33
API/api/src/main/java/tc/oc/api/docs/Participation.java
Normal file
33
API/api/src/main/java/tc/oc/api/docs/Participation.java
Normal 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 {}
|
||||
}
|
25
API/api/src/main/java/tc/oc/api/docs/PlayerId.java
Normal file
25
API/api/src/main/java/tc/oc/api/docs/PlayerId.java
Normal 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();
|
||||
}
|
||||
}
|
5
API/api/src/main/java/tc/oc/api/docs/Punishment.java
Normal file
5
API/api/src/main/java/tc/oc/api/docs/Punishment.java
Normal file
@ -0,0 +1,5 @@
|
||||
package tc.oc.api.docs;
|
||||
|
||||
import tc.oc.api.docs.virtual.PunishmentDoc;
|
||||
|
||||
public interface Punishment extends PunishmentDoc.Complete {}
|
5
API/api/src/main/java/tc/oc/api/docs/Report.java
Normal file
5
API/api/src/main/java/tc/oc/api/docs/Report.java
Normal file
@ -0,0 +1,5 @@
|
||||
package tc.oc.api.docs;
|
||||
|
||||
import tc.oc.api.docs.virtual.ReportDoc;
|
||||
|
||||
public interface Report extends ReportDoc.Complete {}
|
82
API/api/src/main/java/tc/oc/api/docs/SemanticVersion.java
Normal file
82
API/api/src/main/java/tc/oc/api/docs/SemanticVersion.java
Normal 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};
|
||||
}
|
||||
}
|
5
API/api/src/main/java/tc/oc/api/docs/Server.java
Normal file
5
API/api/src/main/java/tc/oc/api/docs/Server.java
Normal file
@ -0,0 +1,5 @@
|
||||
package tc.oc.api.docs;
|
||||
|
||||
import tc.oc.api.docs.virtual.ServerDoc;
|
||||
|
||||
public interface Server extends ServerDoc.Complete {}
|
5
API/api/src/main/java/tc/oc/api/docs/Session.java
Normal file
5
API/api/src/main/java/tc/oc/api/docs/Session.java
Normal file
@ -0,0 +1,5 @@
|
||||
package tc.oc.api.docs;
|
||||
|
||||
import tc.oc.api.docs.virtual.SessionDoc;
|
||||
|
||||
public interface Session extends SessionDoc.Complete {}
|
56
API/api/src/main/java/tc/oc/api/docs/SimplePlayerId.java
Normal file
56
API/api/src/main/java/tc/oc/api/docs/SimplePlayerId.java
Normal 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() +
|
||||
"}";
|
||||
}
|
||||
}
|
46
API/api/src/main/java/tc/oc/api/docs/SimpleUserId.java
Normal file
46
API/api/src/main/java/tc/oc/api/docs/SimpleUserId.java
Normal 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() + "}";
|
||||
}
|
||||
}
|
21
API/api/src/main/java/tc/oc/api/docs/Ticket.java
Normal file
21
API/api/src/main/java/tc/oc/api/docs/Ticket.java
Normal 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();
|
||||
}
|
||||
}
|
43
API/api/src/main/java/tc/oc/api/docs/Tournament.java
Normal file
43
API/api/src/main/java/tc/oc/api/docs/Tournament.java
Normal 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();
|
||||
}
|
||||
}
|
19
API/api/src/main/java/tc/oc/api/docs/Trophy.java
Normal file
19
API/api/src/main/java/tc/oc/api/docs/Trophy.java
Normal 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();
|
||||
}
|
||||
|
||||
}
|
5
API/api/src/main/java/tc/oc/api/docs/User.java
Normal file
5
API/api/src/main/java/tc/oc/api/docs/User.java
Normal file
@ -0,0 +1,5 @@
|
||||
package tc.oc.api.docs;
|
||||
|
||||
import tc.oc.api.docs.virtual.UserDoc;
|
||||
|
||||
public interface User extends UserDoc.Login {}
|
21
API/api/src/main/java/tc/oc/api/docs/UserId.java
Normal file
21
API/api/src/main/java/tc/oc/api/docs/UserId.java
Normal 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();
|
||||
}
|
7
API/api/src/main/java/tc/oc/api/docs/Whisper.java
Normal file
7
API/api/src/main/java/tc/oc/api/docs/Whisper.java
Normal 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 {}
|
28
API/api/src/main/java/tc/oc/api/docs/team.java
Normal file
28
API/api/src/main/java/tc/oc/api/docs/team.java
Normal 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 {}
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
package tc.oc.api.docs.virtual;
|
||||
|
||||
public class BasicDocument implements Document {
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
package tc.oc.api.docs.virtual;
|
||||
|
||||
public interface CompetitorDoc extends Model {}
|
50
API/api/src/main/java/tc/oc/api/docs/virtual/DeathDoc.java
Normal file
50
API/api/src/main/java/tc/oc/api/docs/virtual/DeathDoc.java
Normal 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();
|
||||
}
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
23
API/api/src/main/java/tc/oc/api/docs/virtual/DeployInfo.java
Normal file
23
API/api/src/main/java/tc/oc/api/docs/virtual/DeployInfo.java
Normal 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();
|
||||
}
|
28
API/api/src/main/java/tc/oc/api/docs/virtual/Document.java
Normal file
28
API/api/src/main/java/tc/oc/api/docs/virtual/Document.java
Normal 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 {}
|
@ -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();
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
62
API/api/src/main/java/tc/oc/api/docs/virtual/MapDoc.java
Normal file
62
API/api/src/main/java/tc/oc/api/docs/virtual/MapDoc.java
Normal 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();
|
||||
}
|
||||
}
|
86
API/api/src/main/java/tc/oc/api/docs/virtual/MatchDoc.java
Normal file
86
API/api/src/main/java/tc/oc/api/docs/virtual/MatchDoc.java
Normal 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();
|
||||
}
|
||||
}
|
9
API/api/src/main/java/tc/oc/api/docs/virtual/Model.java
Normal file
9
API/api/src/main/java/tc/oc/api/docs/virtual/Model.java
Normal 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(); }
|
||||
}
|
@ -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 {}
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
37
API/api/src/main/java/tc/oc/api/docs/virtual/ReportDoc.java
Normal file
37
API/api/src/main/java/tc/oc/api/docs/virtual/ReportDoc.java
Normal 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();
|
||||
}
|
||||
}
|
194
API/api/src/main/java/tc/oc/api/docs/virtual/ServerDoc.java
Normal file
194
API/api/src/main/java/tc/oc/api/docs/virtual/ServerDoc.java
Normal 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();
|
||||
}
|
||||
}
|
24
API/api/src/main/java/tc/oc/api/docs/virtual/SessionDoc.java
Normal file
24
API/api/src/main/java/tc/oc/api/docs/virtual/SessionDoc.java
Normal 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();
|
||||
}
|
||||
}
|
113
API/api/src/main/java/tc/oc/api/docs/virtual/UserDoc.java
Normal file
113
API/api/src/main/java/tc/oc/api/docs/virtual/UserDoc.java
Normal 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();
|
||||
}
|
||||
}
|
29
API/api/src/main/java/tc/oc/api/docs/virtual/WhisperDoc.java
Normal file
29
API/api/src/main/java/tc/oc/api/docs/virtual/WhisperDoc.java
Normal 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();
|
||||
}
|
||||
}
|
84
API/api/src/main/java/tc/oc/api/document/Accessor.java
Normal file
84
API/api/src/main/java/tc/oc/api/document/Accessor.java
Normal 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);
|
||||
}
|
116
API/api/src/main/java/tc/oc/api/document/BaseAccessor.java
Normal file
116
API/api/src/main/java/tc/oc/api/document/BaseAccessor.java
Normal 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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
101
API/api/src/main/java/tc/oc/api/document/DocumentMeta.java
Normal file
101
API/api/src/main/java/tc/oc/api/document/DocumentMeta.java
Normal 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()));
|
||||
}
|
||||
}
|
271
API/api/src/main/java/tc/oc/api/document/DocumentRegistry.java
Normal file
271
API/api/src/main/java/tc/oc/api/document/DocumentRegistry.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
104
API/api/src/main/java/tc/oc/api/document/DocumentSerializer.java
Normal file
104
API/api/src/main/java/tc/oc/api/document/DocumentSerializer.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
35
API/api/src/main/java/tc/oc/api/document/FieldAccessor.java
Normal file
35
API/api/src/main/java/tc/oc/api/document/FieldAccessor.java
Normal 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);
|
||||
}
|
||||
}
|
30
API/api/src/main/java/tc/oc/api/document/FieldGetter.java
Normal file
30
API/api/src/main/java/tc/oc/api/document/FieldGetter.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
40
API/api/src/main/java/tc/oc/api/document/FieldSetter.java
Normal file
40
API/api/src/main/java/tc/oc/api/document/FieldSetter.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
18
API/api/src/main/java/tc/oc/api/document/Getter.java
Normal file
18
API/api/src/main/java/tc/oc/api/document/Getter.java
Normal 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);
|
||||
}
|
62
API/api/src/main/java/tc/oc/api/document/GetterMethod.java
Normal file
62
API/api/src/main/java/tc/oc/api/document/GetterMethod.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
26
API/api/src/main/java/tc/oc/api/document/Setter.java
Normal file
26
API/api/src/main/java/tc/oc/api/document/Setter.java
Normal 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);
|
||||
}
|
76
API/api/src/main/java/tc/oc/api/document/SetterMethod.java
Normal file
76
API/api/src/main/java/tc/oc/api/document/SetterMethod.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
53
API/api/src/main/java/tc/oc/api/exceptions/ApiException.java
Normal file
53
API/api/src/main/java/tc/oc/api/exceptions/ApiException.java
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
14
API/api/src/main/java/tc/oc/api/exceptions/Conflict.java
Normal file
14
API/api/src/main/java/tc/oc/api/exceptions/Conflict.java
Normal 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);
|
||||
}
|
||||
}
|
14
API/api/src/main/java/tc/oc/api/exceptions/Forbidden.java
Normal file
14
API/api/src/main/java/tc/oc/api/exceptions/Forbidden.java
Normal 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);
|
||||
}
|
||||
}
|
23
API/api/src/main/java/tc/oc/api/exceptions/NotFound.java
Normal file
23
API/api/src/main/java/tc/oc/api/exceptions/NotFound.java
Normal 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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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 + "\"";
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
41
API/api/src/main/java/tc/oc/api/games/ArenaStore.java
Normal file
41
API/api/src/main/java/tc/oc/api/games/ArenaStore.java
Normal 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);
|
||||
}
|
||||
}
|
35
API/api/src/main/java/tc/oc/api/games/GameModelManifest.java
Normal file
35
API/api/src/main/java/tc/oc/api/games/GameModelManifest.java
Normal 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);
|
||||
}
|
||||
}
|
10
API/api/src/main/java/tc/oc/api/games/GameStore.java
Normal file
10
API/api/src/main/java/tc/oc/api/games/GameStore.java
Normal 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> {
|
||||
}
|
23
API/api/src/main/java/tc/oc/api/games/NullTicketService.java
Normal file
23
API/api/src/main/java/tc/oc/api/games/NullTicketService.java
Normal 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);
|
||||
}
|
||||
}
|
16
API/api/src/main/java/tc/oc/api/games/TicketService.java
Normal file
16
API/api/src/main/java/tc/oc/api/games/TicketService.java
Normal 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);
|
||||
}
|
54
API/api/src/main/java/tc/oc/api/games/TicketStore.java
Normal file
54
API/api/src/main/java/tc/oc/api/games/TicketStore.java
Normal 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);
|
||||
}
|
||||
}
|
307
API/api/src/main/java/tc/oc/api/http/HttpClient.java
Normal file
307
API/api/src/main/java/tc/oc/api/http/HttpClient.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
14
API/api/src/main/java/tc/oc/api/http/HttpManifest.java
Normal file
14
API/api/src/main/java/tc/oc/api/http/HttpManifest.java
Normal 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);
|
||||
}
|
||||
}
|
12
API/api/src/main/java/tc/oc/api/http/HttpOption.java
Normal file
12
API/api/src/main/java/tc/oc/api/http/HttpOption.java
Normal 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
|
||||
}
|
40
API/api/src/main/java/tc/oc/api/http/QueryUri.java
Normal file
40
API/api/src/main/java/tc/oc/api/http/QueryUri.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
18
API/api/src/main/java/tc/oc/api/maps/MapModelManifest.java
Normal file
18
API/api/src/main/java/tc/oc/api/maps/MapModelManifest.java
Normal 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);
|
||||
}
|
||||
}
|
25
API/api/src/main/java/tc/oc/api/maps/MapRatingsRequest.java
Normal file
25
API/api/src/main/java/tc/oc/api/maps/MapRatingsRequest.java
Normal 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());
|
||||
}
|
||||
}
|
12
API/api/src/main/java/tc/oc/api/maps/MapRatingsResponse.java
Normal file
12
API/api/src/main/java/tc/oc/api/maps/MapRatingsResponse.java
Normal 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();
|
||||
}
|
17
API/api/src/main/java/tc/oc/api/maps/MapService.java
Normal file
17
API/api/src/main/java/tc/oc/api/maps/MapService.java
Normal 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
Loading…
x
Reference in New Issue
Block a user