Rename & Repackage

This commit is contained in:
dark98
2021-03-11 00:21:56 +00:00
parent caa4625efa
commit 3bf473129d
1921 changed files with 2017 additions and 2010 deletions
+4
View File
@@ -0,0 +1,4 @@
/bin/
*.iml
/target/
file_server-1.0-jar-with-dependencies.jar
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+118
View File
@@ -0,0 +1,118 @@
<?xml version="1.0" encoding="UTF-8"?>
<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">
<parent>
<artifactId>2006Scape</artifactId>
<groupId>com.rs2</groupId>
<version>1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>file_server</artifactId>
<version>1.0</version>
<packaging>jar</packaging>
<repositories>
<repository>
<id>libs-local</id>
<name>libs</name>
<url>file://${project.basedir}/libs</url>
</repository>
</repositories>
<dependencies>
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.mina/mina-core -->
<dependency>
<groupId>org.apache.mina</groupId>
<artifactId>mina-core</artifactId>
<version>1.1.7</version>
</dependency>
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.16</version>
</dependency>
<!-- https://mvnrepository.com/artifact/io.netty/netty -->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty</artifactId>
<version>3.6.6.Final</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-api -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.5.8</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-nop -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-nop</artifactId>
<version>1.5.8</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/com.thoughtworks.xstream/xstream -->
<dependency>
<groupId>com.thoughtworks.xstream</groupId>
<artifactId>xstream</artifactId>
<version>1.4.10-java7</version>
</dependency>
<dependency>
<groupId>javac</groupId>
<artifactId>javac</artifactId>
<version>1.0</version>
<scope>system</scope>
<systemPath>${project.basedir}/libs/javac++.jar</systemPath>
</dependency>
<dependency>
<groupId>org.xmlpull.v1</groupId>
<artifactId>xpp3</artifactId>
<version>1.1.4c</version>
<scope>system</scope>
<systemPath>${project.basedir}/libs/xpp3-1.1.4c.jar</systemPath>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<archive>
<manifest>
<mainClass>org.apollo.jagcached.FileServer</mainClass>
</manifest>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
<executions>
<execution>
<id>make-assembly</id> <!-- this is used for inheritance merges -->
<phase>package</phase> <!-- bind to the packaging phase -->
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
+6
View File
@@ -0,0 +1,6 @@
echo This is meant to be run by the live server admin! You probably don\'t want to do this!
for i in {0..50}
do
cp target/file_server-1.0-jar-with-dependencies.jar ./fserver.jar
java -jar fserver.jar
done
@@ -0,0 +1,26 @@
package org.apollo.jagcached;
/**
* 2006Scape Development
*
* @author Ryley Kimmel <ryley.kimmel@live.com>
* Jul 9, 2013
* Constants.java
*
* @see java.lang.Object
*/
public final class Constants {
/**
* The directory of the file system.
*/
public static final String FILE_SYSTEM_DIR = "./cache/";
/**
* Default private constructor to prevent instantiation.
*/
private Constants() {
super();
}
}
@@ -0,0 +1,119 @@
package org.apollo.jagcached;
import java.io.File;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apollo.jagcached.dispatch.RequestWorkerPool;
import org.apollo.jagcached.net.FileServerHandler;
import org.apollo.jagcached.net.HttpPipelineFactory;
import org.apollo.jagcached.net.JagGrabPipelineFactory;
import org.apollo.jagcached.net.NetworkConstants;
import org.apollo.jagcached.net.OnDemandPipelineFactory;
import org.jboss.netty.bootstrap.ServerBootstrap;
import org.jboss.netty.channel.ChannelPipelineFactory;
import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory;
import org.jboss.netty.util.HashedWheelTimer;
import org.jboss.netty.util.Timer;
/**
* The core class of the file server.
* @author Graham
*/
public final class FileServer {
/**
* The logger for this class.
*/
private static final Logger logger = Logger.getLogger(FileServer.class.getName());
/**
* The entry point of the application.
* @param args The command-line arguments.
*/
public static void main(String[] args) {
try {
new FileServer().start();
} catch (Throwable t) {
logger.log(Level.SEVERE, "Error starting server.", t);
}
}
/**
* The executor service.
*/
private final ExecutorService service = Executors.newCachedThreadPool();
/**
* The request worker pool.
*/
private final RequestWorkerPool pool = new RequestWorkerPool();
/**
* The file server event handler.
*/
private final FileServerHandler handler = new FileServerHandler();
/**
* The timer used for idle checking.
*/
private final Timer timer = new HashedWheelTimer();
/**
* Starts the file server.
* @throws Exception if an error occurs.
*/
public void start() throws Exception {
if (!new File("cache").exists())
{
System.out.println("************************************");
System.out.println("************************************");
System.out.println("************************************");
System.out.println("WARNING: I could not find the /cache folder. You are LIKELY running this in the wrong directory!");
System.out.println("In IntelliJ, fix it by clicking \"FileServer\" > Edit Configurations at the top of your screen");
System.out.println("Then changing the \"Working Directory\" to be in \"2006Scape/2006Scape file_server\", instead of just \"2006Scape\"");
System.out.println("************************************");
System.out.println("************************************");
System.out.println("************************************");
System.exit(1);
}
logger.info("Starting workers...");
pool.start();
logger.info("Starting services...");
try {
start("HTTP", new HttpPipelineFactory(handler, timer), NetworkConstants.HTTP_PORT);
} catch (Throwable t) {
logger.log(Level.SEVERE, "Failed to start HTTP service.", t);
logger.warning("HTTP will be unavailable. JAGGRAB will be used as a fallback by clients but this isn't reccomended!");
}
start("JAGGRAB", new JagGrabPipelineFactory(handler, timer), NetworkConstants.JAGGRAB_PORT);
start("ondemand", new OnDemandPipelineFactory(handler, timer), NetworkConstants.SERVICE_PORT);
logger.info("Ready for connections.");
}
/**
* Starts the specified service.
* @param name The name of the service.
* @param pipelineFactory The pipeline factory.
* @param port The port.
*/
private void start(String name, ChannelPipelineFactory pipelineFactory, int port) {
SocketAddress address = new InetSocketAddress(port);
logger.info("Binding " + name + " service to " + address + "...");
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.setFactory(new NioServerSocketChannelFactory(service, service));
bootstrap.setPipelineFactory(pipelineFactory);
bootstrap.bind(address);
}
}
@@ -0,0 +1,58 @@
package org.apollo.jagcached.dispatch;
import org.jboss.netty.channel.Channel;
/**
* A specialised request which contains a channel as well as the request object
* itself.
* @author Graham
* @param <T> The type of request.
*/
public final class ChannelRequest<T> implements Comparable<ChannelRequest<T>> {
/**
* The channel.
*/
private final Channel channel;
/**
* The request.
*/
private final T request;
/**
* Creates a new channel request.
* @param channel The channel.
* @param request The request.
*/
public ChannelRequest(Channel channel, T request) {
this.channel = channel;
this.request = request;
}
/**
* Gets the channel.
* @return The channel.
*/
public Channel getChannel() {
return channel;
}
/**
* Gets the request.
* @return The request.
*/
public T getRequest() {
return request;
}
@SuppressWarnings("unchecked")
@Override
public int compareTo(ChannelRequest<T> o) {
if (request instanceof Comparable<?> && o.request instanceof Comparable<?>) {
return ((Comparable<T>) request).compareTo(o.request);
}
return 0;
}
}
@@ -0,0 +1,139 @@
package org.apollo.jagcached.dispatch;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.Date;
import org.apollo.jagcached.fs.IndexedFileSystem;
import org.apollo.jagcached.resource.CombinedResourceProvider;
import org.apollo.jagcached.resource.HypertextResourceProvider;
import org.apollo.jagcached.resource.ResourceProvider;
import org.apollo.jagcached.resource.VirtualResourceProvider;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.buffer.ChannelBuffers;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelFutureListener;
import org.jboss.netty.handler.codec.http.DefaultHttpResponse;
import org.jboss.netty.handler.codec.http.HttpRequest;
import org.jboss.netty.handler.codec.http.HttpResponse;
import org.jboss.netty.handler.codec.http.HttpResponseStatus;
/**
* A worker which services HTTP requests.
* @author Graham
*/
public final class HttpRequestWorker extends RequestWorker<HttpRequest, ResourceProvider> {
/**
* The value of the server header.
*/
private static final String SERVER_IDENTIFIER = "JAGeX/3.1";
/**
* The directory with web files.
*/
private static final File WWW_DIRECTORY = new File("./data/www/");
/**
* The default character set.
*/
private static final Charset CHARACTER_SET = Charset.forName("ISO-8859-1");
/**
* Creates the HTTP request worker.
* @param fs The file system.
*/
public HttpRequestWorker(IndexedFileSystem fs) {
super(new CombinedResourceProvider(new VirtualResourceProvider(fs), new HypertextResourceProvider(WWW_DIRECTORY)));
}
@Override
protected ChannelRequest<HttpRequest> nextRequest() throws InterruptedException {
return RequestDispatcher.nextHttpRequest();
}
@Override
protected void service(ResourceProvider provider, Channel channel, HttpRequest request) throws IOException {
String path = request.getUri();
ByteBuffer buf = provider.get(path);
ChannelBuffer wrappedBuf;
HttpResponseStatus status = HttpResponseStatus.OK;
String mimeType = getMimeType(request.getUri());
if (buf == null) {
status = HttpResponseStatus.NOT_FOUND;
wrappedBuf = createErrorPage(status, "The page you requested could not be found.");
mimeType = "text/html";
} else {
wrappedBuf = ChannelBuffers.wrappedBuffer(buf);
}
HttpResponse resp = new DefaultHttpResponse(request.getProtocolVersion(), status);
resp.setHeader("Date", new Date());
resp.setHeader("Server", SERVER_IDENTIFIER);
resp.setHeader("Content-type", mimeType + ", charset=" + CHARACTER_SET.name());
resp.setHeader("Cache-control", "no-cache");
resp.setHeader("Pragma", "no-cache");
resp.setHeader("Expires", new Date(0));
resp.setHeader("Connection", "close");
resp.setHeader("Content-length", wrappedBuf.readableBytes());
resp.setChunked(false);
resp.setContent(wrappedBuf);
channel.write(resp).addListener(ChannelFutureListener.CLOSE);
}
/**
* Gets the MIME type of a file by its name.
* @param name The file name.
* @return The MIME type.
*/
private String getMimeType(String name) {
if (name.endsWith(".htm") || name.endsWith(".html")) {
return "text/html";
} else if (name.endsWith(".css")) {
return "text/css";
} else if (name.endsWith(".js")) {
return "text/javascript";
} else if (name.endsWith(".jpg") || name.endsWith(".jpeg")) {
return "image/jpeg";
} else if (name.endsWith(".gif")) {
return "image/gif";
} else if (name.endsWith(".png")) {
return "image/png";
} else if (name.endsWith(".txt")) {
return "text/plain";
}
return "application/octect-stream";
}
/**
* Creates an error page.
* @param status The HTTP status.
* @param description The error description.
* @return The error page as a buffer.
*/
private ChannelBuffer createErrorPage(HttpResponseStatus status, String description) {
String title = status.getCode() + " " + status.getReasonPhrase();
StringBuilder bldr = new StringBuilder();
bldr.append("<!DOCTYPE html><html><head><title>");
bldr.append(title);
bldr.append("</title></head><body><h1>");
bldr.append(title);
bldr.append("</h1><p>");
bldr.append(description);
bldr.append("</p><hr /><address>");
bldr.append(SERVER_IDENTIFIER);
bldr.append(" Server</address></body></html>");
return ChannelBuffers.copiedBuffer(bldr.toString(), Charset.defaultCharset());
}
}
@@ -0,0 +1,46 @@
package org.apollo.jagcached.dispatch;
import java.io.IOException;
import java.nio.ByteBuffer;
import org.apollo.jagcached.fs.IndexedFileSystem;
import org.apollo.jagcached.net.jaggrab.JagGrabRequest;
import org.apollo.jagcached.net.jaggrab.JagGrabResponse;
import org.apollo.jagcached.resource.ResourceProvider;
import org.apollo.jagcached.resource.VirtualResourceProvider;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.buffer.ChannelBuffers;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelFutureListener;
/**
* A worker which services JAGGRAB requests.
* @author Graham
*/
public final class JagGrabRequestWorker extends RequestWorker<JagGrabRequest, ResourceProvider> {
/**
* Creates the JAGGRAB request worker.
* @param fs The file system.
*/
public JagGrabRequestWorker(IndexedFileSystem fs) {
super(new VirtualResourceProvider(fs));
}
@Override
protected ChannelRequest<JagGrabRequest> nextRequest() throws InterruptedException {
return RequestDispatcher.nextJagGrabRequest();
}
@Override
protected void service(ResourceProvider provider, Channel channel, JagGrabRequest request) throws IOException {
ByteBuffer buf = provider.get(request.getFilePath());
if (buf == null) {
channel.close();
} else {
ChannelBuffer wrapped = ChannelBuffers.wrappedBuffer(buf);
channel.write(new JagGrabResponse(wrapped)).addListener(ChannelFutureListener.CLOSE);
}
}
}
@@ -0,0 +1,61 @@
package org.apollo.jagcached.dispatch;
import java.io.IOException;
import java.nio.ByteBuffer;
import org.apollo.jagcached.fs.FileDescriptor;
import org.apollo.jagcached.fs.IndexedFileSystem;
import org.apollo.jagcached.net.ondemand.OnDemandRequest;
import org.apollo.jagcached.net.ondemand.OnDemandResponse;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.buffer.ChannelBuffers;
import org.jboss.netty.channel.Channel;
/**
* A worker which services 'on-demand' requests.
* @author Graham
*/
public final class OnDemandRequestWorker extends RequestWorker<OnDemandRequest, IndexedFileSystem> {
/**
* The maximum length of a chunk, in bytes.
*/
private static final int CHUNK_LENGTH = 500;
/**
* Creates the 'on-demand' request worker.
* @param fs The file system.
*/
public OnDemandRequestWorker(IndexedFileSystem fs) {
super(fs);
}
@Override
protected ChannelRequest<OnDemandRequest> nextRequest() throws InterruptedException {
return RequestDispatcher.nextOnDemandRequest();
}
@Override
protected void service(IndexedFileSystem fs, Channel channel, OnDemandRequest request) throws IOException {
FileDescriptor desc = request.getFileDescriptor();
ByteBuffer buf = fs.getFile(desc);
int length = buf.remaining();
for (int chunk = 0; buf.remaining() > 0; chunk++) {
int chunkSize = buf.remaining();
if (chunkSize > CHUNK_LENGTH) {
chunkSize = CHUNK_LENGTH;
}
byte[] tmp = new byte[chunkSize];
buf.get(tmp, 0, tmp.length);
ChannelBuffer chunkData = ChannelBuffers.wrappedBuffer(tmp, 0, chunkSize);
OnDemandResponse response = new OnDemandResponse(desc, length, chunk, chunkData);
channel.write(response);
}
}
}
@@ -0,0 +1,112 @@
package org.apollo.jagcached.dispatch;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.PriorityBlockingQueue;
import org.apollo.jagcached.net.jaggrab.JagGrabRequest;
import org.apollo.jagcached.net.ondemand.OnDemandRequest;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.handler.codec.http.HttpRequest;
/**
* A class which dispatches requests to worker threads.
* @author Graham
*/
public final class RequestDispatcher {
/**
* The maximum size of a queue before requests are rejected.
*/
private static final int MAXIMUM_QUEUE_SIZE = 1024;
/**
* A queue for pending 'on-demand' requests.
*/
private static final BlockingQueue<ChannelRequest<OnDemandRequest>> onDemandQueue = new PriorityBlockingQueue<ChannelRequest<OnDemandRequest>>();
/**
* A queue for pending JAGGRAB requests.
*/
private static final BlockingQueue<ChannelRequest<JagGrabRequest>> jagGrabQueue = new LinkedBlockingQueue<ChannelRequest<JagGrabRequest>>();
/**
* A queue for pending HTTP requests.
*/
private static final BlockingQueue<ChannelRequest<HttpRequest>> httpQueue = new LinkedBlockingQueue<ChannelRequest<HttpRequest>>();
/**
* Gets the next 'on-demand' request from the queue, blocking if none are
* available.
* @return The 'on-demand' request.
* @throws InterruptedException if the thread is interrupted.
*/
static ChannelRequest<OnDemandRequest> nextOnDemandRequest() throws InterruptedException {
return onDemandQueue.take();
}
/**
* Gets the next JAGGRAB request from the queue, blocking if none are
* available.
* @return The JAGGRAB request.
* @throws InterruptedException if the thread is interrupted.
*/
static ChannelRequest<JagGrabRequest> nextJagGrabRequest() throws InterruptedException {
return jagGrabQueue.take();
}
/**
* Gets the next HTTP request from the queue, blocking if none are
* available.
* @return The HTTP request.
* @throws InterruptedException if the thread is interrupted.
*/
static ChannelRequest<HttpRequest> nextHttpRequest() throws InterruptedException {
return httpQueue.take();
}
/**
* Dispatches an 'on-demand' request.
* @param channel The channel.
* @param request The request.
*/
public static void dispatch(Channel channel, OnDemandRequest request) {
if (onDemandQueue.size() >= MAXIMUM_QUEUE_SIZE) {
channel.close();
}
onDemandQueue.add(new ChannelRequest<OnDemandRequest>(channel, request));
}
/**
* Dispatches a JAGGRAB request.
* @param channel The channel.
* @param request The request.
*/
public static void dispatch(Channel channel, JagGrabRequest request) {
if (jagGrabQueue.size() >= MAXIMUM_QUEUE_SIZE) {
channel.close();
}
jagGrabQueue.add(new ChannelRequest<JagGrabRequest>(channel, request));
}
/**
* Dispatches a HTTP request.
* @param channel The channel.
* @param request The request.
*/
public static void dispatch(Channel channel, HttpRequest request) {
if (httpQueue.size() >= MAXIMUM_QUEUE_SIZE) {
channel.close();
}
httpQueue.add(new ChannelRequest<HttpRequest>(channel, request));
}
/**
* Default private constructor to prevent instantiation.
*/
private RequestDispatcher() {
}
}
@@ -0,0 +1,90 @@
package org.apollo.jagcached.dispatch;
import java.io.IOException;
import org.jboss.netty.channel.Channel;
/**
* The base class for request workers.
* @author Graham
* @param <T> The type of request.
* @param <P> The type of provider.
*/
public abstract class RequestWorker<T, P> implements Runnable {
/**
* The resource provider.
*/
private final P provider;
/**
* An object used for locking checks to see if the worker is running.
*/
private final Object lock = new Object();
/**
* A flag indicating if the worker should be running.
*/
private boolean running = true;
/**
* Creates the request worker with the specified file system.
* @param provider The resource provider.
*/
public RequestWorker(P provider) {
this.provider = provider;
}
/**
* Stops this worker. The worker's thread may need to be interrupted.
*/
public final void stop() {
synchronized (lock) {
running = false;
}
}
@Override
public final void run() {
while (true) {
synchronized (lock) {
if (!running) {
break;
}
}
ChannelRequest<T> request;
try {
request = nextRequest();
} catch (InterruptedException e) {
continue;
}
Channel channel = request.getChannel();
try {
service(provider, channel, request.getRequest());
} catch (IOException e) {
e.printStackTrace();
channel.close();
}
}
}
/**
* Gets the next request.
* @return The next request.
* @throws InterruptedException if the thread is interrupted.
*/
protected abstract ChannelRequest<T> nextRequest() throws InterruptedException;
/**
* Services a request.
* @param provider The resource provider.
* @param channel The channel.
* @param request The request to service.
* @throws IOException if an I/O error occurs.
*/
protected abstract void service(P provider, Channel channel, T request) throws IOException;
}
@@ -0,0 +1,75 @@
package org.apollo.jagcached.dispatch;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.apollo.jagcached.Constants;
import org.apollo.jagcached.fs.IndexedFileSystem;
/**
* A class which manages the pool of request workers.
* @author Graham
* @author Ryley Kimmel <ryley.kimmel@live.com>
*/
public final class RequestWorkerPool {
/**
* The number of threads per request type.
*/
private static final int THREADS_PER_REQUEST_TYPE = Runtime.getRuntime().availableProcessors();
/**
* The number of request types.
*/
private static final int REQUEST_TYPES = 3;
/**
* The executor service.
*/
private final ExecutorService service;
/**
* A list of request workers.
*/
private final List<RequestWorker<?, ?>> workers = new ArrayList<RequestWorker<?, ?>>();
/**
* The request worker pool.
*/
public RequestWorkerPool() {
int totalThreads = REQUEST_TYPES * THREADS_PER_REQUEST_TYPE;
service = Executors.newFixedThreadPool(totalThreads);
}
/**
* Starts the threads in the pool.
* @throws Exception if the file system cannot be created.
*/
public void start() throws Exception {
File base = new File(Constants.FILE_SYSTEM_DIR);
for (int i = 0; i < THREADS_PER_REQUEST_TYPE; i++) {
workers.add(new JagGrabRequestWorker(new IndexedFileSystem(base, true)));
workers.add(new OnDemandRequestWorker(new IndexedFileSystem(base, true)));
workers.add(new HttpRequestWorker(new IndexedFileSystem(base, true)));
}
for (RequestWorker<?, ?> worker : workers) {
service.submit(worker);
}
}
/**
* Stops the threads in the pool.
*/
public void stop() {
for (RequestWorker<?, ?> worker : workers) {
worker.stop();
}
service.shutdownNow();
}
}
@@ -0,0 +1,45 @@
package org.apollo.jagcached.fs;
/**
* A class which points to a file in the cache.
* @author Graham
*/
public final class FileDescriptor {
/**
* The file type.
*/
private final int type;
/**
* The file id.
*/
private final int file;
/**
* Creates the file descriptor.
* @param type The file type.
* @param file The file id.
*/
public FileDescriptor(int type, int file) {
this.type = type;
this.file = file;
}
/**
* Gets the file type.
* @return The file type.
*/
public int getType() {
return type;
}
/**
* Gets the file id.
* @return The file id.
*/
public int getFile() {
return file;
}
}
@@ -0,0 +1,46 @@
package org.apollo.jagcached.fs;
/**
* Holds file system related constants.
* @author Graham
*/
public final class FileSystemConstants {
/**
* The number of caches.
*/
public static final int CACHE_COUNT = 5;
/**
* The number of archives in cache 0.
*/
public static final int ARCHIVE_COUNT = 9;
/**
* The size of an index.
*/
public static final int INDEX_SIZE = 6;
/**
* The size of a header.
*/
public static final int HEADER_SIZE = 8;
/**
* The size of a chunk.
*/
public static final int CHUNK_SIZE = 512;
/**
* The size of a block.
*/
public static final int BLOCK_SIZE = HEADER_SIZE + CHUNK_SIZE;
/**
* Default private constructor to prevent instantiation.
*/
private FileSystemConstants() {
}
}
@@ -0,0 +1,62 @@
package org.apollo.jagcached.fs;
/**
* An {@link Index} points to a file in the {@code main_file_cache.dat} file.
* @author Graham
*/
public final class Index {
/**
* Decodes a buffer into an index.
* @param buffer The buffer.
* @return The decoded {@link Index}.
* @throws IllegalArgumentException if the buffer length is invalid.
*/
public static Index decode(byte[] buffer) {
if (buffer.length != FileSystemConstants.INDEX_SIZE) {
throw new IllegalArgumentException("Incorrect buffer length.");
}
int size = ((buffer[0] & 0xFF) << 16) | ((buffer[1] & 0xFF) << 8) | (buffer[2] & 0xFF);
int block = ((buffer[3] & 0xFF) << 16) | ((buffer[4] & 0xFF) << 8) | (buffer[5] & 0xFF);
return new Index(size, block);
}
/**
* The size of the file.
*/
private final int size;
/**
* The first block of the file.
*/
private final int block;
/**
* Creates the index.
* @param size The size of the file.
* @param block The first block of the file.
*/
public Index(int size, int block) {
this.size = size;
this.block = block;
}
/**
* Gets the size of the file.
* @return The size of the file.
*/
public int getSize() {
return size;
}
/**
* Gets the first block of the file.
* @return The first block of the file.
*/
public int getBlock() {
return block;
}
}
@@ -0,0 +1,289 @@
package org.apollo.jagcached.fs;
import java.io.Closeable;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.util.zip.CRC32;
/**
* A file system based on top of the operating system's file system. It
* consists of a data file and index files. Index files point to blocks in the
* data file, which contains the actual data.
* @author Graham
* @author Ryley Kimmel <ryley.kimmel@live.com>
*/
public final class IndexedFileSystem implements Closeable {
/**
* Read only flag.
*/
private final boolean readOnly;
/**
* The index files.
*/
private RandomAccessFile[] indices = new RandomAccessFile[256];
/**
* The data file.
*/
private RandomAccessFile data;
/**
* The cached CRC table.
*/
private ByteBuffer crcTable;
/**
* Creates the file system with the specified base directory.
* @param base The base directory.
* @param readOnly A flag indicating if the file system will be read only.
* @throws Exception if the file system is invalid.
*/
public IndexedFileSystem(File base, boolean readOnly) throws Exception {
this.readOnly = readOnly;
detectLayout(base);
}
/**
* Checks if this {@link IndexedFileSystem} is read only.
* @return {@code true} if so, {@code false} if not.
*/
public boolean isReadOnly() {
return readOnly;
}
/**
* Automatically detect the layout of the specified directory.
* @param base The base directory.
* @throws Exception if the file system is invalid.
*/
private void detectLayout(File base) throws Exception {
int indexCount = 0;
for (int index = 0; index < indices.length; index++) {
File f = new File(base.getAbsolutePath() + "/main_file_cache.idx" + index);
if (f.exists() && !f.isDirectory()) {
indexCount++;
indices[index] = new RandomAccessFile(f, readOnly ? "r" : "rw");
}
}
if (indexCount <= 0) {
throw new Exception("No index file(s) present");
}
File dataFile = new File(base.getAbsolutePath() + "/main_file_cache.dat");
if (dataFile.exists() && !dataFile.isDirectory()) {
data = new RandomAccessFile(dataFile, readOnly ? "r" : "rw");
} else {
throw new Exception("No data file present");
}
}
/**
* Gets the index of a file.
* @param fd The {@link FileDescriptor} which points to the file.
* @return The {@link Index}.
* @throws IOException if an I/O error occurs.
*/
private Index getIndex(FileDescriptor fd) throws IOException {
int index = fd.getType();
if (index < 0 || index >= indices.length) {
throw new IndexOutOfBoundsException();
}
byte[] buffer = new byte[FileSystemConstants.INDEX_SIZE];
RandomAccessFile indexFile = indices[index];
synchronized (indexFile) {
long ptr = (long) fd.getFile() * (long) FileSystemConstants.INDEX_SIZE;
if (ptr >= 0 && indexFile.length() >= (ptr + FileSystemConstants.INDEX_SIZE)) {
indexFile.seek(ptr);
indexFile.readFully(buffer);
} else {
throw new FileNotFoundException();
}
}
return Index.decode(buffer);
}
/**
* Gets the number of files with the specified type.
* @param type The type.
* @return The number of files.
* @throws IOException if an I/O error occurs.
*/
private int getFileCount(int type) throws IOException {
if (type < 0 || type >= indices.length) {
throw new IndexOutOfBoundsException();
}
RandomAccessFile indexFile = indices[type];
synchronized (indexFile) {
return (int) (indexFile.length() / FileSystemConstants.INDEX_SIZE);
}
}
/**
* Gets the CRC table.
* @return The CRC table.
* @throws IOException if an I/O erorr occurs.
*/
public ByteBuffer getCrcTable() throws IOException {
if (readOnly) {
synchronized (this) {
if (crcTable != null) {
return crcTable.slice();
}
}
// the number of archives
int archives = getFileCount(0);
// the hash
int hash = 1234;
// the CRCs
int[] crcs = new int[archives];
// calculate the CRCs
CRC32 crc32 = new CRC32();
for (int i = 1; i < crcs.length; i++) {
crc32.reset();
ByteBuffer bb = getFile(0, i);
byte[] bytes = new byte[bb.remaining()];
bb.get(bytes, 0, bytes.length);
crc32.update(bytes, 0, bytes.length);
crcs[i] = (int) crc32.getValue();
}
// hash the CRCs and place them in the buffer
ByteBuffer buf = ByteBuffer.allocate(crcs.length * 4 + 4);
for (int i = 0; i < crcs.length; i++) {
hash = (hash << 1) + crcs[i];
buf.putInt(crcs[i]);
}
// place the hash into the buffer
buf.putInt(hash);
buf.flip();
synchronized (this) {
crcTable = buf;
return crcTable.slice();
}
} else {
throw new IOException("cannot get CRC table from a writable file system");
}
}
/**
* Gets a file.
* @param type The file type.
* @param file The file id.
* @return A {@link ByteBuffer} which contains the contents of the file.
* @throws IOException if an I/O error occurs.
*/
public ByteBuffer getFile(int type, int file) throws IOException {
return getFile(new FileDescriptor(type, file));
}
/**
* Gets a file.
* @param fd The {@link FileDescriptor} which points to the file.
* @return A {@link ByteBuffer} which contains the contents of the file.
* @throws IOException if an I/O error occurs.
*/
public ByteBuffer getFile(FileDescriptor fd) throws IOException {
Index index = getIndex(fd);
ByteBuffer buffer = ByteBuffer.allocate(index.getSize());
// calculate some initial values
long ptr = (long) index.getBlock() * (long) FileSystemConstants.BLOCK_SIZE;
int read = 0;
int size = index.getSize();
int blocks = size / FileSystemConstants.CHUNK_SIZE;
if (size % FileSystemConstants.CHUNK_SIZE != 0) {
blocks++;
}
for (int i = 0; i < blocks; i++) {
// read header
byte[] header = new byte[FileSystemConstants.HEADER_SIZE];
synchronized (data) {
data.seek(ptr);
data.readFully(header);
}
// increment pointers
ptr += FileSystemConstants.HEADER_SIZE;
// parse header
int nextFile = ((header[0] & 0xFF) << 8) | (header[1] & 0xFF);
int curChunk = ((header[2] & 0xFF) << 8) | (header[3] & 0xFF);
int nextBlock = ((header[4] & 0xFF) << 16) | ((header[5] & 0xFF) << 8) | (header[6] & 0xFF);
int nextType = header[7] & 0xFF;
// check expected chunk id is correct
if (i != curChunk) {
throw new IOException("Chunk id mismatch.");
}
// calculate how much we can read
int chunkSize = size - read;
if (chunkSize > FileSystemConstants.CHUNK_SIZE) {
chunkSize = FileSystemConstants.CHUNK_SIZE;
}
// read the next chunk and put it in the buffer
byte[] chunk = new byte[chunkSize];
synchronized (data) {
data.seek(ptr);
data.readFully(chunk);
}
buffer.put(chunk);
// increment pointers
read += chunkSize;
ptr = (long) nextBlock * (long) FileSystemConstants.BLOCK_SIZE;
// if we still have more data to read, check the validity of the
// header
if (size > read) {
if (nextType != (fd.getType() + 1)) {
throw new IOException("File type mismatch.");
}
if (nextFile != fd.getFile()) {
throw new IOException("File id mismatch.");
}
}
}
buffer.flip();
return buffer;
}
@Override
public void close() throws IOException {
if (data != null) {
synchronized (data) {
data.close();
}
}
for (RandomAccessFile index : indices) {
if (index != null) {
synchronized (index) {
index.close();
}
}
}
}
}
@@ -0,0 +1,63 @@
package org.apollo.jagcached.net;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apollo.jagcached.FileServer;
import org.apollo.jagcached.dispatch.RequestDispatcher;
import org.apollo.jagcached.net.jaggrab.JagGrabRequest;
import org.apollo.jagcached.net.ondemand.OnDemandRequest;
import org.apollo.jagcached.net.service.ServiceRequest;
import org.apollo.jagcached.net.service.ServiceResponse;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ExceptionEvent;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.handler.codec.http.HttpRequest;
import org.jboss.netty.handler.timeout.IdleStateAwareChannelUpstreamHandler;
import org.jboss.netty.handler.timeout.IdleStateEvent;
/**
* An {@link IdleStateAwareChannelUpstreamHandler} for the {@link FileServer}.
* @author Graham
*/
public final class FileServerHandler extends IdleStateAwareChannelUpstreamHandler {
/**
* The logger for this class.
*/
private static final Logger logger = Logger.getLogger(FileServerHandler.class.getName());
@Override
public void channelIdle(ChannelHandlerContext ctx, IdleStateEvent e) throws Exception {
e.getChannel().close();
}
@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
Object msg = e.getMessage();
if (msg instanceof ServiceRequest) {
ServiceRequest request = (ServiceRequest) msg;
if (request.getId() != ServiceRequest.SERVICE_ONDEMAND) {
e.getChannel().close();
} else {
e.getChannel().write(new ServiceResponse());
}
} else if (msg instanceof OnDemandRequest) {
RequestDispatcher.dispatch(e.getChannel(), (OnDemandRequest) msg);
} else if (msg instanceof JagGrabRequest) {
RequestDispatcher.dispatch(e.getChannel(), (JagGrabRequest) msg);
} else if (msg instanceof HttpRequest) {
RequestDispatcher.dispatch(e.getChannel(), (HttpRequest) msg);
} else {
throw new Exception("unknown message type");
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception {
logger.log(Level.SEVERE, "Exception occured, closing channel...", e.getCause());
e.getChannel().close();
}
}
@@ -0,0 +1,61 @@
package org.apollo.jagcached.net;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.ChannelPipelineFactory;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.handler.codec.http.HttpChunkAggregator;
import org.jboss.netty.handler.codec.http.HttpRequestDecoder;
import org.jboss.netty.handler.codec.http.HttpResponseEncoder;
import org.jboss.netty.handler.timeout.IdleStateHandler;
import org.jboss.netty.util.Timer;
/**
* A {@link ChannelPipelineFactory} for the HTTP protocol.
* @author Graham
*/
public final class HttpPipelineFactory implements ChannelPipelineFactory {
/**
* The maximum length of a request, in bytes.
*/
private static final int MAX_REQUEST_LENGTH = 8192;
/**
* The file server event handler.
*/
private final FileServerHandler handler;
/**
* The timer used for idle checking.
*/
private final Timer timer;
/**
* Creates the HTTP pipeline factory.
* @param handler The file server event handler.
* @param timer The timer used for idle checking.
*/
public HttpPipelineFactory(FileServerHandler handler, Timer timer) {
this.handler = handler;
this.timer = timer;
}
@Override
public ChannelPipeline getPipeline() throws Exception {
ChannelPipeline pipeline = Channels.pipeline();
// decoders
pipeline.addLast("decoder", new HttpRequestDecoder());
pipeline.addLast("chunker", new HttpChunkAggregator(MAX_REQUEST_LENGTH));
// encoders
pipeline.addLast("encoder", new HttpResponseEncoder());
// handler
pipeline.addLast("timeout", new IdleStateHandler(timer, NetworkConstants.IDLE_TIME, 0, 0));
pipeline.addLast("handler", handler);
return pipeline;
}
}
@@ -0,0 +1,86 @@
package org.apollo.jagcached.net;
import java.nio.charset.Charset;
import org.apollo.jagcached.net.jaggrab.JagGrabRequestDecoder;
import org.apollo.jagcached.net.jaggrab.JagGrabResponseEncoder;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.buffer.ChannelBuffers;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.ChannelPipelineFactory;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.handler.codec.frame.DelimiterBasedFrameDecoder;
import org.jboss.netty.handler.codec.string.StringDecoder;
import org.jboss.netty.handler.timeout.IdleStateHandler;
import org.jboss.netty.util.Timer;
/**
* A {@link ChannelPipelineFactory} for the JAGGRAB protocol.
* @author Graham
*/
public final class JagGrabPipelineFactory implements ChannelPipelineFactory {
/**
* The maximum length of a request, in bytes.
*/
private static final int MAX_REQUEST_LENGTH = 8192;
/**
* The character set used in the request.
*/
private static final Charset JAGGRAB_CHARSET = Charset.forName("US-ASCII");
/**
* A buffer with two line feed (LF) characters in it.
*/
private static final ChannelBuffer DOUBLE_LINE_FEED_DELIMITER = ChannelBuffers.buffer(2);
/**
* Populates the double line feed buffer.
*/
static {
DOUBLE_LINE_FEED_DELIMITER.writeByte(10);
DOUBLE_LINE_FEED_DELIMITER.writeByte(10);
}
/**
* The file server event handler.
*/
private final FileServerHandler handler;
/**
* The timer used for idle checking.
*/
private final Timer timer;
/**
* Creates a {@code JAGGRAB} pipeline factory.
* @param handler The file server event handler.
* @param timer The timer used for idle checking.
*/
public JagGrabPipelineFactory(FileServerHandler handler, Timer timer) {
this.handler = handler;
this.timer = timer;
}
@Override
public ChannelPipeline getPipeline() throws Exception {
ChannelPipeline pipeline = Channels.pipeline();
// decoders
pipeline.addLast("framer", new DelimiterBasedFrameDecoder(MAX_REQUEST_LENGTH, DOUBLE_LINE_FEED_DELIMITER));
pipeline.addLast("string-decoder", new StringDecoder(JAGGRAB_CHARSET));
pipeline.addLast("jaggrab-decoder", new JagGrabRequestDecoder());
// encoders
pipeline.addLast("jaggrab-encoder", new JagGrabResponseEncoder());
// handler
pipeline.addLast("timeout", new IdleStateHandler(timer, NetworkConstants.IDLE_TIME, 0, 0));
pipeline.addLast("handler", handler);
return pipeline;
}
}
@@ -0,0 +1,37 @@
package org.apollo.jagcached.net;
/**
* A class which holds network-related constants.
* @author Graham
*/
public final class NetworkConstants {
/**
* The HTTP port.
*/
public static final int HTTP_PORT = 8080;
/**
* The JAGGRAB port.
*/
public static final int JAGGRAB_PORT = 43595;
/**
* The service port (which is also used for the 'on-demand' protocol).
*/
public static final int SERVICE_PORT = 43596;
/**
* The number of seconds a channel can be idle before being closed
* automatically.
*/
public static final int IDLE_TIME = 15;
/**
* Default private constructor to prevent instantiaton.
*/
private NetworkConstants() {
}
}
@@ -0,0 +1,59 @@
package org.apollo.jagcached.net;
import org.apollo.jagcached.net.ondemand.OnDemandRequestDecoder;
import org.apollo.jagcached.net.ondemand.OnDemandResponseEncoder;
import org.apollo.jagcached.net.service.ServiceRequestDecoder;
import org.apollo.jagcached.net.service.ServiceResponseEncoder;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.ChannelPipelineFactory;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.handler.timeout.IdleStateHandler;
import org.jboss.netty.util.Timer;
/**
* A {@link ChannelPipelineFactory} for the 'on-demand' protocol.
* @author Graham
*/
public final class OnDemandPipelineFactory implements ChannelPipelineFactory {
/**
* The file server event handler.
*/
private final FileServerHandler handler;
/**
* The timer used for idle checking.
*/
private final Timer timer;
/**
* Creates an 'on-demand' pipeline factory.
* @param handler The file server event handler.
* @param timer The timer used for idle checking.
*/
public OnDemandPipelineFactory(FileServerHandler handler, Timer timer) {
this.handler = handler;
this.timer = timer;
}
@Override
public ChannelPipeline getPipeline() throws Exception {
ChannelPipeline pipeline = Channels.pipeline();
// decoders
pipeline.addLast("serviceDecoder", new ServiceRequestDecoder());
pipeline.addLast("decoder", new OnDemandRequestDecoder());
// encoders
pipeline.addLast("serviceEncoder", new ServiceResponseEncoder());
pipeline.addLast("encoder", new OnDemandResponseEncoder());
// handler
pipeline.addLast("timeout", new IdleStateHandler(timer, NetworkConstants.IDLE_TIME, 0, 0));
pipeline.addLast("handler", handler);
return pipeline;
}
}
@@ -0,0 +1,30 @@
package org.apollo.jagcached.net.jaggrab;
/**
* Represents the request for a single file using the JAGGRAB protocol.
* @author Graham
*/
public final class JagGrabRequest {
/**
* The path to the file.
*/
private final String filePath;
/**
* Creates the request.
* @param filePath The file path.
*/
public JagGrabRequest(String filePath) {
this.filePath = filePath;
}
/**
* Gets the file path.
* @return The file path.
*/
public String getFilePath() {
return filePath;
}
}
@@ -0,0 +1,27 @@
package org.apollo.jagcached.net.jaggrab;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.handler.codec.oneone.OneToOneDecoder;
/**
* A {@link OneToOneDecoder} for the JAGGRAB protocol.
* @author Graham
*/
public final class JagGrabRequestDecoder extends OneToOneDecoder {
@Override
protected Object decode(ChannelHandlerContext ctx, Channel c, Object msg) throws Exception {
if (msg instanceof String) {
String str = ((String) msg);
if (str.startsWith("JAGGRAB /")) {
String filePath = str.substring(8).trim();
return new JagGrabRequest(filePath);
} else {
throw new Exception("corrupted request line");
}
}
return msg;
}
}
@@ -0,0 +1,32 @@
package org.apollo.jagcached.net.jaggrab;
import org.jboss.netty.buffer.ChannelBuffer;
/**
* Represents a single JAGGRAB reponse.
* @author Graham
*/
public final class JagGrabResponse {
/**
* The file data.
*/
private final ChannelBuffer fileData;
/**
* Creates the response.
* @param fileData The file data.
*/
public JagGrabResponse(ChannelBuffer fileData) {
this.fileData = fileData;
}
/**
* Gets the file data.
* @return The file data.
*/
public ChannelBuffer getFileData() {
return fileData;
}
}
@@ -0,0 +1,22 @@
package org.apollo.jagcached.net.jaggrab;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.handler.codec.oneone.OneToOneEncoder;
/**
* A {@link OneToOneEncoder} for the JAGGRAB protocol.
* @author Graham
*/
public final class JagGrabResponseEncoder extends OneToOneEncoder {
@Override
protected Object encode(ChannelHandlerContext ctx, Channel c, Object msg) throws Exception {
if (msg instanceof JagGrabResponse) {
JagGrabResponse resp = (JagGrabResponse) msg;
return resp.getFileData();
}
return msg;
}
}
@@ -0,0 +1,109 @@
package org.apollo.jagcached.net.ondemand;
import org.apollo.jagcached.fs.FileDescriptor;
/**
* Represents a single 'on-demand' request.
* @author Graham
* @author Ryley Kimmel <ryley.kimmel@live.com>
*/
public final class OnDemandRequest implements Comparable<OnDemandRequest> {
/**
* An enumeration containing the different request priorities.
* @author Graham
*/
public enum Priority {
/**
* High priority - used in-game when data is required immediately but
* has not yet been received.
*/
HIGH,
/**
* Medium priority - used while loading the 'bare minimum' required to
* run the game.
*/
MEDIUM,
/**
* Low priority - used when a file is not required urgently. The client
* login screen says "loading extra files.." when low priority loading
* is being performed.
*/
LOW;
/**
* Converts the integer value to a priority.
* @param v The integer value.
* @return The priority.
* @throws IllegalArgumentException if the value is outside of the
* range 1-3 inclusive.
*/
public static Priority valueOf(int v) {
switch (v) {
case 0:
return HIGH;
case 1:
return MEDIUM;
case 2:
return LOW;
default:
throw new IllegalArgumentException("priority out of range");
}
}
}
/**
* The file descriptor.
*/
private final FileDescriptor fileDescriptor;
/**
* The request priority.
*/
private final Priority priority;
/**
* Creates the 'on-demand' request.
* @param fileDescriptor The file descriptor.
* @param priority The priority.
*/
public OnDemandRequest(FileDescriptor fileDescriptor, Priority priority) {
this.fileDescriptor = fileDescriptor;
this.priority = priority;
}
/**
* Gets the file descriptor.
* @return The file descriptor.
*/
public FileDescriptor getFileDescriptor() {
return fileDescriptor;
}
/**
* Gets the priority.
* @return The priority.
*/
public Priority getPriority() {
return priority;
}
@Override
public int compareTo(OnDemandRequest o) {
int thisPriority = priority.ordinal();
int otherPriority = o.priority.ordinal();
if (thisPriority < otherPriority) {
return 1;
} else if (thisPriority == otherPriority) {
return 0;
} else {
return -1;
}
}
}
@@ -0,0 +1,32 @@
package org.apollo.jagcached.net.ondemand;
import org.apollo.jagcached.fs.FileDescriptor;
import org.apollo.jagcached.net.ondemand.OnDemandRequest.Priority;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.handler.codec.frame.FrameDecoder;
/**
* A {@link FrameDecoder} for the 'on-demand' protocol.
* @author Graham
*/
public final class OnDemandRequestDecoder extends FrameDecoder {
@Override
protected Object decode(ChannelHandlerContext ctx, Channel c, ChannelBuffer buf) throws Exception {
if (buf.readableBytes() >= 4) {
int type = buf.readUnsignedByte() + 1;
int file = buf.readUnsignedShort();
int priority = buf.readUnsignedByte();
FileDescriptor desc = new FileDescriptor(type, file);
Priority p = Priority.valueOf(priority);
return new OnDemandRequest(desc, p);
}
return null;
}
}
@@ -0,0 +1,79 @@
package org.apollo.jagcached.net.ondemand;
import org.apollo.jagcached.fs.FileDescriptor;
import org.jboss.netty.buffer.ChannelBuffer;
/**
* Represents a single 'on-demand' response.
* @author Graham
*/
public final class OnDemandResponse {
/**
* The file descriptor.
*/
private final FileDescriptor fileDescriptor;
/**
* The file size.
*/
private final int fileSize;
/**
* The chunk id.
*/
private final int chunkId;
/**
* The chunk data.
*/
private final ChannelBuffer chunkData;
/**
* Creates the 'on-demand' response.
* @param fileDescriptor The file descriptor.
* @param fileSize The file size.
* @param chunkId The chunk id.
* @param chunkData The chunk data.
*/
public OnDemandResponse(FileDescriptor fileDescriptor, int fileSize, int chunkId, ChannelBuffer chunkData) {
this.fileDescriptor = fileDescriptor;
this.fileSize = fileSize;
this.chunkId = chunkId;
this.chunkData = chunkData;
}
/**
* Gets the file descriptor.
* @return The file descriptor.
*/
public FileDescriptor getFileDescriptor() {
return fileDescriptor;
}
/**
* Gets the file size.
* @return The file size.
*/
public int getFileSize() {
return fileSize;
}
/**
* Gets the chunk id.
* @return The chunk id.
*/
public int getChunkId() {
return chunkId;
}
/**
* Gets the chunk data.
* @return The chunk data.
*/
public ChannelBuffer getChunkData() {
return chunkData;
}
}
@@ -0,0 +1,39 @@
package org.apollo.jagcached.net.ondemand;
import org.apollo.jagcached.fs.FileDescriptor;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.buffer.ChannelBuffers;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.handler.codec.oneone.OneToOneEncoder;
/**
* A {@link OneToOneEncoder} for the 'on-demand' protocol.
* @author Graham
*/
public final class OnDemandResponseEncoder extends OneToOneEncoder {
@Override
protected Object encode(ChannelHandlerContext ctx, Channel c, Object msg) throws Exception {
if (msg instanceof OnDemandResponse) {
OnDemandResponse resp = (OnDemandResponse) msg;
FileDescriptor fileDescriptor = resp.getFileDescriptor();
int fileSize = resp.getFileSize();
int chunkId = resp.getChunkId();
ChannelBuffer chunkData = resp.getChunkData();
ChannelBuffer buf = ChannelBuffers.buffer(6 + chunkData.readableBytes());
buf.writeByte(fileDescriptor.getType() - 1);
buf.writeShort(fileDescriptor.getFile());
buf.writeShort(fileSize);
buf.writeByte(chunkId);
buf.writeBytes(chunkData);
return buf;
}
return msg;
}
}
@@ -0,0 +1,40 @@
package org.apollo.jagcached.net.service;
/**
* Represents a service request message.
* @author Graham
*/
public final class ServiceRequest {
/**
* The game service id.
*/
public static final int SERVICE_GAME = 14;
/**
* The 'on-demand' service id.
*/
public static final int SERVICE_ONDEMAND = 15;
/**
* The service id.
*/
private final int id;
/**
* Creates a service request.
* @param id The service id.
*/
public ServiceRequest(int id) {
this.id = id;
}
/**
* Gets the service id.
* @return The service id.
*/
public int getId() {
return id;
}
}
@@ -0,0 +1,39 @@
package org.apollo.jagcached.net.service;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.handler.codec.frame.FrameDecoder;
/**
* A {@link FrameDecoder} which decodes {@link ServiceRequest} messages.
* @author Graham
*/
public final class ServiceRequestDecoder extends FrameDecoder {
/**
* Creates the decoder, enabling the 'unfold' mechanism.
*/
public ServiceRequestDecoder() {
super(true);
}
@Override
protected Object decode(ChannelHandlerContext ctx, Channel c, ChannelBuffer buf) throws Exception {
if (buf.readable()) {
ServiceRequest request = new ServiceRequest(buf.readUnsignedByte());
ChannelPipeline pipeline = ctx.getPipeline();
pipeline.remove(this);
if (buf.readable()) {
return new Object[] { request, buf.readBytes(buf.readableBytes()) };
} else {
return request;
}
}
return null;
}
}
@@ -0,0 +1,9 @@
package org.apollo.jagcached.net.service;
/**
* Represents a response to a service request.
* @author Graham
*/
public final class ServiceResponse {
}
@@ -0,0 +1,25 @@
package org.apollo.jagcached.net.service;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.buffer.ChannelBuffers;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.handler.codec.oneone.OneToOneEncoder;
/**
* A {@link OneToOneEncoder} which encodes {@link ServiceResponse} messages.
* @author Graham
*/
public final class ServiceResponseEncoder extends OneToOneEncoder {
@Override
protected Object encode(ChannelHandlerContext ctx, Channel c, Object msg) throws Exception {
if (msg instanceof ServiceResponse) {
ChannelBuffer buf = ChannelBuffers.buffer(8);
buf.writeLong(0);
return buf;
}
return msg;
}
}
@@ -0,0 +1,40 @@
package org.apollo.jagcached.resource;
import java.io.IOException;
import java.nio.ByteBuffer;
/**
* A resource provider composed of multiple resource providers.
* @author Graham Edgecombe
*/
public final class CombinedResourceProvider extends ResourceProvider {
/**
* An array of resource providers.
*/
private final ResourceProvider[] providers;
/**
* Creates the combined resource providers.
* @param providers The providers this provider delegates to.
*/
public CombinedResourceProvider(ResourceProvider... providers) {
this.providers = providers;
}
@Override
public boolean accept(String path) throws IOException {
return true;
}
@Override
public ByteBuffer get(String path) throws IOException {
for (ResourceProvider provider : providers) {
if (provider.accept(path)) {
return provider.get(path);
}
}
return null;
}
}
@@ -0,0 +1,64 @@
package org.apollo.jagcached.resource;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel.MapMode;
/**
* A {@link ResourceProvider} which provides additional hypertext resources.
* @author Graham Edgecombe
*/
public final class HypertextResourceProvider extends ResourceProvider {
/**
* The base directory from which documents are served.
*/
private final File base;
/**
* Creates a new hypertext resource provider with the specified base
* directory.
* @param base The base directory.
*/
public HypertextResourceProvider(File base) {
this.base = base;
}
@Override
public boolean accept(String path) throws IOException {
File f = new File(base, path);
URI target = f.toURI().normalize();
if (target.toASCIIString().startsWith(base.toURI().normalize().toASCIIString())) {
if (f.isDirectory()) {
f = new File(f, "index.html");
}
return f.exists();
}
return false;
}
@Override
public ByteBuffer get(String path) throws IOException {
File f = new File(base, path);
if (f.isDirectory()) {
f = new File(f, "index.html");
}
if (!f.exists()) {
return null;
}
RandomAccessFile raf = new RandomAccessFile(f, "r");
ByteBuffer buf;
try {
buf = raf.getChannel().map(MapMode.READ_ONLY, 0, raf.length());
} finally {
raf.close();
}
return buf;
}
}
@@ -0,0 +1,30 @@
package org.apollo.jagcached.resource;
import java.io.IOException;
import java.nio.ByteBuffer;
/**
* A class which provides resources.
* @author Graham Edgecombe
*/
public abstract class ResourceProvider {
/**
* Checks that this provider can fulfil a request to the specified
* resource.
* @param path The path to the resource, e.g. {@code /crc}.
* @return {@code true} if the provider can fulfil a request to the
* resource, {@code false} otherwise.
* @throws IOException if an I/O error occurs.
*/
public abstract boolean accept(String path) throws IOException;
/**
* Gets a resource by its path.
* @param path The path.
* @return The resource, or {@code null} if it doesn't exist.
* @throws IOException if an I/O error occurs.
*/
public abstract ByteBuffer get(String path) throws IOException;
}
@@ -0,0 +1,70 @@
package org.apollo.jagcached.resource;
import java.io.IOException;
import java.nio.ByteBuffer;
import org.apollo.jagcached.fs.IndexedFileSystem;
/**
* A {@link ResourceProvider} which maps virtual resources (such as
* {@code /media}) to files in an {@link IndexedFileSystem}.
* @author Graham Edgecombe
*/
public final class VirtualResourceProvider extends ResourceProvider {
/**
* An array of valid prefixes.
*/
private static final String[] VALID_PREFIXES = {
"crc", "title", "config", "interface", "media", "versionlist",
"textures", "wordenc", "sounds"
};
/**
* The file system.
*/
private final IndexedFileSystem fs;
/**
* Creates a new virtual resource provider with the specified file system.
* @param fs The file system.
*/
public VirtualResourceProvider(IndexedFileSystem fs) {
this.fs = fs;
}
@Override
public boolean accept(String path) throws IOException {
for (String prefix : VALID_PREFIXES) {
if (path.startsWith("/" + prefix)) {
return true;
}
}
return false;
}
@Override
public ByteBuffer get(String path) throws IOException {
if (path.startsWith("/crc")) {
return fs.getCrcTable();
} else if (path.startsWith("/title")) {
return fs.getFile(0, 1);
} else if (path.startsWith("/config")) {
return fs.getFile(0, 2);
} else if (path.startsWith("/interface")) {
return fs.getFile(0, 3);
} else if (path.startsWith("/media")) {
return fs.getFile(0, 4);
} else if (path.startsWith("/versionlist")) {
return fs.getFile(0, 5);
} else if (path.startsWith("/textures")) {
return fs.getFile(0, 6);
} else if (path.startsWith("/wordenc")) {
return fs.getFile(0, 7);
} else if (path.startsWith("/sounds")) {
return fs.getFile(0, 8);
}
return null;
}
}