Skip to content

Commit 1fa3a84

Browse files
SamCarlbergJLLeitschuh
authored andcommitted
Add HTTP publishing and image source operations (#551)
* HTTP publishing now works * Implemented an image source for images POSTed to the HTTP server. Images must be posted to /GRIP/upload/image -- there could be a need to have the path be user-defined Images are treated as 8-bit 3-channel color images. * Adds support for setting pipelines via HTTP POST operations * Changed pipeline upload to upload the contents of a GRIP project rather than the path to a save file * Changed GripServer to return a 500 error on a POST only if an exception was thrown while handling the data. * Removes need to specify project file at startup * 500 error if no data is received from a POST * Pipeline no longer runs during deserialization GripServer returns a 423 error if a data request is sent while the pipeline is running * Return 503 Service Unavailable when trying to get data when pipeline is running Use library-defined HTTP codes instead of redefining them 404 error when going to a URI such as /GRIP/data/foo or /GRIP/data* (now it has to be on a path that has a handler specifically assigned to it) Update tests to stop codacy bot from complaining so much HTTP sources can only supply images POSTed to URLs under '/GRIP/upload/image', e.g. '/GRIP/upload/image/foo'
1 parent 8f4709d commit 1fa3a84

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+2322
-28
lines changed

build.gradle

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,11 @@ project(":core") {
202202
compile group: 'org.apache.commons', name: 'commons-lang3', version: '3.4'
203203
compile group: 'com.google.guava', name: 'guava', version: '19.0'
204204
compile group: 'com.google.auto.value', name: 'auto-value', version: '1.1'
205+
compile group: 'com.google.code.gson', name: 'gson', version: '2.6.2'
206+
compile group: 'org.eclipse.jetty', name:'jetty-server', version:'9.3.8.v20160314'
207+
testCompile group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5.2'
208+
testCompile group: 'org.apache.httpcomponents', name: 'httpcore', version: '4.2.2'
209+
testCompile group: 'org.apache.httpcomponents', name: 'httpmime', version: '4.5.2'
205210
// We use the no_aop version of Guice because the aop isn't avaiable in arm java
206211
// http://stackoverflow.com/a/15235190/3708426
207212
// https://github.com/google/guice/wiki/OptionalAOP

core/src/main/java/edu/wpi/grip/core/GripCoreModule.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import edu.wpi.grip.core.sockets.OutputSocket;
99
import edu.wpi.grip.core.sockets.OutputSocketImpl;
1010
import edu.wpi.grip.core.sources.CameraSource;
11+
import edu.wpi.grip.core.sources.HttpSource;
1112
import edu.wpi.grip.core.sources.ImageFileSource;
1213
import edu.wpi.grip.core.sources.MultiImageFileSource;
1314
import edu.wpi.grip.core.util.ExceptionWitness;
@@ -137,6 +138,9 @@ public <I> void hear(TypeLiteral<I> type, TypeEncounter<I> encounter) {
137138
install(new FactoryModuleBuilder()
138139
.implement(MultiImageFileSource.class, MultiImageFileSource.class)
139140
.build(MultiImageFileSource.Factory.class));
141+
install(new FactoryModuleBuilder()
142+
.implement(HttpSource.class, HttpSource.class)
143+
.build(HttpSource.Factory.class));
140144

141145
install(new FactoryModuleBuilder().build(ExceptionWitness.Factory.class));
142146
}

core/src/main/java/edu/wpi/grip/core/Main.java

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import edu.wpi.grip.core.events.ExceptionClearedEvent;
44
import edu.wpi.grip.core.events.ExceptionEvent;
5+
import edu.wpi.grip.core.http.GripServer;
6+
import edu.wpi.grip.core.http.HttpPipelineSwitcher;
57
import edu.wpi.grip.core.operations.CVOperations;
68
import edu.wpi.grip.core.operations.Operations;
79
import edu.wpi.grip.core.operations.network.GripNetworkModule;
@@ -12,6 +14,7 @@
1214
import com.google.common.eventbus.Subscribe;
1315
import com.google.inject.Guice;
1416
import com.google.inject.Injector;
17+
import com.google.inject.util.Modules;
1518

1619
import java.io.File;
1720
import java.io.IOException;
@@ -37,30 +40,35 @@ public class Main {
3740
private CVOperations cvOperations;
3841
@Inject
3942
private Logger logger;
43+
@Inject
44+
private GripServer gripServer;
45+
@Inject
46+
private HttpPipelineSwitcher pipelineSwitcher;
4047

4148
@SuppressWarnings({"PMD.SystemPrintln", "JavadocMethod"})
4249
public static void main(String[] args) throws IOException, InterruptedException {
43-
final Injector injector = Guice.createInjector(new GripCoreModule(), new GripNetworkModule(),
44-
new GripSourcesHardwareModule());
50+
final Injector injector = Guice.createInjector(Modules.override(
51+
new GripCoreModule(), new GripSourcesHardwareModule()).with(new GripNetworkModule()));
4552
injector.getInstance(Main.class).start(args);
4653
}
4754

4855
@SuppressWarnings({"PMD.SystemPrintln", "JavadocMethod"})
4956
public void start(String[] args) throws IOException, InterruptedException {
50-
if (args.length != 1) {
51-
System.err.println("Usage: GRIP.jar project.grip");
52-
return;
53-
} else {
57+
String projectPath = null;
58+
if (args.length == 1) {
5459
logger.log(Level.INFO, "Loading file " + args[0]);
60+
projectPath = args[0];
5561
}
5662

5763
operations.addOperations();
5864
cvOperations.addOperations();
59-
60-
final String projectPath = args[0];
65+
gripServer.addHandler(pipelineSwitcher);
66+
gripServer.start();
6167

6268
// Open a project from a .grip file specified on the command line
63-
project.open(new File(projectPath));
69+
if (projectPath != null) {
70+
project.open(new File(projectPath));
71+
}
6472

6573
pipelineRunner.startAsync();
6674

@@ -88,4 +96,4 @@ public final void onExceptionClearedEvent(ExceptionClearedEvent event) {
8896
+ "Event");
8997
}
9098

91-
}
99+
}

core/src/main/java/edu/wpi/grip/core/PipelineRunner.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
import edu.wpi.grip.core.events.RenderEvent;
55
import edu.wpi.grip.core.events.RunPipelineEvent;
6+
import edu.wpi.grip.core.events.RunStartedEvent;
7+
import edu.wpi.grip.core.events.RunStoppedEvent;
68
import edu.wpi.grip.core.events.StopPipelineEvent;
79
import edu.wpi.grip.core.util.SinglePermitSemaphore;
810
import edu.wpi.grip.core.util.service.AutoRestartingService;
@@ -28,6 +30,8 @@
2830
import javax.annotation.Nullable;
2931
import javax.inject.Inject;
3032

33+
import static com.google.common.base.Preconditions.checkNotNull;
34+
3135
/**
3236
* Runs the pipeline in a separate thread. The runner listens for {@link RunPipelineEvent
3337
* RunPipelineEvents} and releases the pipeline thread to update the sources and run the steps.
@@ -68,6 +72,7 @@ protected void runOneIteration() throws InterruptedException {
6872
}
6973

7074
pipelineFlag.acquire();
75+
eventBus.post(new RunStartedEvent());
7176

7277
if (!super.isRunning()) {
7378
return;
@@ -77,6 +82,7 @@ protected void runOneIteration() throws InterruptedException {
7782
if (super.isRunning()) {
7883
eventBus.post(new RenderEvent());
7984
}
85+
eventBus.post(new RunStoppedEvent());
8086
}
8187

8288
@Override
@@ -196,6 +202,7 @@ private void runPipeline(Supplier<Boolean> isRunning) {
196202
@Subscribe
197203
@AllowConcurrentEvents
198204
public void onRunPipeline(RunPipelineEvent event) {
205+
checkNotNull(event);
199206
if (event.pipelineShouldRun()) {
200207
pipelineFlag.release();
201208
}

core/src/main/java/edu/wpi/grip/core/Source.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import edu.wpi.grip.core.sockets.OutputSocket;
44
import edu.wpi.grip.core.sources.CameraSource;
5+
import edu.wpi.grip.core.sources.HttpSource;
56
import edu.wpi.grip.core.sources.ImageFileSource;
67
import edu.wpi.grip.core.sources.MultiImageFileSource;
78
import edu.wpi.grip.core.util.ExceptionWitness;
@@ -108,6 +109,8 @@ public static class SourceFactoryImpl implements SourceFactory {
108109
ImageFileSource.Factory imageFactory;
109110
@Inject
110111
MultiImageFileSource.Factory multiImageFactory;
112+
@Inject
113+
HttpSource.Factory httpFactory;
111114

112115
@Override
113116
public Source create(Class<?> type, Properties properties) throws IOException {
@@ -117,6 +120,8 @@ public Source create(Class<?> type, Properties properties) throws IOException {
117120
return imageFactory.create(properties);
118121
} else if (type.isAssignableFrom(MultiImageFileSource.class)) {
119122
return multiImageFactory.create(properties);
123+
} else if (type.isAssignableFrom(HttpSource.class)) {
124+
return httpFactory.create(properties);
120125
} else {
121126
throw new IllegalArgumentException(type + " was not a valid type");
122127
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package edu.wpi.grip.core.events;
2+
3+
/**
4+
* An event fired when the pipeline starts running. This is guaranteed to be followed by a
5+
* corresponding {@link RunStoppedEvent}.
6+
*/
7+
public class RunStartedEvent {
8+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package edu.wpi.grip.core.events;
2+
3+
/**
4+
* An event fired when the pipeline stops running. This is guaranteed to follow a corresponding
5+
* {@link RunStartedEvent}.
6+
*
7+
* <p>This is different from {@link RenderEvent} in that it will <i>always</i> be fired when the
8+
* pipeline runs.
9+
*/
10+
public class RunStoppedEvent {
11+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
2+
package edu.wpi.grip.core.exception;
3+
4+
/**
5+
* An exception thrown when something goes wrong with an internal GRIP
6+
* operation. This class is {@code abstract} to encourage making subclasses
7+
* for specific cases.
8+
*/
9+
public abstract class GripException extends RuntimeException {
10+
11+
public GripException(String message) {
12+
super(message);
13+
}
14+
15+
public GripException(String message, Throwable cause) {
16+
super(message, cause);
17+
}
18+
19+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package edu.wpi.grip.core.exception;
2+
3+
/**
4+
* An exception thrown when something goes wrong in the
5+
* {@link edu.wpi.grip.core.http.GripServer GripServer}.
6+
*/
7+
public class GripServerException extends GripException {
8+
9+
public GripServerException(String message) {
10+
super(message);
11+
}
12+
13+
public GripServerException(String message, Throwable cause) {
14+
super(message, cause);
15+
}
16+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package edu.wpi.grip.core.http;
2+
3+
import com.google.inject.Singleton;
4+
5+
import java.util.HashSet;
6+
import java.util.Set;
7+
8+
import javax.annotation.Nonnull;
9+
import javax.annotation.Nullable;
10+
11+
import static com.google.common.base.Preconditions.checkNotNull;
12+
13+
/**
14+
* Keeps a record of contexts claimed by HTTP request handlers.
15+
*/
16+
@Singleton
17+
public class ContextStore {
18+
19+
private final Set<String> store = new HashSet<>();
20+
21+
/**
22+
* Records the given context.
23+
*
24+
* @param context the context to record. This cannot be null.
25+
* @throws IllegalArgumentException if the given context has already been claimed
26+
*/
27+
public void record(@Nonnull String context) throws IllegalArgumentException {
28+
checkNotNull(context);
29+
if (!store.add(context)) {
30+
throw new IllegalArgumentException("Context is already claimed: " + context);
31+
}
32+
}
33+
34+
/**
35+
* Erases the given context from this store, if it's present. If {@code context} is {@code null},
36+
* this will do nothing and return {@code false}.
37+
*
38+
* @param context the context to erase
39+
* @return true if the context was erased, false if it wasn't erased
40+
* or if it wasn't present to begin with.
41+
*/
42+
public boolean erase(@Nullable String context) {
43+
return store.remove(context);
44+
}
45+
46+
/**
47+
* Checks if the given context has been recorded in this store.
48+
* If {@code context} is {@code null}, this will return {@code false}.
49+
*
50+
* @param context the context to check
51+
* @return true if the given context has been recorded in this store
52+
*/
53+
public boolean contains(@Nullable String context) {
54+
return store.contains(context);
55+
}
56+
57+
58+
}

0 commit comments

Comments
 (0)