Skip to content

Commit

Permalink
Cleanup, graphql type registries and a GraphiQL endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
florensie committed Apr 20, 2021
1 parent ae30750 commit 915324b
Show file tree
Hide file tree
Showing 17 changed files with 338 additions and 107 deletions.
83 changes: 16 additions & 67 deletions src/main/java/be/florens/craftql/CraftQL.java
Original file line number Diff line number Diff line change
@@ -1,49 +1,36 @@
package be.florens.craftql;

import be.florens.craftql.servlet.GraphQLServlet;
import be.florens.craftql.servlet.SubscriptionEndpoint;
import be.florens.craftql.servlet.SubscriptionEndpointConfigurer;
import graphql.kickstart.servlet.GraphQLConfiguration;
import graphql.schema.GraphQLSchema;
import be.florens.craftql.resolver.Resolvers;
import be.florens.craftql.scalar.Scalars;
import be.florens.craftql.servlet.GraphQLContext;
import net.fabricmc.api.DedicatedServerModInitializer;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
import net.fabricmc.fabric.api.resource.ResourceManagerHelper;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.resource.ResourceType;
import net.minecraft.server.MinecraftServer;
import net.minecraft.util.Identifier;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.eclipse.jetty.websocket.jsr356.server.deploy.WebSocketServerContainerInitializer;

import javax.websocket.server.ServerEndpointConfig;
import java.util.Optional;

public class CraftQL implements DedicatedServerModInitializer {

public static final String MOD_ID = "craftql";
public static final Logger LOGGER = LogManager.getLogger();
public static final int PORT = 8080;
public static GraphQLConfiguration graphQLConfig;
private static Server graphQLServer;
private static GraphQLContext graphQLContext;

@Override
public void onInitializeServer() {
ResourceManagerHelper.get(ResourceType.SERVER_DATA).registerReloadListener(new SchemaResourceReloadListener());

// Make sure we don't hang Minecraft when the server is stopped
ServerLifecycleEvents.SERVER_STOPPED.register(server -> {
if (graphQLServer != null) {
try {
graphQLServer.stop();
} catch (Exception e) {
LOGGER.error(String.format("[%s] Exception occured while stopping GraphQL server thread", MOD_ID), e);
}
}
});
// Register default GraphQL types
Resolvers.registerAll();
Scalars.registerAll();
}

public static Identifier id(String path) {
return new Identifier(MOD_ID, path);
}

public static Optional<MinecraftServer> getMinecraftServer() {
Expand All @@ -55,49 +42,11 @@ public static Optional<MinecraftServer> getMinecraftServer() {
return Optional.empty();
}

private static void startGraphQLServer() {
// Basic server init
graphQLServer = new Server();
ServerConnector connector = new ServerConnector(graphQLServer);
connector.setPort(PORT);
graphQLServer.addConnector(connector);

// Sets the prefix to use for the names of the threads
if (graphQLServer.getThreadPool() instanceof QueuedThreadPool) {
((QueuedThreadPool) graphQLServer.getThreadPool()).setName(String.format("%s-server", MOD_ID));
public static GraphQLContext getOrCreateGraphQLContext() {
if (graphQLContext == null) {
graphQLContext = new GraphQLContext(8080);
}

// GraphQL servlet
ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
context.setContextPath("/");

// HTTP endpoint
context.addServlet(GraphQLServlet.class, "/graphql");
graphQLServer.setHandler(context);

// Websocket endpoint (for subscriptions)
WebSocketServerContainerInitializer.configure(context, (servletContext, serverContainer) -> {
serverContainer.addEndpoint(ServerEndpointConfig.Builder
.create(SubscriptionEndpoint.class, "/subscriptions")
.configurator(new SubscriptionEndpointConfigurer())
.build());
});
graphQLServer.setHandler(context);

// Start the server threads
try {
graphQLServer.setStopAtShutdown(true); // This doesn't appear do anything but it's safe to leave it
graphQLServer.start();
} catch (Exception e) {
LOGGER.error(String.format("[%s] Exception in GraphQL server thread", MOD_ID), e);
}
}

public static void updateGraphQLSchema(GraphQLSchema schema) {
graphQLConfig = GraphQLConfiguration.with(schema).build();

if (graphQLServer == null) {
startGraphQLServer();
}
return graphQLContext;
}
}
27 changes: 6 additions & 21 deletions src/main/java/be/florens/craftql/SchemaResourceReloadListener.java
Original file line number Diff line number Diff line change
@@ -1,43 +1,28 @@
package be.florens.craftql;

import be.florens.craftql.resolver.PlayerInventoryResolver;
import be.florens.craftql.resolver.RootQueryResolver;
import be.florens.craftql.scalar.Scalars;
import com.fasterxml.jackson.databind.ObjectMapper;
import graphql.kickstart.tools.ObjectMapperConfigurer;
import graphql.kickstart.tools.ObjectMapperConfigurerContext;
import graphql.kickstart.tools.PerFieldObjectMapperProvider;
import graphql.kickstart.tools.SchemaParser;
import graphql.kickstart.tools.SchemaParserBuilder;
import graphql.kickstart.tools.SchemaParserOptions;
import graphql.language.FieldDefinition;
import net.fabricmc.fabric.api.resource.SimpleSynchronousResourceReloadListener;
import net.minecraft.resource.ResourceManager;
import net.minecraft.util.Identifier;
import org.apache.commons.io.IOUtils;
import org.jetbrains.annotations.NotNull;

import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class SchemaResourceReloadListener implements SimpleSynchronousResourceReloadListener {

@Override
public Identifier getFabricId() {
return new Identifier(CraftQL.MOD_ID, "schemas");
return CraftQL.id("schemas");
}

@Override
public void apply(ResourceManager manager) {
// Base schema builder with resolvers and scalars
SchemaParserBuilder schemaParserBuilder = SchemaParser.newParser()
.resolvers(new RootQueryResolver(), new PlayerInventoryResolver())
.scalars(Scalars.TEXT);

// Read from schema files
manager.findResources("schemas", path -> path.endsWith(".graphqls")).stream()
List<String> rawSchemas = manager.findResources("schemas", path -> path.endsWith(".graphqls")).stream()
.flatMap(id -> {
try (InputStream stream = manager.getResource(id).getInputStream()) {
String schemaString = IOUtils.toString(stream, StandardCharsets.UTF_8);
Expand All @@ -49,11 +34,11 @@ public void apply(ResourceManager manager) {
return Stream.empty();
}
})
.forEach(schemaParserBuilder::schemaString);
.collect(Collectors.toList());

// Update the server configuration
long start = System.currentTimeMillis();
CraftQL.updateGraphQLSchema(schemaParserBuilder.build().makeExecutableSchema());
CraftQL.getOrCreateGraphQLContext().parseAndApplySchemas(rawSchemas);
CraftQL.LOGGER.info(String.format("[%s] Schema was parsed in %s milliseconds", CraftQL.MOD_ID, System.currentTimeMillis() - start));
}
}
18 changes: 18 additions & 0 deletions src/main/java/be/florens/craftql/api/Registries.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package be.florens.craftql.api;

import be.florens.craftql.CraftQL;
import graphql.kickstart.tools.GraphQLResolver;
import graphql.schema.GraphQLScalarType;
import net.fabricmc.fabric.api.event.registry.FabricRegistryBuilder;
import net.minecraft.util.registry.Registry;

public class Registries {

public static final Registry<GraphQLScalarType> SCALARS = FabricRegistryBuilder.createSimple(GraphQLScalarType.class,
CraftQL.id("scalars")).buildAndRegister();
@SuppressWarnings("unchecked")
public static final Registry<GraphQLResolver<?>> RESOLVERS = FabricRegistryBuilder.createDefaulted(
(Class<GraphQLResolver<?>>) (Class<?>) GraphQLResolver.class, CraftQL.id("resolvers"),
CraftQL.id("root_query")).buildAndRegister();
// TODO: directives
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ public class YarnMappingResolver {
private static final String CURRENT_NAMESPACE = FabricLoader.getInstance().getMappingResolver()
.getCurrentRuntimeNamespace();
private static YarnMappingResolver instance;
private final TinyTree tinyTree;
private final MappingResolver mappingResolver;

private YarnMappingResolver() {
Expand All @@ -30,22 +29,23 @@ private YarnMappingResolver() {
.orElseThrow(() -> new RuntimeException("Yarn Jar-in-Jar was not loaded"))
.getPath("mappings/mappings.tiny");

TinyTree tinyTree;
try {
BufferedReader mappingsReader = Files.newBufferedReader(mappingsPath);
tinyTree = TinyMappingFactory.loadWithDetection(mappingsReader);
} catch (IOException e) {
throw new RuntimeException("Yarn mappings could not be read");
throw new RuntimeException("Yarn mappings could not be read", e);
}

// Create MappingResolver
try {
// Get FabricMappingResolver constructor
Class<?> clazz = Class.forName("net.fabricmc.loader.FabricMappingResolver");
Constructor<?> constructor = clazz.getDeclaredConstructor(Supplier.class, String.class);
constructor.setAccessible(true);

// Create a new instance
mappingResolver = (net.fabricmc.loader.api.MappingResolver) constructor.newInstance(
(Supplier<TinyTree>) () -> tinyTree, "named");
mappingResolver = (MappingResolver) constructor.newInstance((Supplier<TinyTree>) () -> tinyTree, "named");
} catch (Exception e) {
throw new RuntimeException("Failed to create FabricMappingResolver", e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,12 @@
import graphql.kickstart.tools.GraphQLResolver;
import net.minecraft.entity.player.PlayerInventory;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.Tag;

import java.util.ArrayList;
import java.util.List;

// TODO: Generalize for all inventories
public class PlayerInventoryResolver implements GraphQLResolver<PlayerInventory> {
public class InventoryResolver implements GraphQLResolver<PlayerInventory> {

public List<ItemStack> getStacks(PlayerInventory inventory) {
ArrayList<ItemStack> stacks = new ArrayList<>();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package be.florens.craftql.resolver;

import graphql.kickstart.tools.GraphQLMutationResolver;

public class MutationResolver implements GraphQLMutationResolver {

}
25 changes: 25 additions & 0 deletions src/main/java/be/florens/craftql/resolver/Resolvers.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package be.florens.craftql.resolver;

import be.florens.craftql.CraftQL;
import be.florens.craftql.api.Registries;
import graphql.kickstart.tools.GraphQLResolver;
import net.minecraft.util.registry.Registry;

public class Resolvers {

public static final GraphQLResolver<?> ROOT_QUERY = new RootQueryResolver();
public static final GraphQLResolver<?> MUTATION = new MutationResolver();
public static final GraphQLResolver<?> SUBSCRIPTION = new SubscriptionResolver();
public static final GraphQLResolver<?> INVENTORY = new InventoryResolver();

public static void registerAll() {
registerResolver("root_query", ROOT_QUERY);
registerResolver("mutation", MUTATION);
registerResolver("subscription", SUBSCRIPTION);
registerResolver("inventory", INVENTORY);
}

private static void registerResolver(String path, GraphQLResolver<?> resolver) {
Registry.register(Registries.RESOLVERS, CraftQL.id(path), resolver);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

import be.florens.craftql.CraftQL;
import graphql.kickstart.tools.GraphQLQueryResolver;
import net.minecraft.server.network.ServerPlayerEntity;

import java.util.Collections;
import java.util.List;
import net.minecraft.server.network.ServerPlayerEntity;

public class RootQueryResolver implements GraphQLQueryResolver {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package be.florens.craftql.resolver;

import graphql.kickstart.tools.GraphQLSubscriptionResolver;
import net.fabricmc.fabric.api.event.player.PlayerBlockBreakEvents;
import net.minecraft.server.network.ServerPlayerEntity;
import org.reactivestreams.Publisher;
import org.reactivestreams.Subscriber;

public class SubscriptionResolver implements GraphQLSubscriptionResolver {

public Publisher<ServerPlayerEntity> onBreakBlock() {
//noinspection ReactiveStreamsPublisherImplementation
return new Publisher<ServerPlayerEntity>() {
private int count;

@Override
public void subscribe(Subscriber s) {
PlayerBlockBreakEvents.AFTER.register((world, player, pos, state, blockEntity) -> {
count++;
if (count == 10) {
s.onComplete();
} else {
//noinspection unchecked
s.onNext(player);
}
});
}
};
}
}
14 changes: 12 additions & 2 deletions src/main/java/be/florens/craftql/scalar/Scalars.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
package be.florens.craftql.scalar;

import be.florens.craftql.CraftQL;
import be.florens.craftql.api.Registries;
import graphql.schema.GraphQLScalarType;
import net.minecraft.util.registry.Registry;

public class Scalars {

public static final GraphQLScalarType TEXT = GraphQLScalarType.newScalar().name("Text")
.coercing(new TextCoercing()).build();
public static final GraphQLScalarType TEXT = GraphQLScalarType.newScalar().name("Text").coercing(new TextCoercing()).build();

public static void registerAll() {
registerScalar("text", TEXT);
}

private static void registerScalar(String path, GraphQLScalarType scalar) {
Registry.register(Registries.SCALARS, CraftQL.id(path), scalar);
}
}
Loading

0 comments on commit 915324b

Please sign in to comment.