From f3a0f53995001f5455b7d9589d6c849b42d996f1 Mon Sep 17 00:00:00 2001 From: Sam Carlberg Date: Tue, 25 Oct 2016 23:00:20 -0400 Subject: [PATCH 1/9] Add versioning If a save file cannot be loaded, no changes happen to the pipeline and the user is notified. This will happen for any save file, pre-versioning or otherwise. When saving a loaded project, the original version string will be changed to the version of GRIP that saved it. Pre-versioning save files will also be upgraded to the versioned format and have the current version of GRIP --- build.gradle | 1 + .../edu/wpi/grip/core/GripCoreModule.java | 4 ++ .../main/java/edu/wpi/grip/core/Palette.java | 58 +++++++++++++++++-- .../edu/wpi/grip/core/VersionManager.java | 38 ++++++++++++ .../IncompatibleVersionException.java | 17 ++++++ .../core/exception/InvalidSaveException.java | 16 +++++ .../grip/core/http/HttpPipelineSwitcher.java | 13 ++++- .../operations/composite/BlurOperation.java | 8 +-- .../composite/ConvexHullsOperation.java | 12 ++-- .../composite/DesaturateOperation.java | 4 +- .../composite/DistanceTransformOperation.java | 8 +-- .../composite/FilterContoursOperation.java | 28 ++++----- .../composite/FilterLinesOperation.java | 8 +-- .../composite/FindBlobsOperation.java | 10 ++-- .../composite/FindContoursOperation.java | 6 +- .../composite/FindLinesOperation.java | 4 +- .../composite/HSLThresholdOperation.java | 10 ++-- .../composite/HSVThresholdOperation.java | 10 ++-- .../operations/composite/MaskOperation.java | 6 +- .../composite/NormalizeOperation.java | 10 ++-- .../composite/PublishVideoOperation.java | 4 +- .../composite/RGBThresholdOperation.java | 10 ++-- .../operations/composite/ResizeOperation.java | 13 +++-- .../composite/SaveImageOperation.java | 12 ++-- .../operations/composite/SwitchOperation.java | 2 +- .../operations/composite/ThresholdMoving.java | 8 ++- .../operations/composite/ValveOperation.java | 2 +- .../composite/WatershedOperation.java | 6 +- .../network/NetworkPublishOperation.java | 4 +- .../operations/opencv/MatFieldAccessor.java | 16 ++--- .../core/operations/opencv/MinMaxLoc.java | 12 ++-- .../operations/opencv/NewPointOperation.java | 6 +- .../operations/opencv/NewSizeOperation.java | 6 +- .../wpi/grip/core/serialization/Project.java | 38 ++++++++++-- .../grip/core/serialization/ProjectModel.java | 37 ++++++++++++ .../core/serialization/SocketConverter.java | 36 ++++++++---- .../core/serialization/VersionConverter.java | 31 ++++++++++ .../wpi/grip/core/sockets/InputSocket.java | 16 ++++- .../grip/core/sockets/InputSocketImpl.java | 15 +++-- .../wpi/grip/core/sockets/OutputSocket.java | 12 +++- .../grip/core/sockets/OutputSocketImpl.java | 15 +++-- .../edu/wpi/grip/core/sockets/Socket.java | 13 +++++ .../edu/wpi/grip/core/sockets/SocketImpl.java | 10 +++- .../wpi/grip/core/sources/CameraSource.java | 4 +- .../edu/wpi/grip/core/sources/HttpSource.java | 2 +- .../grip/core/sources/ImageFileSource.java | 2 +- .../core/sources/MultiImageFileSource.java | 2 +- .../core/sources/NetworkTableEntrySource.java | 2 +- .../edu/wpi/grip/core/CoreSanityTest.java | 2 + .../grip/core/sockets/MockInputSocket.java | 2 +- .../grip/core/sockets/MockOutputSocket.java | 2 +- .../edu/wpi/grip/ui/MainWindowController.java | 14 ++++- .../InputSocketControllerFactoryTest.java | 1 + 53 files changed, 477 insertions(+), 151 deletions(-) create mode 100644 core/src/main/java/edu/wpi/grip/core/VersionManager.java create mode 100644 core/src/main/java/edu/wpi/grip/core/exception/IncompatibleVersionException.java create mode 100644 core/src/main/java/edu/wpi/grip/core/exception/InvalidSaveException.java create mode 100644 core/src/main/java/edu/wpi/grip/core/serialization/ProjectModel.java create mode 100644 core/src/main/java/edu/wpi/grip/core/serialization/VersionConverter.java diff --git a/build.gradle b/build.gradle index 081223afa5..bef96e15eb 100644 --- a/build.gradle +++ b/build.gradle @@ -252,6 +252,7 @@ project(":core") { compile group: 'org.bytedeco.javacpp-presets', name: 'videoinput', version: '0.200-1.1', classifier: os compile group: 'org.python', name: 'jython', version: '2.7.0' compile group: 'com.thoughtworks.xstream', name: 'xstream', version: '1.4.9' + compile group: 'com.github.zafarkhaja', name: 'java-semver', version: '0.9.0' compile group: 'org.apache.commons', name: 'commons-lang3', version: '3.4' compile group: 'com.google.guava', name: 'guava', version: '19.0' compile group: 'com.google.auto.value', name: 'auto-value', version: '1.2' diff --git a/core/src/main/java/edu/wpi/grip/core/GripCoreModule.java b/core/src/main/java/edu/wpi/grip/core/GripCoreModule.java index 314e77170a..d77b65bb90 100644 --- a/core/src/main/java/edu/wpi/grip/core/GripCoreModule.java +++ b/core/src/main/java/edu/wpi/grip/core/GripCoreModule.java @@ -15,6 +15,7 @@ import edu.wpi.grip.core.util.ExceptionWitness; import edu.wpi.grip.core.util.GripMode; +import com.github.zafarkhaja.semver.Version; import com.google.common.eventbus.EventBus; import com.google.common.eventbus.SubscriberExceptionContext; import com.google.inject.AbstractModule; @@ -125,6 +126,9 @@ public void hear(TypeLiteral type, TypeEncounter encounter) { // Allow for just injecting the settings provider, instead of the whole pipeline bind(SettingsProvider.class).to(Pipeline.class); + // Bind the current GRIP version. + bind(Version.class).toInstance(VersionManager.CURRENT_VERSION); + install(new FactoryModuleBuilder().build(new TypeLiteral>() { })); diff --git a/core/src/main/java/edu/wpi/grip/core/Palette.java b/core/src/main/java/edu/wpi/grip/core/Palette.java index 1768fc5e6a..3288d45a1c 100644 --- a/core/src/main/java/edu/wpi/grip/core/Palette.java +++ b/core/src/main/java/edu/wpi/grip/core/Palette.java @@ -1,13 +1,18 @@ package edu.wpi.grip.core; import edu.wpi.grip.core.events.OperationAddedEvent; +import edu.wpi.grip.core.sockets.Socket; +import com.google.common.collect.ImmutableList; import com.google.common.eventbus.Subscribe; import java.util.Collection; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; import javax.inject.Singleton; @@ -24,10 +29,54 @@ public class Palette { @Subscribe public void onOperationAdded(OperationAddedEvent event) { - final OperationMetaData operation = event.getOperation(); - map(operation.getDescription().name(), operation); - for (String alias : operation.getDescription().aliases()) { - map(alias, operation); + final OperationMetaData operationData = event.getOperation(); + map(operationData.getDescription().name(), operationData); + for (String alias : operationData.getDescription().aliases()) { + map(alias, operationData); + } + // Validate that every input and output socket has a unique name and UID + Operation operation = operationData.getOperationSupplier().get(); + try { + final List sockets = new ImmutableList.Builder() + .addAll(operation.getInputSockets()) + .addAll(operation.getOutputSockets()) + .build(); + checkDuplicates( + operationData, + "input socket names", + operation.getInputSockets(), s -> s.getSocketHint().getIdentifier() + ); + checkDuplicates( + operationData, + "output socket names", + operation.getOutputSockets(), s -> s.getSocketHint().getIdentifier() + ); + checkDuplicates(operationData, "socket IDs", sockets, Socket::getUid); + } finally { + operation.cleanUp(); + } + } + + private static void checkDuplicates(OperationMetaData operationMetaData, + String type, + List list, + Function extractionFunction) { + List duplicates = list.stream() + .map(extractionFunction) + .collect(Collectors.toList()); + list.stream() + .map(extractionFunction) + .distinct() + .forEach(duplicates::remove); + if (!duplicates.isEmpty()) { + throw new IllegalArgumentException( + String.format( + "Duplicate %s found in operation %s: %s", + type, + operationMetaData.getDescription().name(), + duplicates + ) + ); } } @@ -36,6 +85,7 @@ public void onOperationAdded(OperationAddedEvent event) { * * @param key The key the operation should be mapped to * @param operation The operation to map the key to + * * @throws IllegalArgumentException if the key is already in the {@link #operations} map. */ private void map(String key, OperationMetaData operation) { diff --git a/core/src/main/java/edu/wpi/grip/core/VersionManager.java b/core/src/main/java/edu/wpi/grip/core/VersionManager.java new file mode 100644 index 0000000000..a512ec70d9 --- /dev/null +++ b/core/src/main/java/edu/wpi/grip/core/VersionManager.java @@ -0,0 +1,38 @@ +package edu.wpi.grip.core; + +import edu.wpi.grip.core.exception.IncompatibleVersionException; + +import com.github.zafarkhaja.semver.Version; + +/** + * Manager for GRIP versions. + */ +public final class VersionManager { + + /** + * The final release of GRIP without versioned saves. + */ + public static final Version LAST_UNVERSIONED_RELEASE = Version.valueOf("1.5.0"); + + /** + * The current version of GRIP. + */ + public static final Version CURRENT_VERSION = Version.valueOf("2.0.0-beta"); + + /** + * Checks compatibility between two versions of GRIP. + * + * @param current the current version of GRIP + * @param check the version to check + * + * @throws IncompatibleVersionException if the versions are incompatible + */ + public static void checkVersionCompatibility(Version current, Version check) + throws IncompatibleVersionException { + if (check.getMajorVersion() > current.getMajorVersion() + || check.getMinorVersion() > current.getMinorVersion()) { + throw new IncompatibleVersionException("Incompatible future version: " + check); + } + } + +} diff --git a/core/src/main/java/edu/wpi/grip/core/exception/IncompatibleVersionException.java b/core/src/main/java/edu/wpi/grip/core/exception/IncompatibleVersionException.java new file mode 100644 index 0000000000..0a95532cc2 --- /dev/null +++ b/core/src/main/java/edu/wpi/grip/core/exception/IncompatibleVersionException.java @@ -0,0 +1,17 @@ +package edu.wpi.grip.core.exception; + +/** + * An exception thrown when trying to load a saved project created in an incompatible version + * of GRIP. + */ +public class IncompatibleVersionException extends InvalidSaveException { + + public IncompatibleVersionException(String message) { + super(message); + } + + public IncompatibleVersionException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/core/src/main/java/edu/wpi/grip/core/exception/InvalidSaveException.java b/core/src/main/java/edu/wpi/grip/core/exception/InvalidSaveException.java new file mode 100644 index 0000000000..3e33f3abbb --- /dev/null +++ b/core/src/main/java/edu/wpi/grip/core/exception/InvalidSaveException.java @@ -0,0 +1,16 @@ +package edu.wpi.grip.core.exception; + +/** + * An exception thrown when trying to load an invalid saved project. + */ +public class InvalidSaveException extends GripException { + + public InvalidSaveException(String message) { + super(message); + } + + public InvalidSaveException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/core/src/main/java/edu/wpi/grip/core/http/HttpPipelineSwitcher.java b/core/src/main/java/edu/wpi/grip/core/http/HttpPipelineSwitcher.java index d349427193..004c6c39e0 100644 --- a/core/src/main/java/edu/wpi/grip/core/http/HttpPipelineSwitcher.java +++ b/core/src/main/java/edu/wpi/grip/core/http/HttpPipelineSwitcher.java @@ -1,5 +1,6 @@ package edu.wpi.grip.core.http; +import edu.wpi.grip.core.exception.InvalidSaveException; import edu.wpi.grip.core.serialization.Project; import edu.wpi.grip.core.util.GripMode; @@ -43,9 +44,15 @@ protected void handleIfPassed(String target, } switch (mode) { case HEADLESS: - project.open(new String(IOUtils.toByteArray(request.getInputStream()), "UTF-8")); - response.setStatus(HttpServletResponse.SC_CREATED); - baseRequest.setHandled(true); + try { + project.open(new String(IOUtils.toByteArray(request.getInputStream()), "UTF-8")); + response.setStatus(HttpServletResponse.SC_CREATED); + baseRequest.setHandled(true); + } catch (InvalidSaveException e) { + // 403 - Forbidden if given an invalid save + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + baseRequest.setHandled(true); + } break; case GUI: // Don't run in GUI mode, it doesn't make much sense and can easily deadlock if pipelines diff --git a/core/src/main/java/edu/wpi/grip/core/operations/composite/BlurOperation.java b/core/src/main/java/edu/wpi/grip/core/operations/composite/BlurOperation.java index 0baefa312d..da1f49379e 100644 --- a/core/src/main/java/edu/wpi/grip/core/operations/composite/BlurOperation.java +++ b/core/src/main/java/edu/wpi/grip/core/operations/composite/BlurOperation.java @@ -47,11 +47,11 @@ public class BlurOperation implements Operation { @SuppressWarnings("JavadocMethod") public BlurOperation(InputSocket.Factory inputSocketFactory, OutputSocket.Factory outputSocketFactory) { - this.inputSocket = inputSocketFactory.create(inputHint); - this.typeSocket = inputSocketFactory.create(typeHint); - this.radiusSocket = inputSocketFactory.create(radiusHint); + this.inputSocket = inputSocketFactory.create(inputHint, "source-image"); + this.typeSocket = inputSocketFactory.create(typeHint, "blur-type"); + this.radiusSocket = inputSocketFactory.create(radiusHint, "blur-radius"); - this.outputSocket = outputSocketFactory.create(outputHint); + this.outputSocket = outputSocketFactory.create(outputHint, "result"); } @Override diff --git a/core/src/main/java/edu/wpi/grip/core/operations/composite/ConvexHullsOperation.java b/core/src/main/java/edu/wpi/grip/core/operations/composite/ConvexHullsOperation.java index 583430fa50..331facbdf5 100644 --- a/core/src/main/java/edu/wpi/grip/core/operations/composite/ConvexHullsOperation.java +++ b/core/src/main/java/edu/wpi/grip/core/operations/composite/ConvexHullsOperation.java @@ -26,9 +26,11 @@ public class ConvexHullsOperation implements Operation { .category(OperationDescription.Category.FEATURE_DETECTION) .build(); - private final SocketHint contoursHint = new SocketHint.Builder<>(ContoursReport - .class) - .identifier("Contours").initialValueSupplier(ContoursReport::new).build(); + private final SocketHint contoursHint = + new SocketHint.Builder<>(ContoursReport.class) + .identifier("Contours") + .initialValueSupplier(ContoursReport::new) + .build(); private final InputSocket inputSocket; private final OutputSocket outputSocket; @@ -36,9 +38,9 @@ public class ConvexHullsOperation implements Operation { @SuppressWarnings("JavadocMethod") public ConvexHullsOperation(InputSocket.Factory inputSocketFactory, OutputSocket.Factory outputSocketFactory) { - this.inputSocket = inputSocketFactory.create(contoursHint); + this.inputSocket = inputSocketFactory.create(contoursHint, "contours"); - this.outputSocket = outputSocketFactory.create(contoursHint); + this.outputSocket = outputSocketFactory.create(contoursHint, "hulls"); } @Override diff --git a/core/src/main/java/edu/wpi/grip/core/operations/composite/DesaturateOperation.java b/core/src/main/java/edu/wpi/grip/core/operations/composite/DesaturateOperation.java index 49284ed13e..9023de3b17 100644 --- a/core/src/main/java/edu/wpi/grip/core/operations/composite/DesaturateOperation.java +++ b/core/src/main/java/edu/wpi/grip/core/operations/composite/DesaturateOperation.java @@ -39,8 +39,8 @@ public class DesaturateOperation implements Operation { @SuppressWarnings("JavadocMethod") public DesaturateOperation(InputSocket.Factory inputSocketFactory, OutputSocket.Factory outputSocketFactory) { - this.inputSocket = inputSocketFactory.create(inputHint); - this.outputSocket = outputSocketFactory.create(outputHint); + this.inputSocket = inputSocketFactory.create(inputHint, "source-image"); + this.outputSocket = outputSocketFactory.create(outputHint, "result"); } diff --git a/core/src/main/java/edu/wpi/grip/core/operations/composite/DistanceTransformOperation.java b/core/src/main/java/edu/wpi/grip/core/operations/composite/DistanceTransformOperation.java index f720571c9c..f01238cbd9 100644 --- a/core/src/main/java/edu/wpi/grip/core/operations/composite/DistanceTransformOperation.java +++ b/core/src/main/java/edu/wpi/grip/core/operations/composite/DistanceTransformOperation.java @@ -46,11 +46,11 @@ public class DistanceTransformOperation implements Operation { @SuppressWarnings("JavadocMethod") public DistanceTransformOperation(InputSocket.Factory inputSocketFactory, OutputSocket.Factory outputSocketFactory) { - this.srcSocket = inputSocketFactory.create(srcHint); - this.typeSocket = inputSocketFactory.create(typeHint); - this.maskSizeSocket = inputSocketFactory.create(maskSizeHint); + this.srcSocket = inputSocketFactory.create(srcHint, "source-image"); + this.typeSocket = inputSocketFactory.create(typeHint, "transform-type"); + this.maskSizeSocket = inputSocketFactory.create(maskSizeHint, "mask-size"); - this.outputSocket = outputSocketFactory.create(outputHint); + this.outputSocket = outputSocketFactory.create(outputHint, "result"); } @Override diff --git a/core/src/main/java/edu/wpi/grip/core/operations/composite/FilterContoursOperation.java b/core/src/main/java/edu/wpi/grip/core/operations/composite/FilterContoursOperation.java index 433ccd06cb..07a40d777a 100644 --- a/core/src/main/java/edu/wpi/grip/core/operations/composite/FilterContoursOperation.java +++ b/core/src/main/java/edu/wpi/grip/core/operations/composite/FilterContoursOperation.java @@ -95,20 +95,20 @@ public class FilterContoursOperation implements Operation { @SuppressWarnings("JavadocMethod") public FilterContoursOperation(InputSocket.Factory inputSocketFactory, OutputSocket.Factory outputSocketFactory) { - this.contoursSocket = inputSocketFactory.create(contoursHint); - this.minAreaSocket = inputSocketFactory.create(minAreaHint); - this.minPerimeterSocket = inputSocketFactory.create(minPerimeterHint); - this.minWidthSocket = inputSocketFactory.create(minWidthHint); - this.maxWidthSocket = inputSocketFactory.create(maxWidthHint); - this.minHeightSocket = inputSocketFactory.create(minHeightHint); - this.maxHeightSocket = inputSocketFactory.create(maxHeightHint); - this.soliditySocket = inputSocketFactory.create(solidityHint); - this.minVertexSocket = inputSocketFactory.create(minVertexHint); - this.maxVertexSocket = inputSocketFactory.create(maxVertexHint); - this.minRatioSocket = inputSocketFactory.create(minRatioHint); - this.maxRatioSocket = inputSocketFactory.create(maxRatioHint); - - this.outputSocket = outputSocketFactory.create(contoursHint); + this.contoursSocket = inputSocketFactory.create(contoursHint, "contours"); + this.minAreaSocket = inputSocketFactory.create(minAreaHint, "min-area"); + this.minPerimeterSocket = inputSocketFactory.create(minPerimeterHint, "min-perimeter"); + this.minWidthSocket = inputSocketFactory.create(minWidthHint, "min-width"); + this.maxWidthSocket = inputSocketFactory.create(maxWidthHint, "max-width"); + this.minHeightSocket = inputSocketFactory.create(minHeightHint, "min-height"); + this.maxHeightSocket = inputSocketFactory.create(maxHeightHint, "max-height"); + this.soliditySocket = inputSocketFactory.create(solidityHint, "solidity"); + this.minVertexSocket = inputSocketFactory.create(minVertexHint, "min-vertices"); + this.maxVertexSocket = inputSocketFactory.create(maxVertexHint, "max-vertices"); + this.minRatioSocket = inputSocketFactory.create(minRatioHint, "min-ratio"); + this.maxRatioSocket = inputSocketFactory.create(maxRatioHint, "max-ratio"); + + this.outputSocket = outputSocketFactory.create(contoursHint, "filtered-contours"); } @Override diff --git a/core/src/main/java/edu/wpi/grip/core/operations/composite/FilterLinesOperation.java b/core/src/main/java/edu/wpi/grip/core/operations/composite/FilterLinesOperation.java index 1f4e9f46f8..5067e65b6c 100644 --- a/core/src/main/java/edu/wpi/grip/core/operations/composite/FilterLinesOperation.java +++ b/core/src/main/java/edu/wpi/grip/core/operations/composite/FilterLinesOperation.java @@ -50,11 +50,11 @@ public class FilterLinesOperation implements Operation { @SuppressWarnings("JavadocMethod") public FilterLinesOperation(InputSocket.Factory inputSocketFactory, OutputSocket.Factory outputSocketFactory) { - this.inputSocket = inputSocketFactory.create(inputHint); - this.minLengthSocket = inputSocketFactory.create(minLengthHint); - this.angleSocket = inputSocketFactory.create(angleHint); + this.inputSocket = inputSocketFactory.create(inputHint, "lines"); + this.minLengthSocket = inputSocketFactory.create(minLengthHint, "min-length"); + this.angleSocket = inputSocketFactory.create(angleHint, "angle"); - this.linesOutputSocket = outputSocketFactory.create(outputHint); + this.linesOutputSocket = outputSocketFactory.create(outputHint, "filtered-lines"); } @Override diff --git a/core/src/main/java/edu/wpi/grip/core/operations/composite/FindBlobsOperation.java b/core/src/main/java/edu/wpi/grip/core/operations/composite/FindBlobsOperation.java index 24dc7116ed..d17e4f245b 100644 --- a/core/src/main/java/edu/wpi/grip/core/operations/composite/FindBlobsOperation.java +++ b/core/src/main/java/edu/wpi/grip/core/operations/composite/FindBlobsOperation.java @@ -54,12 +54,12 @@ public class FindBlobsOperation implements Operation { @SuppressWarnings("JavadocMethod") public FindBlobsOperation(InputSocket.Factory inputSocketFactory, OutputSocket.Factory outputSocketFactory) { - this.inputSocket = inputSocketFactory.create(inputHint); - this.minAreaSocket = inputSocketFactory.create(minAreaHint); - this.circularitySocket = inputSocketFactory.create(circularityHint); - this.colorSocket = inputSocketFactory.create(colorHint); + this.inputSocket = inputSocketFactory.create(inputHint, "source-image"); + this.minAreaSocket = inputSocketFactory.create(minAreaHint, "min-area"); + this.circularitySocket = inputSocketFactory.create(circularityHint, "circularity"); + this.colorSocket = inputSocketFactory.create(colorHint, "find-dark-blobs"); - this.outputSocket = outputSocketFactory.create(blobsHint); + this.outputSocket = outputSocketFactory.create(blobsHint, "found-blobs"); } @Override diff --git a/core/src/main/java/edu/wpi/grip/core/operations/composite/FindContoursOperation.java b/core/src/main/java/edu/wpi/grip/core/operations/composite/FindContoursOperation.java index 7abab98167..ba5bcb399a 100644 --- a/core/src/main/java/edu/wpi/grip/core/operations/composite/FindContoursOperation.java +++ b/core/src/main/java/edu/wpi/grip/core/operations/composite/FindContoursOperation.java @@ -52,10 +52,10 @@ public class FindContoursOperation implements Operation { @SuppressWarnings("JavadocMethod") public FindContoursOperation(InputSocket.Factory inputSocketFactory, OutputSocket.Factory outputSocketFactory) { - this.inputSocket = inputSocketFactory.create(inputHint); - this.externalSocket = inputSocketFactory.create(externalHint); + this.inputSocket = inputSocketFactory.create(inputHint, "source-image"); + this.externalSocket = inputSocketFactory.create(externalHint, "find-external-only"); - this.contoursSocket = outputSocketFactory.create(contoursHint); + this.contoursSocket = outputSocketFactory.create(contoursHint, "found-contours"); } @Override diff --git a/core/src/main/java/edu/wpi/grip/core/operations/composite/FindLinesOperation.java b/core/src/main/java/edu/wpi/grip/core/operations/composite/FindLinesOperation.java index b05ea36034..e58dbca94e 100644 --- a/core/src/main/java/edu/wpi/grip/core/operations/composite/FindLinesOperation.java +++ b/core/src/main/java/edu/wpi/grip/core/operations/composite/FindLinesOperation.java @@ -44,8 +44,8 @@ public class FindLinesOperation implements Operation { public FindLinesOperation(InputSocket.Factory inputSocketFactory, OutputSocket.Factory outputSocketFactory) { - this.inputSocket = inputSocketFactory.create(inputHint); - this.linesReportSocket = outputSocketFactory.create(linesHint); + this.inputSocket = inputSocketFactory.create(inputHint, "source-image"); + this.linesReportSocket = outputSocketFactory.create(linesHint, "found-lines"); } @Override diff --git a/core/src/main/java/edu/wpi/grip/core/operations/composite/HSLThresholdOperation.java b/core/src/main/java/edu/wpi/grip/core/operations/composite/HSLThresholdOperation.java index d8de90c6ed..5b7aff737b 100644 --- a/core/src/main/java/edu/wpi/grip/core/operations/composite/HSLThresholdOperation.java +++ b/core/src/main/java/edu/wpi/grip/core/operations/composite/HSLThresholdOperation.java @@ -56,12 +56,12 @@ public class HSLThresholdOperation extends ThresholdOperation { @SuppressWarnings("JavadocMethod") public HSLThresholdOperation(InputSocket.Factory inputSocketFactory, OutputSocket.Factory outputSocketFactory) { - this.inputSocket = inputSocketFactory.create(inputHint); - this.hueSocket = inputSocketFactory.create(hueHint); - this.saturationSocket = inputSocketFactory.create(saturationHint); - this.luminanceSocket = inputSocketFactory.create(luminanceHint); + this.inputSocket = inputSocketFactory.create(inputHint, "source-image"); + this.hueSocket = inputSocketFactory.create(hueHint, "hue"); + this.saturationSocket = inputSocketFactory.create(saturationHint, "saturation"); + this.luminanceSocket = inputSocketFactory.create(luminanceHint, "luminance"); - this.outputSocket = outputSocketFactory.create(outputHint); + this.outputSocket = outputSocketFactory.create(outputHint, "result"); } @Override diff --git a/core/src/main/java/edu/wpi/grip/core/operations/composite/HSVThresholdOperation.java b/core/src/main/java/edu/wpi/grip/core/operations/composite/HSVThresholdOperation.java index c7e8700b95..9019a6deb5 100644 --- a/core/src/main/java/edu/wpi/grip/core/operations/composite/HSVThresholdOperation.java +++ b/core/src/main/java/edu/wpi/grip/core/operations/composite/HSVThresholdOperation.java @@ -56,12 +56,12 @@ public class HSVThresholdOperation extends ThresholdOperation { @SuppressWarnings("JavadocMethod") public HSVThresholdOperation(InputSocket.Factory inputSocketFactory, OutputSocket.Factory outputSocketFactory) { - this.inputSocket = inputSocketFactory.create(inputHint); - this.hueSocket = inputSocketFactory.create(hueHint); - this.saturationSocket = inputSocketFactory.create(saturationHint); - this.valueSocket = inputSocketFactory.create(valueHint); + this.inputSocket = inputSocketFactory.create(inputHint, "source-image"); + this.hueSocket = inputSocketFactory.create(hueHint, "hue"); + this.saturationSocket = inputSocketFactory.create(saturationHint, "saturation"); + this.valueSocket = inputSocketFactory.create(valueHint, "value"); - this.outputSocket = outputSocketFactory.create(outputHint); + this.outputSocket = outputSocketFactory.create(outputHint, "result"); } @Override diff --git a/core/src/main/java/edu/wpi/grip/core/operations/composite/MaskOperation.java b/core/src/main/java/edu/wpi/grip/core/operations/composite/MaskOperation.java index 6521f5947a..772089537b 100644 --- a/core/src/main/java/edu/wpi/grip/core/operations/composite/MaskOperation.java +++ b/core/src/main/java/edu/wpi/grip/core/operations/composite/MaskOperation.java @@ -42,10 +42,10 @@ public class MaskOperation implements Operation { @SuppressWarnings("JavadocMethod") public MaskOperation(InputSocket.Factory inputSocketFactory, OutputSocket.Factory outputSocketFactory) { - this.inputSocket = inputSocketFactory.create(inputHint); - this.maskSocket = inputSocketFactory.create(maskHint); + this.inputSocket = inputSocketFactory.create(inputHint, "source-image"); + this.maskSocket = inputSocketFactory.create(maskHint, "mask-image"); - this.outputSocket = outputSocketFactory.create(outputHint); + this.outputSocket = outputSocketFactory.create(outputHint, "result"); } @Override diff --git a/core/src/main/java/edu/wpi/grip/core/operations/composite/NormalizeOperation.java b/core/src/main/java/edu/wpi/grip/core/operations/composite/NormalizeOperation.java index 6b98c5a979..44ae81ab96 100644 --- a/core/src/main/java/edu/wpi/grip/core/operations/composite/NormalizeOperation.java +++ b/core/src/main/java/edu/wpi/grip/core/operations/composite/NormalizeOperation.java @@ -47,12 +47,12 @@ public class NormalizeOperation implements Operation { @SuppressWarnings("JavadocMethod") public NormalizeOperation(InputSocket.Factory inputSocketFactory, OutputSocket.Factory outputSocketFactory) { - this.srcSocket = inputSocketFactory.create(srcHint); - this.typeSocket = inputSocketFactory.create(typeHint); - this.alphaSocket = inputSocketFactory.create(aHint); - this.betaSocket = inputSocketFactory.create(bHint); + this.srcSocket = inputSocketFactory.create(srcHint, "source-image"); + this.typeSocket = inputSocketFactory.create(typeHint, "normalize-type"); + this.alphaSocket = inputSocketFactory.create(aHint, "alpha"); + this.betaSocket = inputSocketFactory.create(bHint, "beta"); - this.outputSocket = outputSocketFactory.create(dstHint); + this.outputSocket = outputSocketFactory.create(dstHint, "result"); } @Override diff --git a/core/src/main/java/edu/wpi/grip/core/operations/composite/PublishVideoOperation.java b/core/src/main/java/edu/wpi/grip/core/operations/composite/PublishVideoOperation.java index a30153bb9b..612a4fd9df 100644 --- a/core/src/main/java/edu/wpi/grip/core/operations/composite/PublishVideoOperation.java +++ b/core/src/main/java/edu/wpi/grip/core/operations/composite/PublishVideoOperation.java @@ -142,9 +142,9 @@ public PublishVideoOperation(InputSocket.Factory inputSocketFactory) { throw new IllegalStateException("Only one instance of PublishVideoOperation may exist"); } this.inputSocket = inputSocketFactory.create(SocketHints.Inputs.createMatSocketHint("Image", - false)); + false), "source-image"); this.qualitySocket = inputSocketFactory.create(SocketHints.Inputs - .createNumberSliderSocketHint("Quality", 80, 0, 100)); + .createNumberSliderSocketHint("Quality", 80, 0, 100), "image-quality"); numSteps++; serverThread = new Thread(runServer, "Camera Server"); diff --git a/core/src/main/java/edu/wpi/grip/core/operations/composite/RGBThresholdOperation.java b/core/src/main/java/edu/wpi/grip/core/operations/composite/RGBThresholdOperation.java index 965d4fea68..755cee5898 100644 --- a/core/src/main/java/edu/wpi/grip/core/operations/composite/RGBThresholdOperation.java +++ b/core/src/main/java/edu/wpi/grip/core/operations/composite/RGBThresholdOperation.java @@ -54,12 +54,12 @@ public class RGBThresholdOperation extends ThresholdOperation { @SuppressWarnings("JavadocMethod") public RGBThresholdOperation(InputSocket.Factory inputSocketFactory, OutputSocket.Factory outputSocketFactory) { - this.inputSocket = inputSocketFactory.create(inputHint); - this.redSocket = inputSocketFactory.create(redHint); - this.greenSocket = inputSocketFactory.create(greenHint); - this.blueSocket = inputSocketFactory.create(blueHint); + this.inputSocket = inputSocketFactory.create(inputHint, "source-image"); + this.redSocket = inputSocketFactory.create(redHint, "red"); + this.greenSocket = inputSocketFactory.create(greenHint, "green"); + this.blueSocket = inputSocketFactory.create(blueHint, "blue"); - this.outputSocket = outputSocketFactory.create(outputHint); + this.outputSocket = outputSocketFactory.create(outputHint, "result"); } @Override diff --git a/core/src/main/java/edu/wpi/grip/core/operations/composite/ResizeOperation.java b/core/src/main/java/edu/wpi/grip/core/operations/composite/ResizeOperation.java index dcf66f3b6b..9594b330a1 100644 --- a/core/src/main/java/edu/wpi/grip/core/operations/composite/ResizeOperation.java +++ b/core/src/main/java/edu/wpi/grip/core/operations/composite/ResizeOperation.java @@ -46,16 +46,17 @@ public class ResizeOperation implements Operation { public ResizeOperation(InputSocket.Factory inputSocketFactory, OutputSocket.Factory outputSocketFactory) { this.inputSocket = inputSocketFactory.create(SocketHints.Inputs - .createMatSocketHint("Input", false)); + .createMatSocketHint("Input", false), "source-image"); this.widthSocket = inputSocketFactory.create(SocketHints.Inputs - .createNumberSpinnerSocketHint("Width", 640)); + .createNumberSpinnerSocketHint("Width", 640), "new-width"); this.heightSocket = inputSocketFactory.create(SocketHints.Inputs - .createNumberSpinnerSocketHint("Height", 480)); - this.interpolationSocket = inputSocketFactory - .create(SocketHints.createEnumSocketHint("Interpolation", Interpolation.CUBIC)); + .createNumberSpinnerSocketHint("Height", 480), "new-height"); + this.interpolationSocket = inputSocketFactory.create( + SocketHints.createEnumSocketHint("Interpolation", Interpolation.CUBIC), "interp-type" + ); this.outputSocket = outputSocketFactory.create(SocketHints.Outputs - .createMatSocketHint("Output")); + .createMatSocketHint("Output"), "resized-image"); } @Override diff --git a/core/src/main/java/edu/wpi/grip/core/operations/composite/SaveImageOperation.java b/core/src/main/java/edu/wpi/grip/core/operations/composite/SaveImageOperation.java index 37b179481a..72228bec03 100644 --- a/core/src/main/java/edu/wpi/grip/core/operations/composite/SaveImageOperation.java +++ b/core/src/main/java/edu/wpi/grip/core/operations/composite/SaveImageOperation.java @@ -79,13 +79,13 @@ public SaveImageOperation(InputSocket.Factory inputSocketFactory, FileManager fileManager) { this.fileManager = fileManager; - inputSocket = inputSocketFactory.create(inputHint); - fileTypesSocket = inputSocketFactory.create(fileTypeHint); - qualitySocket = inputSocketFactory.create(qualityHint); - periodSocket = inputSocketFactory.create(periodHint); - activeSocket = inputSocketFactory.create(activeHint); + inputSocket = inputSocketFactory.create(inputHint, "source-image"); + fileTypesSocket = inputSocketFactory.create(fileTypeHint, "file-type"); + qualitySocket = inputSocketFactory.create(qualityHint, "compression-quality"); + periodSocket = inputSocketFactory.create(periodHint, "period"); + activeSocket = inputSocketFactory.create(activeHint, "enable-saving"); - outputSocket = outputSocketFactory.create(outputHint); + outputSocket = outputSocketFactory.create(outputHint, "passthrough"); } @Override diff --git a/core/src/main/java/edu/wpi/grip/core/operations/composite/SwitchOperation.java b/core/src/main/java/edu/wpi/grip/core/operations/composite/SwitchOperation.java index f3176ece20..8a9c42b01e 100644 --- a/core/src/main/java/edu/wpi/grip/core/operations/composite/SwitchOperation.java +++ b/core/src/main/java/edu/wpi/grip/core/operations/composite/SwitchOperation.java @@ -40,7 +40,7 @@ public SwitchOperation(InputSocket.Factory inputSocketFactory, OutputSocket.Fact final LinkedSocketHint linkedSocketHint = new LinkedSocketHint(inputSocketFactory, outputSocketFactory); - this.switcherSocket = inputSocketFactory.create(switcherHint); + this.switcherSocket = inputSocketFactory.create(switcherHint, "switch"); this.inputSocket1 = linkedSocketHint.linkedInputSocket("If True"); this.inputSocket2 = linkedSocketHint.linkedInputSocket("If False"); diff --git a/core/src/main/java/edu/wpi/grip/core/operations/composite/ThresholdMoving.java b/core/src/main/java/edu/wpi/grip/core/operations/composite/ThresholdMoving.java index 6f11218a7c..9a7a8a6df6 100644 --- a/core/src/main/java/edu/wpi/grip/core/operations/composite/ThresholdMoving.java +++ b/core/src/main/java/edu/wpi/grip/core/operations/composite/ThresholdMoving.java @@ -30,8 +30,12 @@ public class ThresholdMoving implements Operation { @SuppressWarnings("JavadocMethod") public ThresholdMoving(InputSocket.Factory inputSocketFactory, OutputSocket.Factory outputSocketFactory) { - imageSocket = inputSocketFactory.create(SocketHints.Inputs.createMatSocketHint("image", false)); - outputSocket = outputSocketFactory.create(SocketHints.Outputs.createMatSocketHint("moved")); + imageSocket = inputSocketFactory.create( + SocketHints.Inputs.createMatSocketHint("image", false), "source-image" + ); + outputSocket = outputSocketFactory.create( + SocketHints.Outputs.createMatSocketHint("moved"), "diff-image" + ); lastImage = new Mat(); } diff --git a/core/src/main/java/edu/wpi/grip/core/operations/composite/ValveOperation.java b/core/src/main/java/edu/wpi/grip/core/operations/composite/ValveOperation.java index aff111cc08..4f0e1a9b7d 100644 --- a/core/src/main/java/edu/wpi/grip/core/operations/composite/ValveOperation.java +++ b/core/src/main/java/edu/wpi/grip/core/operations/composite/ValveOperation.java @@ -35,7 +35,7 @@ public ValveOperation(InputSocket.Factory inputSocketFactory, OutputSocket.Facto outputSocketFactory); final SocketHint switcherHint = SocketHints.createBooleanSocketHint("valve", true); - this.switcherSocket = inputSocketFactory.create(switcherHint); + this.switcherSocket = inputSocketFactory.create(switcherHint, "switcher"); this.inputSocket = linkedSocketHint.linkedInputSocket("Input"); this.outputSocket = linkedSocketHint.linkedOutputSocket("Output"); diff --git a/core/src/main/java/edu/wpi/grip/core/operations/composite/WatershedOperation.java b/core/src/main/java/edu/wpi/grip/core/operations/composite/WatershedOperation.java index 13181916e2..11ff0e9e84 100644 --- a/core/src/main/java/edu/wpi/grip/core/operations/composite/WatershedOperation.java +++ b/core/src/main/java/edu/wpi/grip/core/operations/composite/WatershedOperation.java @@ -67,9 +67,9 @@ public class WatershedOperation implements Operation { @SuppressWarnings("JavadocMethod") public WatershedOperation(InputSocket.Factory inputSocketFactory, OutputSocket.Factory outputSocketFactory) { - srcSocket = inputSocketFactory.create(srcHint); - contoursSocket = inputSocketFactory.create(contoursHint); - outputSocket = outputSocketFactory.create(outputHint); + srcSocket = inputSocketFactory.create(srcHint, "source-image"); + contoursSocket = inputSocketFactory.create(contoursHint, "contours"); + outputSocket = outputSocketFactory.create(outputHint, "feature-contours"); } @Override diff --git a/core/src/main/java/edu/wpi/grip/core/operations/network/NetworkPublishOperation.java b/core/src/main/java/edu/wpi/grip/core/operations/network/NetworkPublishOperation.java index 9ebf11534c..95cf184009 100644 --- a/core/src/main/java/edu/wpi/grip/core/operations/network/NetworkPublishOperation.java +++ b/core/src/main/java/edu/wpi/grip/core/operations/network/NetworkPublishOperation.java @@ -42,8 +42,8 @@ protected NetworkPublishOperation(InputSocket.Factory isf, Class dataType) { .identifier("Data") .build(); this.dataType = dataType; - this.dataSocket = isf.create(dataHint); - this.nameSocket = isf.create(nameHint); + this.dataSocket = isf.create(dataHint, "data-type"); + this.nameSocket = isf.create(nameHint, "published-name"); } @Override diff --git a/core/src/main/java/edu/wpi/grip/core/operations/opencv/MatFieldAccessor.java b/core/src/main/java/edu/wpi/grip/core/operations/opencv/MatFieldAccessor.java index fb6dff4ac6..20c2d2cd89 100644 --- a/core/src/main/java/edu/wpi/grip/core/operations/opencv/MatFieldAccessor.java +++ b/core/src/main/java/edu/wpi/grip/core/operations/opencv/MatFieldAccessor.java @@ -48,14 +48,14 @@ public class MatFieldAccessor implements CVOperation { @SuppressWarnings("JavadocMethod") public MatFieldAccessor(InputSocket.Factory inputSocketFactory, OutputSocket.Factory outputSocketFactory) { - this.inputSocket = inputSocketFactory.create(matHint); - - this.sizeSocket = outputSocketFactory.create(sizeHint); - this.emptySocket = outputSocketFactory.create(emptyHint); - this.channelsSocket = outputSocketFactory.create(channelsHint); - this.colsSocket = outputSocketFactory.create(colsHint); - this.rowsSocket = outputSocketFactory.create(rowsHint); - this.highValueSocket = outputSocketFactory.create(highValueHint); + this.inputSocket = inputSocketFactory.create(matHint, "mat"); + + this.sizeSocket = outputSocketFactory.create(sizeHint, "size"); + this.emptySocket = outputSocketFactory.create(emptyHint, "empty"); + this.channelsSocket = outputSocketFactory.create(channelsHint, "channels"); + this.colsSocket = outputSocketFactory.create(colsHint, "columns"); + this.rowsSocket = outputSocketFactory.create(rowsHint, "rows"); + this.highValueSocket = outputSocketFactory.create(highValueHint, "largest-value"); } @Override diff --git a/core/src/main/java/edu/wpi/grip/core/operations/opencv/MinMaxLoc.java b/core/src/main/java/edu/wpi/grip/core/operations/opencv/MinMaxLoc.java index cc0771597c..cb8015ef4b 100644 --- a/core/src/main/java/edu/wpi/grip/core/operations/opencv/MinMaxLoc.java +++ b/core/src/main/java/edu/wpi/grip/core/operations/opencv/MinMaxLoc.java @@ -51,13 +51,13 @@ public class MinMaxLoc implements CVOperation { @SuppressWarnings("JavadocMethod") public MinMaxLoc(InputSocket.Factory inputSocketFactory, OutputSocket.Factory outputSocketFactory) { - this.srcSocket = inputSocketFactory.create(srcInputHint); - this.maskSocket = inputSocketFactory.create(maskInputHint); + this.srcSocket = inputSocketFactory.create(srcInputHint, "source-image"); + this.maskSocket = inputSocketFactory.create(maskInputHint, "mask-image"); - this.minValSocket = outputSocketFactory.create(minValOutputHint); - this.maxValSocket = outputSocketFactory.create(maxValOutputHint); - this.minLocSocket = outputSocketFactory.create(minLocOutputHint); - this.maxLocSocket = outputSocketFactory.create(maxLocOutputHint); + this.minValSocket = outputSocketFactory.create(minValOutputHint, "min-value"); + this.maxValSocket = outputSocketFactory.create(maxValOutputHint, "max-value"); + this.minLocSocket = outputSocketFactory.create(minLocOutputHint, "min-value-location"); + this.maxLocSocket = outputSocketFactory.create(maxLocOutputHint, "max-value-location"); } @Override diff --git a/core/src/main/java/edu/wpi/grip/core/operations/opencv/NewPointOperation.java b/core/src/main/java/edu/wpi/grip/core/operations/opencv/NewPointOperation.java index 5389ed710f..608c1a843d 100644 --- a/core/src/main/java/edu/wpi/grip/core/operations/opencv/NewPointOperation.java +++ b/core/src/main/java/edu/wpi/grip/core/operations/opencv/NewPointOperation.java @@ -38,10 +38,10 @@ public class NewPointOperation implements CVOperation { @SuppressWarnings("JavadocMethod") public NewPointOperation(InputSocket.Factory inputSocketFactory, OutputSocket.Factory outputSocketFactory) { - this.xSocket = inputSocketFactory.create(xHint); - this.ySocket = inputSocketFactory.create(yHint); + this.xSocket = inputSocketFactory.create(xHint, "x"); + this.ySocket = inputSocketFactory.create(yHint, "y"); - this.outputSocket = outputSocketFactory.create(outputHint); + this.outputSocket = outputSocketFactory.create(outputHint, "result"); } @Override diff --git a/core/src/main/java/edu/wpi/grip/core/operations/opencv/NewSizeOperation.java b/core/src/main/java/edu/wpi/grip/core/operations/opencv/NewSizeOperation.java index 8f2eaffb45..a7b3b920b4 100644 --- a/core/src/main/java/edu/wpi/grip/core/operations/opencv/NewSizeOperation.java +++ b/core/src/main/java/edu/wpi/grip/core/operations/opencv/NewSizeOperation.java @@ -37,10 +37,10 @@ public class NewSizeOperation implements CVOperation { @SuppressWarnings("JavadocMethod") public NewSizeOperation(InputSocket.Factory inputSocketFactory, OutputSocket.Factory outputSocketFactory) { - this.widthSocket = inputSocketFactory.create(widthHint); - this.heightSocket = inputSocketFactory.create(heightHint); + this.widthSocket = inputSocketFactory.create(widthHint, "width"); + this.heightSocket = inputSocketFactory.create(heightHint, "height"); - this.outputSocket = outputSocketFactory.create(outputHint); + this.outputSocket = outputSocketFactory.create(outputHint, "result"); } @Override diff --git a/core/src/main/java/edu/wpi/grip/core/serialization/Project.java b/core/src/main/java/edu/wpi/grip/core/serialization/Project.java index 64812c491e..ca8d2fdaae 100644 --- a/core/src/main/java/edu/wpi/grip/core/serialization/Project.java +++ b/core/src/main/java/edu/wpi/grip/core/serialization/Project.java @@ -2,13 +2,17 @@ import edu.wpi.grip.core.Pipeline; import edu.wpi.grip.core.PipelineRunner; +import edu.wpi.grip.core.VersionManager; import edu.wpi.grip.core.events.DirtiesSaveEvent; +import edu.wpi.grip.core.exception.IncompatibleVersionException; +import edu.wpi.grip.core.exception.InvalidSaveException; import com.google.common.annotations.VisibleForTesting; import com.google.common.eventbus.Subscribe; import com.google.common.reflect.ClassPath; import com.thoughtworks.xstream.XStream; import com.thoughtworks.xstream.annotations.XStreamAlias; +import com.thoughtworks.xstream.converters.ConversionException; import com.thoughtworks.xstream.converters.reflection.PureJavaReflectionProvider; import java.io.File; @@ -25,6 +29,7 @@ import java.util.List; import java.util.Optional; import java.util.function.Consumer; + import javax.inject.Inject; import javax.inject.Singleton; @@ -37,6 +42,7 @@ public class Project { protected final XStream xstream = new XStream(new PureJavaReflectionProvider()); @Inject private Pipeline pipeline; + private ProjectModel model; @Inject private PipelineRunner pipelineRunner; private Optional file = Optional.empty(); @@ -47,13 +53,16 @@ public void initialize(StepConverter stepConverter, SourceConverter sourceConverter, SocketConverter socketConverter, ConnectionConverter connectionConverter, - ProjectSettingsConverter projectSettingsConverter) { + ProjectSettingsConverter projectSettingsConverter, + VersionConverter versionConverter) { + model = new ProjectModel(pipeline, VersionManager.CURRENT_VERSION); xstream.setMode(XStream.NO_REFERENCES); xstream.registerConverter(stepConverter); xstream.registerConverter(sourceConverter); xstream.registerConverter(socketConverter); xstream.registerConverter(connectionConverter); xstream.registerConverter(projectSettingsConverter); + xstream.registerConverter(versionConverter); try { ClassPath cp = ClassPath.from(getClass().getClassLoader()); cp.getAllClasses() @@ -109,10 +118,31 @@ public void open(String projectXml) { } @VisibleForTesting - void open(Reader reader) { + void open(Reader reader) throws IncompatibleVersionException { pipelineRunner.stopAndAwait(); this.pipeline.clear(); - this.xstream.fromXML(reader); + try { + Object loaded = xstream.fromXML(reader); + if (loaded instanceof Pipeline) { + // Unversioned pre-2.0.0 save. + // It's compatible with the current version because it loaded without exceptions. + // When saved, the old save file will be upgraded to the current version. + model = new ProjectModel(pipeline, VersionManager.CURRENT_VERSION); + } else if (loaded instanceof ProjectModel) { + // Version 2.0.0 or above. Since we got to this point, we know it's compatible + // (otherwise a ConversionException would have been thrown). + // Saves from different versions of GRIP will be upgraded/downgraded to the current version + model = new ProjectModel(pipeline, VersionManager.CURRENT_VERSION); + } else { + // Uhh... probably a future version + throw new InvalidSaveException( + String.format("Unknown save format (loaded a %s)", loaded.getClass().getName()) + ); + } + } catch (ConversionException e) { + // Incompatible save, or a bug with de/serialization + throw new InvalidSaveException("Incompatible operations in save file", e); + } pipelineRunner.startAsync(); saveIsDirty.set(false); } @@ -129,7 +159,7 @@ public void save(File file) throws IOException { } public void save(Writer writer) { - this.xstream.toXML(this.pipeline, writer); + this.xstream.toXML(model, writer); saveIsDirty.set(false); } diff --git a/core/src/main/java/edu/wpi/grip/core/serialization/ProjectModel.java b/core/src/main/java/edu/wpi/grip/core/serialization/ProjectModel.java new file mode 100644 index 0000000000..d4ee4f89dd --- /dev/null +++ b/core/src/main/java/edu/wpi/grip/core/serialization/ProjectModel.java @@ -0,0 +1,37 @@ +package edu.wpi.grip.core.serialization; + +import edu.wpi.grip.core.Pipeline; + +import com.github.zafarkhaja.semver.Version; +import com.thoughtworks.xstream.annotations.XStreamAlias; + +/** + * Data model class for saving and loading projects from save files. + */ +@XStreamAlias("grip:Project") +public class ProjectModel { + + private Pipeline pipeline; + private Version version; + + /** + * Only used for XStream deserialization. + */ + public ProjectModel() { + this(null, null); + } + + public ProjectModel(Pipeline pipeline, Version version) { + this.pipeline = pipeline; + this.version = version; + } + + public Pipeline getPipeline() { + return pipeline; + } + + public Version getVersion() { + return version; + } + +} diff --git a/core/src/main/java/edu/wpi/grip/core/serialization/SocketConverter.java b/core/src/main/java/edu/wpi/grip/core/serialization/SocketConverter.java index 060f3a9012..2c6b4d6226 100644 --- a/core/src/main/java/edu/wpi/grip/core/serialization/SocketConverter.java +++ b/core/src/main/java/edu/wpi/grip/core/serialization/SocketConverter.java @@ -18,6 +18,7 @@ import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.List; +import java.util.stream.Collectors; import javax.inject.Inject; @@ -50,18 +51,14 @@ public void marshal(Object obj, HierarchicalStreamWriter writer, MarshallingCont // Save the location of the socket in the pipeline. socket.getStep().ifPresent(step -> { - final List sockets = (socket.getDirection() == Socket.Direction.INPUT) - ? step.getInputSockets() - : step.getOutputSockets(); writer.addAttribute(STEP_ATTRIBUTE, String.valueOf(pipeline.getSteps().indexOf(step))); - writer.addAttribute(SOCKET_ATTRIBUTE, String.valueOf(sockets.indexOf(socket))); + writer.addAttribute(SOCKET_ATTRIBUTE, socket.getUid()); }); socket.getSource().ifPresent(source -> { - final List sockets = source.getOutputSockets(); writer.addAttribute(SOURCE_ATTRIBUTE, String.valueOf(pipeline.getSources() .indexOf(source))); - writer.addAttribute(SOCKET_ATTRIBUTE, String.valueOf(sockets.indexOf(socket))); + writer.addAttribute(SOCKET_ATTRIBUTE, socket.getUid()); }); // Save whether or not output sockets are previewed @@ -114,18 +111,16 @@ public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext co Socket socket; if (reader.getAttribute(STEP_ATTRIBUTE) != null) { final int stepIndex = Integer.parseInt(reader.getAttribute(STEP_ATTRIBUTE)); - final int socketIndex = Integer.parseInt(reader.getAttribute(SOCKET_ATTRIBUTE)); final Step step = pipeline.getSteps().get(stepIndex); socket = (direction == Socket.Direction.INPUT) - ? step.getInputSockets().get(socketIndex) - : step.getOutputSockets().get(socketIndex); + ? socketFor(step.getInputSockets(), reader.getAttribute(SOCKET_ATTRIBUTE)) + : socketFor(step.getOutputSockets(), reader.getAttribute(SOCKET_ATTRIBUTE)); } else if (reader.getAttribute(SOURCE_ATTRIBUTE) != null) { final int sourceIndex = Integer.parseInt(reader.getAttribute(SOURCE_ATTRIBUTE)); - final int socketIndex = Integer.parseInt(reader.getAttribute(SOCKET_ATTRIBUTE)); final Source source = pipeline.getSources().get(sourceIndex); - socket = source.getOutputSockets().get(socketIndex); + socket = socketFor(source.getOutputSockets(), reader.getAttribute(SOCKET_ATTRIBUTE)); } else { throw new ConversionException("Sockets must have either a step or source attribute"); } @@ -174,6 +169,25 @@ private Class getDeserializedType(Socket socket) { + socketType); } + private S socketFor(List sockets, String attributeValue) { + if (attributeValue.matches("\\d+")) { + // All numbers, assume it's an index + return sockets.get(Integer.parseInt(attributeValue)); + } else { + // Letters -- assume it's a socket UID + List matching = sockets.stream() + .filter(s -> s.getUid().equals(attributeValue)) + .collect(Collectors.toList()); + if (matching.size() > 1) { + throw new ConversionException("Multiple sockets with UID '" + attributeValue + "'"); + } else if (matching.isEmpty()) { + throw new ConversionException("No sockets with UID '" + attributeValue + "'"); + } else { + return matching.get(0); + } + } + } + @Override public boolean canConvert(Class type) { return Socket.class.isAssignableFrom(type); diff --git a/core/src/main/java/edu/wpi/grip/core/serialization/VersionConverter.java b/core/src/main/java/edu/wpi/grip/core/serialization/VersionConverter.java new file mode 100644 index 0000000000..a735f13c8b --- /dev/null +++ b/core/src/main/java/edu/wpi/grip/core/serialization/VersionConverter.java @@ -0,0 +1,31 @@ +package edu.wpi.grip.core.serialization; + +import com.github.zafarkhaja.semver.Version; +import com.thoughtworks.xstream.converters.Converter; +import com.thoughtworks.xstream.converters.MarshallingContext; +import com.thoughtworks.xstream.converters.UnmarshallingContext; +import com.thoughtworks.xstream.io.HierarchicalStreamReader; +import com.thoughtworks.xstream.io.HierarchicalStreamWriter; + +/** + * Converter for {@link Version GRIP verisons}. + */ +public class VersionConverter implements Converter { + + @Override + public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) { + Version v = (Version) source; + writer.setValue(v.toString()); + } + + @Override + public Version unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) { + return Version.valueOf(reader.getValue()); + } + + @Override + public boolean canConvert(Class type) { + return Version.class.isAssignableFrom(type); + } + +} diff --git a/core/src/main/java/edu/wpi/grip/core/sockets/InputSocket.java b/core/src/main/java/edu/wpi/grip/core/sockets/InputSocket.java index 84b5450998..6bc8e68684 100644 --- a/core/src/main/java/edu/wpi/grip/core/sockets/InputSocket.java +++ b/core/src/main/java/edu/wpi/grip/core/sockets/InputSocket.java @@ -28,7 +28,16 @@ public interface InputSocket extends Socket { void onValueChanged(); interface Factory { - InputSocket create(SocketHint hint); + /** + * Creates a new input socket from a socket hint. This should only be used for + * generated sockets (like for Python operations) or for templated operations. For + * everything else, use {@link #create(SocketHint, String)}. + */ + default InputSocket create(SocketHint hint) { + return create(hint, hint.getIdentifier().toLowerCase().replaceAll("\\s+", "-")); + } + + InputSocket create(SocketHint hint, String uid); } /** @@ -47,6 +56,11 @@ public Decorator(InputSocket socket) { this.decorated = socket; } + @Override + public String getUid() { + return decorated.getUid(); + } + @Override public Direction getDirection() { return decorated.getDirection(); diff --git a/core/src/main/java/edu/wpi/grip/core/sockets/InputSocketImpl.java b/core/src/main/java/edu/wpi/grip/core/sockets/InputSocketImpl.java index 40456510e5..83537b11ec 100644 --- a/core/src/main/java/edu/wpi/grip/core/sockets/InputSocketImpl.java +++ b/core/src/main/java/edu/wpi/grip/core/sockets/InputSocketImpl.java @@ -3,6 +3,7 @@ import edu.wpi.grip.core.Connection; +import com.google.common.annotations.VisibleForTesting; import com.google.common.eventbus.EventBus; import com.google.inject.Inject; import com.google.inject.Singleton; @@ -19,12 +20,18 @@ public class InputSocketImpl extends SocketImpl implements InputSocket { private final AtomicBoolean dirty = new AtomicBoolean(false); + @VisibleForTesting + InputSocketImpl(EventBus eventBus, SocketHint socketHint) { + this(eventBus, socketHint, socketHint.getIdentifier()); + } + /** * @param eventBus The Guava {@link EventBus} used by the application. * @param socketHint {@link #getSocketHint} + * @param uid a unique string for identifying this socket */ - InputSocketImpl(EventBus eventBus, SocketHint socketHint) { - super(eventBus, socketHint, Socket.Direction.INPUT); + InputSocketImpl(EventBus eventBus, SocketHint socketHint, String uid) { + super(eventBus, socketHint, Socket.Direction.INPUT, uid); } /** @@ -63,8 +70,8 @@ public static class FactoryImpl implements Factory { } @Override - public InputSocket create(SocketHint hint) { - return new InputSocketImpl<>(eventBus, hint); + public InputSocket create(SocketHint hint, String uid) { + return new InputSocketImpl<>(eventBus, hint, uid); } } diff --git a/core/src/main/java/edu/wpi/grip/core/sockets/OutputSocket.java b/core/src/main/java/edu/wpi/grip/core/sockets/OutputSocket.java index 3bb44e484f..27c9eaa8ad 100644 --- a/core/src/main/java/edu/wpi/grip/core/sockets/OutputSocket.java +++ b/core/src/main/java/edu/wpi/grip/core/sockets/OutputSocket.java @@ -12,6 +12,7 @@ public interface OutputSocket extends Socket { /** * @return Whether or not this socket is shown in a preview in the GUI. + * * @see #setPreviewed(boolean) d(boolean) */ boolean isPreviewed(); @@ -27,6 +28,15 @@ public interface OutputSocket extends Socket { void resetValueToInitial(); interface Factory { - OutputSocket create(SocketHint hint); + /** + * Creates a new output socket from a socket hint. This should only be used for + * generated sockets (like for Python operations) or for templated operations. For + * everything else, use {@link #create(SocketHint, String)}. + */ + default OutputSocket create(SocketHint hint) { + return create(hint, hint.getIdentifier().toLowerCase().replaceAll("\\s+", "-")); + } + + OutputSocket create(SocketHint hint, String uid); } } diff --git a/core/src/main/java/edu/wpi/grip/core/sockets/OutputSocketImpl.java b/core/src/main/java/edu/wpi/grip/core/sockets/OutputSocketImpl.java index 7fed378fec..03e615c141 100644 --- a/core/src/main/java/edu/wpi/grip/core/sockets/OutputSocketImpl.java +++ b/core/src/main/java/edu/wpi/grip/core/sockets/OutputSocketImpl.java @@ -3,6 +3,7 @@ import edu.wpi.grip.core.events.SocketPreviewChangedEvent; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.MoreObjects; import com.google.common.eventbus.EventBus; import com.google.inject.Inject; @@ -21,12 +22,18 @@ public class OutputSocketImpl extends SocketImpl implements OutputSocket socketHint) { + this(eventBus, socketHint, socketHint.getIdentifier()); + } + /** * @param eventBus The Guava {@link EventBus} used by the application. * @param socketHint {@link #getSocketHint} + * @param uid a unique string for identifying this socket */ - OutputSocketImpl(EventBus eventBus, SocketHint socketHint) { - super(eventBus, socketHint, Direction.OUTPUT); + OutputSocketImpl(EventBus eventBus, SocketHint socketHint, String uid) { + super(eventBus, socketHint, Direction.OUTPUT, uid); this.eventBus = eventBus; } @@ -72,8 +79,8 @@ public static class FactoryImpl implements OutputSocket.Factory { } @Override - public OutputSocket create(SocketHint hint) { - return new OutputSocketImpl<>(eventBus, hint); + public OutputSocket create(SocketHint hint, String uid) { + return new OutputSocketImpl<>(eventBus, hint, uid); } } } diff --git a/core/src/main/java/edu/wpi/grip/core/sockets/Socket.java b/core/src/main/java/edu/wpi/grip/core/sockets/Socket.java index 35a46ab2e5..e2e2b4d71d 100644 --- a/core/src/main/java/edu/wpi/grip/core/sockets/Socket.java +++ b/core/src/main/java/edu/wpi/grip/core/sockets/Socket.java @@ -25,6 +25,17 @@ public interface Socket { */ SocketHint getSocketHint(); + /** + * Gets a String that uniquely identifies this socket. This only needs to be unique for the set + * of sockets containing this one (e.g. only per-operation or per-source); it does not need to be + * universally unique. However, this is not allowed to change; even if + * the name, view, or even the type changes, the UID has to be constant in order for projects + * saved from different versions of GRIP to be compatible. + * + * @implSpec This value MAY NOT change + */ + String getUid(); + /** * Set the value of the socket using an {@link Optional}, and fire off a {@link * edu.wpi.grip.core.events.SocketChangedEvent}. @@ -60,6 +71,7 @@ default void setValue(@Nullable T value) { * If this socket is in a step return it. * * @return The step that this socket is part of + * * @see #getSource() */ Optional getStep(); @@ -73,6 +85,7 @@ default void setValue(@Nullable T value) { * If this socket is in a source return it. * * @return The source that this socket is part of. + * * @see #getStep() */ Optional getSource(); diff --git a/core/src/main/java/edu/wpi/grip/core/sockets/SocketImpl.java b/core/src/main/java/edu/wpi/grip/core/sockets/SocketImpl.java index 84b902b727..80df912acb 100644 --- a/core/src/main/java/edu/wpi/grip/core/sockets/SocketImpl.java +++ b/core/src/main/java/edu/wpi/grip/core/sockets/SocketImpl.java @@ -28,6 +28,7 @@ public class SocketImpl implements Socket { private final Direction direction; private final Set connections = new HashSet<>(); private final SocketHint socketHint; + private final String uid; private Optional step = Optional.empty(); private Optional source = Optional.empty(); private Optional value = Optional.empty(); @@ -38,10 +39,11 @@ public class SocketImpl implements Socket { * @param socketHint {@link #getSocketHint} * @param direction The direction that this socket represents */ - SocketImpl(EventBus eventBus, SocketHint socketHint, Direction direction) { + SocketImpl(EventBus eventBus, SocketHint socketHint, Direction direction, String uid) { this.eventBus = checkNotNull(eventBus, "EventBus can not be null"); this.socketHint = checkNotNull(socketHint, "Socket Hint can not be null"); this.direction = checkNotNull(direction, "Direction can not be null"); + this.uid = checkNotNull(uid, "UID cannot be null"); } @Override @@ -49,6 +51,11 @@ public SocketHint getSocketHint() { return socketHint; } + @Override + public String getUid() { + return uid; + } + @Override public void setValueOptional(Optional optionalValue) { checkNotNull(optionalValue, "The optional value can not be null"); @@ -127,6 +134,7 @@ public void removeConnection(Connection connection) { @Override public String toString() { return MoreObjects.toStringHelper(this) + .add("UID", getUid()) .add("socketHint", getSocketHint()) .add("value", getValue()) .add("direction", getDirection()) diff --git a/core/src/main/java/edu/wpi/grip/core/sources/CameraSource.java b/core/src/main/java/edu/wpi/grip/core/sources/CameraSource.java index 0fbb07e5d5..9936ec1ecb 100644 --- a/core/src/main/java/edu/wpi/grip/core/sources/CameraSource.java +++ b/core/src/main/java/edu/wpi/grip/core/sources/CameraSource.java @@ -135,8 +135,8 @@ public class CameraSource extends Source implements RestartableService { @Assisted final Properties properties) throws MalformedURLException { super(exceptionWitnessFactory); this.eventBus = eventBus; - this.frameOutputSocket = outputSocketFactory.create(imageOutputHint); - this.frameRateOutputSocket = outputSocketFactory.create(frameRateOutputHint); + this.frameOutputSocket = outputSocketFactory.create(imageOutputHint, "image-output"); + this.frameRateOutputSocket = outputSocketFactory.create(frameRateOutputHint, "fps-output"); this.properties = properties; final String deviceNumberProperty = properties.getProperty(DEVICE_NUMBER_PROPERTY); diff --git a/core/src/main/java/edu/wpi/grip/core/sources/HttpSource.java b/core/src/main/java/edu/wpi/grip/core/sources/HttpSource.java index 3c36260cd5..a78f65cfc6 100644 --- a/core/src/main/java/edu/wpi/grip/core/sources/HttpSource.java +++ b/core/src/main/java/edu/wpi/grip/core/sources/HttpSource.java @@ -90,7 +90,7 @@ public interface Factory { super(exceptionWitnessFactory); this.path = path; this.imageHandler = handlers.computeIfAbsent(path, p -> new HttpImageHandler(store, p)); - this.imageOutput = osf.create(outputHint); + this.imageOutput = osf.create(outputHint, "image-output"); this.eventBus = eventBus; // Will add the handler only when the first HttpSource is created -- no-op every subsequent time // (Otherwise, multiple handlers would be getting called and it'd be a mess) diff --git a/core/src/main/java/edu/wpi/grip/core/sources/ImageFileSource.java b/core/src/main/java/edu/wpi/grip/core/sources/ImageFileSource.java index d7bded3c70..397f84fa35 100644 --- a/core/src/main/java/edu/wpi/grip/core/sources/ImageFileSource.java +++ b/core/src/main/java/edu/wpi/grip/core/sources/ImageFileSource.java @@ -72,7 +72,7 @@ private ImageFileSource( super(exceptionWitnessFactory); this.path = checkNotNull(path, "Path can not be null"); this.name = Files.getNameWithoutExtension(this.path); - this.outputSocket = outputSocketFactory.create(imageOutputHint); + this.outputSocket = outputSocketFactory.create(imageOutputHint, "image-output"); } /** diff --git a/core/src/main/java/edu/wpi/grip/core/sources/MultiImageFileSource.java b/core/src/main/java/edu/wpi/grip/core/sources/MultiImageFileSource.java index bd3fbdbbc7..d643dcb1f9 100644 --- a/core/src/main/java/edu/wpi/grip/core/sources/MultiImageFileSource.java +++ b/core/src/main/java/edu/wpi/grip/core/sources/MultiImageFileSource.java @@ -106,7 +106,7 @@ private MultiImageFileSource( final int index) { super(exceptionWitnessFactory); this.eventBus = eventBus; - this.outputSocket = outputSocketFactory.create(imageOutputHint); + this.outputSocket = outputSocketFactory.create(imageOutputHint, "image-output"); this.index = new AtomicInteger(checkElementIndex(index, paths.length, "File List Index")); this.paths = Arrays.asList(paths); } diff --git a/core/src/main/java/edu/wpi/grip/core/sources/NetworkTableEntrySource.java b/core/src/main/java/edu/wpi/grip/core/sources/NetworkTableEntrySource.java index 9742a4392c..f7b4863599 100644 --- a/core/src/main/java/edu/wpi/grip/core/sources/NetworkTableEntrySource.java +++ b/core/src/main/java/edu/wpi/grip/core/sources/NetworkTableEntrySource.java @@ -107,7 +107,7 @@ public String toProperty() { this.path = path; this.type = type; networkReceiver = networkReceiverFactory.create(path); - output = osf.create(type.createSocketHint()); + output = osf.create(type.createSocketHint(), "data-output"); } @Override diff --git a/core/src/test/java/edu/wpi/grip/core/CoreSanityTest.java b/core/src/test/java/edu/wpi/grip/core/CoreSanityTest.java index 5dcc0ce5b5..a7f5d5c29a 100644 --- a/core/src/test/java/edu/wpi/grip/core/CoreSanityTest.java +++ b/core/src/test/java/edu/wpi/grip/core/CoreSanityTest.java @@ -9,6 +9,7 @@ import edu.wpi.grip.core.sockets.SocketHints; import edu.wpi.grip.core.util.service.SingleActionListener; +import com.github.zafarkhaja.semver.Version; import com.google.common.testing.AbstractPackageSanityTests; import com.google.common.util.concurrent.Service; @@ -41,5 +42,6 @@ public CoreSanityTest() { () -> null)); setDefault(OperationDescription.class, OperationDescription.builder().name("").summary("") .build()); + setDefault(Version.class, VersionManager.CURRENT_VERSION); } } diff --git a/core/src/test/java/edu/wpi/grip/core/sockets/MockInputSocket.java b/core/src/test/java/edu/wpi/grip/core/sockets/MockInputSocket.java index 4848729bbe..3855983097 100644 --- a/core/src/test/java/edu/wpi/grip/core/sockets/MockInputSocket.java +++ b/core/src/test/java/edu/wpi/grip/core/sockets/MockInputSocket.java @@ -5,6 +5,6 @@ public class MockInputSocket extends InputSocketImpl { public MockInputSocket(String name) { - super(new EventBus(), SocketHints.Outputs.createBooleanSocketHint(name, false)); + super(new EventBus(), SocketHints.Outputs.createBooleanSocketHint(name, false), "mock"); } } diff --git a/core/src/test/java/edu/wpi/grip/core/sockets/MockOutputSocket.java b/core/src/test/java/edu/wpi/grip/core/sockets/MockOutputSocket.java index 8fa07a641d..72a59780d7 100644 --- a/core/src/test/java/edu/wpi/grip/core/sockets/MockOutputSocket.java +++ b/core/src/test/java/edu/wpi/grip/core/sockets/MockOutputSocket.java @@ -4,6 +4,6 @@ public class MockOutputSocket extends OutputSocketImpl { public MockOutputSocket(String socketName) { - super(new EventBus(), SocketHints.Outputs.createBooleanSocketHint(socketName, false)); + super(new EventBus(), SocketHints.Outputs.createBooleanSocketHint(socketName, false), "mock"); } } diff --git a/ui/src/main/java/edu/wpi/grip/ui/MainWindowController.java b/ui/src/main/java/edu/wpi/grip/ui/MainWindowController.java index e6d67b1905..3da431ab8f 100644 --- a/ui/src/main/java/edu/wpi/grip/ui/MainWindowController.java +++ b/ui/src/main/java/edu/wpi/grip/ui/MainWindowController.java @@ -3,8 +3,11 @@ import edu.wpi.grip.core.Palette; import edu.wpi.grip.core.Pipeline; import edu.wpi.grip.core.PipelineRunner; +import edu.wpi.grip.core.events.ExceptionClearedEvent; +import edu.wpi.grip.core.events.ExceptionEvent; import edu.wpi.grip.core.events.ProjectSettingsChangedEvent; import edu.wpi.grip.core.events.UnexpectedThrowableEvent; +import edu.wpi.grip.core.exception.InvalidSaveException; import edu.wpi.grip.core.serialization.Project; import edu.wpi.grip.core.settings.ProjectSettings; import edu.wpi.grip.core.settings.SettingsProvider; @@ -12,6 +15,7 @@ import edu.wpi.grip.core.util.service.SingleActionListener; import edu.wpi.grip.ui.codegeneration.Exporter; import edu.wpi.grip.ui.codegeneration.Language; +import edu.wpi.grip.ui.components.ExceptionWitnessResponderButton; import edu.wpi.grip.ui.components.StartStoppableButton; import edu.wpi.grip.ui.util.DPIUtility; @@ -83,6 +87,8 @@ public class MainWindowController { private Palette palette; @Inject private Project project; + @Inject + private ExceptionWitnessResponderButton.Factory ewrbFactory; private Stage aboutDialogStage; @@ -90,6 +96,7 @@ public class MainWindowController { protected void initialize() { pipelineView.prefHeightProperty().bind(bottomPane.heightProperty()); statusBar.getLeftItems().add(startStoppableButtonFactory.create(pipelineRunner)); + statusBar.getRightItems().add(ewrbFactory.create(project, "Incompatible save file")); pipelineRunner.addListener(new SingleActionListener(() -> { final Service.State state = pipelineRunner.state(); final String stateMessage = @@ -169,8 +176,12 @@ public void openProject() { Thread fileOpenThread = new Thread(() -> { try { project.open(file); + eventBus.post(new ExceptionClearedEvent(project)); } catch (IOException e) { eventBus.post(new UnexpectedThrowableEvent(e, "Failed to load save file")); + } catch (InvalidSaveException e) { + pipeline.clear(); + eventBus.post(new ExceptionEvent(project, e.getMessage())); } }, "Project Open Thread"); fileOpenThread.setDaemon(true); @@ -254,10 +265,11 @@ protected void quit() { SafeShutdown.exit(0); } } - + /** * Controls the export button in the main menu. Opens a filechooser with language selection. * The user can select the language to export to, save location and file name. + * * @param actionEvent Unused event passed by the controller. */ public void generate(ActionEvent actionEvent) { diff --git a/ui/src/test/java/edu/wpi/grip/ui/pipeline/input/InputSocketControllerFactoryTest.java b/ui/src/test/java/edu/wpi/grip/ui/pipeline/input/InputSocketControllerFactoryTest.java index 632227c5d3..db17d25f83 100644 --- a/ui/src/test/java/edu/wpi/grip/ui/pipeline/input/InputSocketControllerFactoryTest.java +++ b/ui/src/test/java/edu/wpi/grip/ui/pipeline/input/InputSocketControllerFactoryTest.java @@ -111,6 +111,7 @@ public void testCreateAllKnownInputSocketControllers() throws Exception { verifyThat(controller.getHandle(), NodeMatchers.isEnabled()); verifyThat(controller.getHandle(), NodeMatchers.isVisible()); } + step.setRemoved(); }); } } From f77edf7721b02cd3cd0d8cfeb545610f7f359e85 Mon Sep 17 00:00:00 2001 From: Sam Carlberg Date: Wed, 26 Oct 2016 14:32:00 -0400 Subject: [PATCH 2/9] Add mechanism for alerting the user when they try to do something wrong e.g. trying to generate code with operations that don't support it Post a WarningEvent to the eventbus and an alert dialog will appear. The alert supports markdown formatting for the body text. --- build.gradle | 1 + .../wpi/grip/core/events/WarningEvent.java | 48 +++++++++++++ .../edu/wpi/grip/ui/MainWindowController.java | 38 +++++++--- .../java/edu/wpi/grip/ui/WarningAlert.java | 69 +++++++++++++++++++ .../wpi/grip/ui/codegeneration/Exporter.java | 2 +- .../edu/wpi/grip/ui/warning_alert.css | 3 + 6 files changed, 150 insertions(+), 11 deletions(-) create mode 100644 core/src/main/java/edu/wpi/grip/core/events/WarningEvent.java create mode 100644 ui/src/main/java/edu/wpi/grip/ui/WarningAlert.java create mode 100644 ui/src/main/resources/edu/wpi/grip/ui/warning_alert.css diff --git a/build.gradle b/build.gradle index 081223afa5..a36fa6a2cb 100644 --- a/build.gradle +++ b/build.gradle @@ -411,6 +411,7 @@ project(":ui") { compile group: 'org.controlsfx', name: 'controlsfx', version: '8.40.11' compile group: 'com.hierynomus', name: 'sshj', version: '0.16.0' compile group: 'org.apache.velocity', name: 'velocity', version: '1.7' + compile group: 'org.pegdown', name: 'pegdown', version: '1.6.0' testCompile files(project(':core').sourceSets.test.output.classesDir) testCompile files(project(':core').sourceSets.test.output.resourcesDir) testCompile group: 'org.testfx', name: 'testfx-core', version: '4.0.+' diff --git a/core/src/main/java/edu/wpi/grip/core/events/WarningEvent.java b/core/src/main/java/edu/wpi/grip/core/events/WarningEvent.java new file mode 100644 index 0000000000..9bf0517690 --- /dev/null +++ b/core/src/main/java/edu/wpi/grip/core/events/WarningEvent.java @@ -0,0 +1,48 @@ +package edu.wpi.grip.core.events; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * An event fired when the user should be warned they tried to do something unsupported, such as + * trying to generate code with an operation that doesn't support code gen. + * + *

The event contains a short header text describing the warning and a detailed body text + * that lets the user know why what they attempted was not allowed. This body text supports + * markdown formatting.

+ */ +public class WarningEvent { + + private final String header; + private final String body; + + /** + * Creates a new warning event. + * + * @param header the header or title of the warning (e.g. "Cannot generate code"). + * This should be short and descriptive. + * @param body the body of the warning. + * This should go into detail about what the user did wrong and may use markdown + * formatting. + */ + public WarningEvent(String header, String body) { + checkNotNull(header, "Header text cannot be null"); + checkNotNull(body, "Body text cannot be null"); + this.header = header; + this.body = body; + } + + /** + * Gets the warning header. + */ + public String getHeader() { + return header; + } + + /** + * Gets the warning body. + */ + public String getBody() { + return body; + } + +} diff --git a/ui/src/main/java/edu/wpi/grip/ui/MainWindowController.java b/ui/src/main/java/edu/wpi/grip/ui/MainWindowController.java index e6d67b1905..d8451c36a8 100644 --- a/ui/src/main/java/edu/wpi/grip/ui/MainWindowController.java +++ b/ui/src/main/java/edu/wpi/grip/ui/MainWindowController.java @@ -5,6 +5,7 @@ import edu.wpi.grip.core.PipelineRunner; import edu.wpi.grip.core.events.ProjectSettingsChangedEvent; import edu.wpi.grip.core.events.UnexpectedThrowableEvent; +import edu.wpi.grip.core.events.WarningEvent; import edu.wpi.grip.core.serialization.Project; import edu.wpi.grip.core.settings.ProjectSettings; import edu.wpi.grip.core.settings.SettingsProvider; @@ -17,6 +18,7 @@ import com.google.common.base.CaseFormat; import com.google.common.eventbus.EventBus; +import com.google.common.eventbus.Subscribe; import com.google.common.util.concurrent.Service; import org.controlsfx.control.StatusBar; @@ -29,7 +31,6 @@ import java.util.logging.Logger; import javafx.application.Platform; -import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.Parent; import javafx.scene.Scene; @@ -254,13 +255,13 @@ protected void quit() { SafeShutdown.exit(0); } } - + /** * Controls the export button in the main menu. Opens a filechooser with language selection. * The user can select the language to export to, save location and file name. - * @param actionEvent Unused event passed by the controller. */ - public void generate(ActionEvent actionEvent) { + @FXML + public void generate() { final FileChooser fileChooser = new FileChooser(); fileChooser.setTitle("Export to"); fileChooser.getExtensionFilters().add(new ExtensionFilter(Language.JAVA.name, "*.java")); @@ -273,13 +274,15 @@ public void generate(ActionEvent actionEvent) { } Language lang = Language.get(fileChooser.getSelectedExtensionFilter().getDescription()); Exporter exporter = new Exporter(pipeline.getSteps(), lang, file); - final Set nonExportableSteps = exporter.getNonExportableSteps(); + final Set nonExportableSteps = exporter.getNonExportableStepNames(); if (!nonExportableSteps.isEmpty()) { - StringBuilder b = new StringBuilder("The following steps cannot be exported:\n"); - nonExportableSteps.forEach(n -> b.append(" ").append(n).append('\n')); - Alert alert = new Alert(Alert.AlertType.WARNING); - alert.setContentText(b.toString()); - alert.showAndWait(); + StringBuilder b = new StringBuilder( + "The following steps do not support code generation:\n\n" + ); + nonExportableSteps.stream() + .sorted() + .forEach(n -> b.append(" - ").append(n).append("\n")); + eventBus.post(new WarningEvent("Cannot generate code", b.toString())); return; } Thread exportRunner = new Thread(exporter); @@ -306,4 +309,19 @@ protected void deploy() { dialog.setResizable(true); dialog.showAndWait(); } + + @Subscribe + public void onWarningEvent(WarningEvent e) { + if (Platform.isFxApplicationThread()) { + showWarningAlert(e); + } else { + Platform.runLater(() -> showWarningAlert(e)); + } + } + + private void showWarningAlert(WarningEvent e) { + Alert alert = new WarningAlert(e.getHeader(), e.getBody(), root.getScene().getWindow()); + alert.showAndWait(); + } + } diff --git a/ui/src/main/java/edu/wpi/grip/ui/WarningAlert.java b/ui/src/main/java/edu/wpi/grip/ui/WarningAlert.java new file mode 100644 index 0000000000..87a50eb5c5 --- /dev/null +++ b/ui/src/main/java/edu/wpi/grip/ui/WarningAlert.java @@ -0,0 +1,69 @@ +package edu.wpi.grip.ui; + +import org.pegdown.PegDownProcessor; + +import javafx.scene.control.Alert; +import javafx.scene.effect.BlendMode; +import javafx.scene.web.WebEngine; +import javafx.scene.web.WebView; +import javafx.stage.StageStyle; +import javafx.stage.Window; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * An alert that warns the user when they try to do something unsupported, such as trying to + * generate code with an operation that doesn't support code gen. + * + *

The alert renders the body text as markdown. + *

+ */ +public class WarningAlert extends Alert { + + /** + * Markdown processor. Converts markdown to HTML. + */ + private static final PegDownProcessor mdProcessor = new PegDownProcessor(); + private final WebView webView = new WebView(); + private final WebEngine webEngine = webView.getEngine(); + + // Width and height of the alert dialog + private static final double WIDTH = 360; + private static final double HEIGHT = 180; + + /** + * Creates a new warning alert. + * + * @param header the header text of the alert. This should be short and descriptive. + * @param body the body text of the alert. This supports markdown formatting. + * @param owner the owner window of this alert + * + * @throws NullPointerException if any of the parameters are null + */ + public WarningAlert(String header, String body, Window owner) { + super(AlertType.WARNING); + checkNotNull(header, "The header text cannot be null"); + checkNotNull(body, "The body text cannot be null"); + checkNotNull(owner, "The owner window cannot be null"); + + initStyle(StageStyle.UNDECORATED); + initOwner(owner); + initWebView(); + + setHeaderText(header); + webEngine.loadContent(mdProcessor.markdownToHtml(body)); + getDialogPane().setContent(webView); + } + + /** + * Initializes the web view and engine prior to setting its content. + */ + private void initWebView() { + webView.setBlendMode(BlendMode.DARKEN); // make the background transparent + webEngine.setUserStyleSheetLocation( + getClass().getResource("warning_alert.css").toExternalForm() + ); + webView.setPrefSize(WIDTH, HEIGHT); + } + +} diff --git a/ui/src/main/java/edu/wpi/grip/ui/codegeneration/Exporter.java b/ui/src/main/java/edu/wpi/grip/ui/codegeneration/Exporter.java index cea7aeaf7b..10ade1f883 100644 --- a/ui/src/main/java/edu/wpi/grip/ui/codegeneration/Exporter.java +++ b/ui/src/main/java/edu/wpi/grip/ui/codegeneration/Exporter.java @@ -99,7 +99,7 @@ public void run() { * * @return a set of the names of the non-exportable operations in the pipeline */ - public Set getNonExportableSteps() { + public Set getNonExportableStepNames() { return steps.stream() .filter(s -> { return s.getOperationDescription().category() != OperationDescription.Category.NETWORK; diff --git a/ui/src/main/resources/edu/wpi/grip/ui/warning_alert.css b/ui/src/main/resources/edu/wpi/grip/ui/warning_alert.css new file mode 100644 index 0000000000..62dfc1b558 --- /dev/null +++ b/ui/src/main/resources/edu/wpi/grip/ui/warning_alert.css @@ -0,0 +1,3 @@ +html { + font: 14px Roboto; +} From 33e9cc08d890bf859af5c72c43ac6a8a0e7394f4 Mon Sep 17 00:00:00 2001 From: Sam Carlberg Date: Wed, 26 Oct 2016 16:24:25 -0400 Subject: [PATCH 3/9] Use WarningEvent to notify when a save can't be loaded Loading a file (.grip or otherwise) should never throw a fatal exception now --- .../edu/wpi/grip/core/VersionManager.java | 2 +- .../IncompatibleVersionException.java | 13 ++++++--- .../exception/UnknownSaveFormatException.java | 12 ++++++++ .../wpi/grip/core/serialization/Project.java | 10 +++++-- .../edu/wpi/grip/ui/MainWindowController.java | 29 ++++++++++++++----- 5 files changed, 50 insertions(+), 16 deletions(-) create mode 100644 core/src/main/java/edu/wpi/grip/core/exception/UnknownSaveFormatException.java diff --git a/core/src/main/java/edu/wpi/grip/core/VersionManager.java b/core/src/main/java/edu/wpi/grip/core/VersionManager.java index a512ec70d9..92053267bc 100644 --- a/core/src/main/java/edu/wpi/grip/core/VersionManager.java +++ b/core/src/main/java/edu/wpi/grip/core/VersionManager.java @@ -31,7 +31,7 @@ public static void checkVersionCompatibility(Version current, Version check) throws IncompatibleVersionException { if (check.getMajorVersion() > current.getMajorVersion() || check.getMinorVersion() > current.getMinorVersion()) { - throw new IncompatibleVersionException("Incompatible future version: " + check); + throw new IncompatibleVersionException(check); } } diff --git a/core/src/main/java/edu/wpi/grip/core/exception/IncompatibleVersionException.java b/core/src/main/java/edu/wpi/grip/core/exception/IncompatibleVersionException.java index 0a95532cc2..49d6d2af75 100644 --- a/core/src/main/java/edu/wpi/grip/core/exception/IncompatibleVersionException.java +++ b/core/src/main/java/edu/wpi/grip/core/exception/IncompatibleVersionException.java @@ -1,17 +1,22 @@ package edu.wpi.grip.core.exception; +import com.github.zafarkhaja.semver.Version; + /** * An exception thrown when trying to load a saved project created in an incompatible version * of GRIP. */ public class IncompatibleVersionException extends InvalidSaveException { - public IncompatibleVersionException(String message) { - super(message); + private final Version loaded; + + public IncompatibleVersionException(Version loaded) { + super("Incompatible future version: " + loaded); + this.loaded = loaded; } - public IncompatibleVersionException(String message, Throwable cause) { - super(message, cause); + public Version getLoaded() { + return loaded; } } diff --git a/core/src/main/java/edu/wpi/grip/core/exception/UnknownSaveFormatException.java b/core/src/main/java/edu/wpi/grip/core/exception/UnknownSaveFormatException.java new file mode 100644 index 0000000000..de20b6f32d --- /dev/null +++ b/core/src/main/java/edu/wpi/grip/core/exception/UnknownSaveFormatException.java @@ -0,0 +1,12 @@ +package edu.wpi.grip.core.exception; + +/** + * An exception thrown when loading a save file that can't be deserialized to a known type. + */ +public class UnknownSaveFormatException extends InvalidSaveException { + + public UnknownSaveFormatException(String message) { + super(message); + } + +} diff --git a/core/src/main/java/edu/wpi/grip/core/serialization/Project.java b/core/src/main/java/edu/wpi/grip/core/serialization/Project.java index ca8d2fdaae..0b24feb084 100644 --- a/core/src/main/java/edu/wpi/grip/core/serialization/Project.java +++ b/core/src/main/java/edu/wpi/grip/core/serialization/Project.java @@ -4,8 +4,8 @@ import edu.wpi.grip.core.PipelineRunner; import edu.wpi.grip.core.VersionManager; import edu.wpi.grip.core.events.DirtiesSaveEvent; -import edu.wpi.grip.core.exception.IncompatibleVersionException; import edu.wpi.grip.core.exception.InvalidSaveException; +import edu.wpi.grip.core.exception.UnknownSaveFormatException; import com.google.common.annotations.VisibleForTesting; import com.google.common.eventbus.Subscribe; @@ -14,6 +14,7 @@ import com.thoughtworks.xstream.annotations.XStreamAlias; import com.thoughtworks.xstream.converters.ConversionException; import com.thoughtworks.xstream.converters.reflection.PureJavaReflectionProvider; +import com.thoughtworks.xstream.io.StreamException; import java.io.File; import java.io.FileInputStream; @@ -118,7 +119,7 @@ public void open(String projectXml) { } @VisibleForTesting - void open(Reader reader) throws IncompatibleVersionException { + void open(Reader reader) throws InvalidSaveException { pipelineRunner.stopAndAwait(); this.pipeline.clear(); try { @@ -135,13 +136,16 @@ void open(Reader reader) throws IncompatibleVersionException { model = new ProjectModel(pipeline, VersionManager.CURRENT_VERSION); } else { // Uhh... probably a future version - throw new InvalidSaveException( + throw new UnknownSaveFormatException( String.format("Unknown save format (loaded a %s)", loaded.getClass().getName()) ); } } catch (ConversionException e) { // Incompatible save, or a bug with de/serialization throw new InvalidSaveException("Incompatible operations in save file", e); + } catch (StreamException e) { + // Invalid XML + throw new InvalidSaveException("Invalid XML", e); } pipelineRunner.startAsync(); saveIsDirty.set(false); diff --git a/ui/src/main/java/edu/wpi/grip/ui/MainWindowController.java b/ui/src/main/java/edu/wpi/grip/ui/MainWindowController.java index fd543d4af6..24cef45318 100644 --- a/ui/src/main/java/edu/wpi/grip/ui/MainWindowController.java +++ b/ui/src/main/java/edu/wpi/grip/ui/MainWindowController.java @@ -4,11 +4,11 @@ import edu.wpi.grip.core.Pipeline; import edu.wpi.grip.core.PipelineRunner; import edu.wpi.grip.core.events.ExceptionClearedEvent; -import edu.wpi.grip.core.events.ExceptionEvent; import edu.wpi.grip.core.events.ProjectSettingsChangedEvent; -import edu.wpi.grip.core.events.UnexpectedThrowableEvent; import edu.wpi.grip.core.events.WarningEvent; +import edu.wpi.grip.core.exception.IncompatibleVersionException; import edu.wpi.grip.core.exception.InvalidSaveException; +import edu.wpi.grip.core.exception.UnknownSaveFormatException; import edu.wpi.grip.core.serialization.Project; import edu.wpi.grip.core.settings.ProjectSettings; import edu.wpi.grip.core.settings.SettingsProvider; @@ -16,7 +16,6 @@ import edu.wpi.grip.core.util.service.SingleActionListener; import edu.wpi.grip.ui.codegeneration.Exporter; import edu.wpi.grip.ui.codegeneration.Language; -import edu.wpi.grip.ui.components.ExceptionWitnessResponderButton; import edu.wpi.grip.ui.components.StartStoppableButton; import edu.wpi.grip.ui.util.DPIUtility; @@ -28,6 +27,7 @@ import org.controlsfx.control.StatusBar; import java.io.File; +import java.io.FileNotFoundException; import java.io.IOException; import java.util.Optional; import java.util.Set; @@ -88,8 +88,6 @@ public class MainWindowController { private Palette palette; @Inject private Project project; - @Inject - private ExceptionWitnessResponderButton.Factory ewrbFactory; private Stage aboutDialogStage; @@ -97,7 +95,6 @@ public class MainWindowController { protected void initialize() { pipelineView.prefHeightProperty().bind(bottomPane.heightProperty()); statusBar.getLeftItems().add(startStoppableButtonFactory.create(pipelineRunner)); - statusBar.getRightItems().add(ewrbFactory.create(project, "Incompatible save file")); pipelineRunner.addListener(new SingleActionListener(() -> { final Service.State state = pipelineRunner.state(); final String stateMessage = @@ -178,11 +175,27 @@ public void openProject() { try { project.open(file); eventBus.post(new ExceptionClearedEvent(project)); + } catch (FileNotFoundException e) { + eventBus.post(new WarningEvent( + "File does not exist", + "The file at " + file.getAbsolutePath() + " does not exist.\n\n" + + "It may have been deleted by another program." + )); } catch (IOException e) { - eventBus.post(new UnexpectedThrowableEvent(e, "Failed to load save file")); + eventBus.post(new WarningEvent( + "Could not read file", + "The file at " + file.getAbsolutePath() + " could not be read.\n\n" + + "Caused by: " + e.getMessage() + )); + } catch (UnknownSaveFormatException e) { + pipeline.clear(); + eventBus.post(new WarningEvent("Unknown save format", e.getMessage())); + } catch (IncompatibleVersionException e) { + pipeline.clear(); + eventBus.post(new WarningEvent("Incompatible saved version", e.getLoaded().toString())); } catch (InvalidSaveException e) { pipeline.clear(); - eventBus.post(new ExceptionEvent(project, e.getMessage())); + eventBus.post(new WarningEvent("Invalid save file", e.getMessage())); } }, "Project Open Thread"); fileOpenThread.setDaemon(true); From 033cecab555a1a050ba7bbb980a72cd35a8e395d Mon Sep 17 00:00:00 2001 From: Sam Carlberg Date: Wed, 26 Oct 2016 23:44:58 -0400 Subject: [PATCH 4/9] Remove markdown from alert body content JavaFX webview is a mess. --- build.gradle | 1 - .../wpi/grip/core/events/WarningEvent.java | 8 ++-- .../java/edu/wpi/grip/ui/WarningAlert.java | 40 +++---------------- .../edu/wpi/grip/ui/warning_alert.css | 3 -- 4 files changed, 9 insertions(+), 43 deletions(-) delete mode 100644 ui/src/main/resources/edu/wpi/grip/ui/warning_alert.css diff --git a/build.gradle b/build.gradle index a36fa6a2cb..081223afa5 100644 --- a/build.gradle +++ b/build.gradle @@ -411,7 +411,6 @@ project(":ui") { compile group: 'org.controlsfx', name: 'controlsfx', version: '8.40.11' compile group: 'com.hierynomus', name: 'sshj', version: '0.16.0' compile group: 'org.apache.velocity', name: 'velocity', version: '1.7' - compile group: 'org.pegdown', name: 'pegdown', version: '1.6.0' testCompile files(project(':core').sourceSets.test.output.classesDir) testCompile files(project(':core').sourceSets.test.output.resourcesDir) testCompile group: 'org.testfx', name: 'testfx-core', version: '4.0.+' diff --git a/core/src/main/java/edu/wpi/grip/core/events/WarningEvent.java b/core/src/main/java/edu/wpi/grip/core/events/WarningEvent.java index 9bf0517690..2dbbbe56e3 100644 --- a/core/src/main/java/edu/wpi/grip/core/events/WarningEvent.java +++ b/core/src/main/java/edu/wpi/grip/core/events/WarningEvent.java @@ -4,11 +4,10 @@ /** * An event fired when the user should be warned they tried to do something unsupported, such as - * trying to generate code with an operation that doesn't support code gen. + * trying to generate code with an operation that does not support code gen. * *

The event contains a short header text describing the warning and a detailed body text - * that lets the user know why what they attempted was not allowed. This body text supports - * markdown formatting.

+ * that lets the user know why what they attempted was not allowed.

*/ public class WarningEvent { @@ -21,8 +20,7 @@ public class WarningEvent { * @param header the header or title of the warning (e.g. "Cannot generate code"). * This should be short and descriptive. * @param body the body of the warning. - * This should go into detail about what the user did wrong and may use markdown - * formatting. + * This should go into detail about what the user did wrong. */ public WarningEvent(String header, String body) { checkNotNull(header, "Header text cannot be null"); diff --git a/ui/src/main/java/edu/wpi/grip/ui/WarningAlert.java b/ui/src/main/java/edu/wpi/grip/ui/WarningAlert.java index 87a50eb5c5..c6fe9aa8bf 100644 --- a/ui/src/main/java/edu/wpi/grip/ui/WarningAlert.java +++ b/ui/src/main/java/edu/wpi/grip/ui/WarningAlert.java @@ -1,11 +1,7 @@ package edu.wpi.grip.ui; -import org.pegdown.PegDownProcessor; - import javafx.scene.control.Alert; -import javafx.scene.effect.BlendMode; -import javafx.scene.web.WebEngine; -import javafx.scene.web.WebView; +import javafx.scene.layout.Region; import javafx.stage.StageStyle; import javafx.stage.Window; @@ -14,28 +10,15 @@ /** * An alert that warns the user when they try to do something unsupported, such as trying to * generate code with an operation that doesn't support code gen. - * - *

The alert renders the body text as markdown. - *

*/ public class WarningAlert extends Alert { - /** - * Markdown processor. Converts markdown to HTML. - */ - private static final PegDownProcessor mdProcessor = new PegDownProcessor(); - private final WebView webView = new WebView(); - private final WebEngine webEngine = webView.getEngine(); - - // Width and height of the alert dialog - private static final double WIDTH = 360; - private static final double HEIGHT = 180; - /** * Creates a new warning alert. * * @param header the header text of the alert. This should be short and descriptive. - * @param body the body text of the alert. This supports markdown formatting. + * @param body the body text of the alert. This should go into detail about the warning + * and what prompted it. * @param owner the owner window of this alert * * @throws NullPointerException if any of the parameters are null @@ -48,22 +31,11 @@ public WarningAlert(String header, String body, Window owner) { initStyle(StageStyle.UNDECORATED); initOwner(owner); - initWebView(); + getDialogPane().setMinHeight(Region.USE_PREF_SIZE); // expand to fit content + getDialogPane().setMinWidth(Region.USE_PREF_SIZE); setHeaderText(header); - webEngine.loadContent(mdProcessor.markdownToHtml(body)); - getDialogPane().setContent(webView); - } - - /** - * Initializes the web view and engine prior to setting its content. - */ - private void initWebView() { - webView.setBlendMode(BlendMode.DARKEN); // make the background transparent - webEngine.setUserStyleSheetLocation( - getClass().getResource("warning_alert.css").toExternalForm() - ); - webView.setPrefSize(WIDTH, HEIGHT); + setContentText(body); } } diff --git a/ui/src/main/resources/edu/wpi/grip/ui/warning_alert.css b/ui/src/main/resources/edu/wpi/grip/ui/warning_alert.css deleted file mode 100644 index 62dfc1b558..0000000000 --- a/ui/src/main/resources/edu/wpi/grip/ui/warning_alert.css +++ /dev/null @@ -1,3 +0,0 @@ -html { - font: 14px Roboto; -} From 4f10cd766a46358c2ff52027dd5b8dc7073c8341 Mon Sep 17 00:00:00 2001 From: Sam Carlberg Date: Fri, 28 Oct 2016 13:22:50 -0400 Subject: [PATCH 5/9] Add IDs to Steps and Sources Put the version as an attribute of the project rather than an XML entry. --- .../wpi/grip/core/AbstractPipelineEntry.java | 148 ++++++++++++++++++ .../java/edu/wpi/grip/core/PipelineEntry.java | 61 ++++++++ .../main/java/edu/wpi/grip/core/Source.java | 36 ++++- .../src/main/java/edu/wpi/grip/core/Step.java | 37 ++--- .../wpi/grip/core/serialization/Project.java | 6 +- .../core/serialization/ProjectConverter.java | 40 +++++ .../core/serialization/SocketConverter.java | 39 +++-- .../core/serialization/SourceConverter.java | 7 + .../core/serialization/StepConverter.java | 13 +- .../wpi/grip/core/sources/CameraSource.java | 22 +-- .../edu/wpi/grip/core/sources/HttpSource.java | 12 +- .../grip/core/sources/ImageFileSource.java | 2 +- .../core/sources/MultiImageFileSource.java | 2 +- .../core/sources/NetworkTableEntrySource.java | 10 +- .../java/edu/wpi/grip/core/MockSource.java | 2 +- .../grip/core/sources/MockNumberSource.java | 6 +- .../input/RangeInputSocketController.java | 8 +- 17 files changed, 379 insertions(+), 72 deletions(-) create mode 100644 core/src/main/java/edu/wpi/grip/core/AbstractPipelineEntry.java create mode 100644 core/src/main/java/edu/wpi/grip/core/PipelineEntry.java create mode 100644 core/src/main/java/edu/wpi/grip/core/serialization/ProjectConverter.java diff --git a/core/src/main/java/edu/wpi/grip/core/AbstractPipelineEntry.java b/core/src/main/java/edu/wpi/grip/core/AbstractPipelineEntry.java new file mode 100644 index 0000000000..f89f2252c0 --- /dev/null +++ b/core/src/main/java/edu/wpi/grip/core/AbstractPipelineEntry.java @@ -0,0 +1,148 @@ +package edu.wpi.grip.core; + +import edu.wpi.grip.core.sockets.InputSocket; +import edu.wpi.grip.core.sockets.OutputSocket; +import edu.wpi.grip.core.sockets.Socket; + +import com.google.common.base.CaseFormat; + +import org.apache.commons.lang.RandomStringUtils; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.NoSuchElementException; +import java.util.Set; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * A partial implementation of {@code PipelineEntry} that implements the removal and ID methods. + */ +public abstract class AbstractPipelineEntry implements PipelineEntry { + + private static final IdPool idPool = IdPool.INSTANCE; + + protected final Object removedLock = new Object(); + private boolean removed = false; + private String id; + + /** + * Creates a unique ID string for the given subclass. + * + * @param entryClass the subclass + * + * @return an ID string for the subclass. + */ + protected static String makeId(Class entryClass) { + String id; + do { + id = CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_HYPHEN, entryClass.getSimpleName()) + + "." + RandomStringUtils.randomAlphanumeric(8); + } while (idPool.checkId(entryClass, id)); + return id; + } + + /** + * Creates a new pipeline entry. + * + * @param id the ID of the new entry. This must be unique for all instances of the concrete class. + * + * @throws NullPointerException if the ID is null + * @throws IllegalArgumentException if the ID is already taken + */ + protected AbstractPipelineEntry(String id) { + idPool.get(getClass()).add(id); + this.id = id; + } + + @Override + public final Socket getSocketByUid(String uid) throws NoSuchElementException { + checkNotNull(uid, "UID"); + for (InputSocket in : getInputSockets()) { + if (in.getUid().equals(uid)) { + return in; + } + } + for (OutputSocket out : getOutputSockets()) { + if (out.getUid().equals(uid)) { + return out; + } + } + throw new NoSuchElementException(uid); + } + + /** + * Cleans up this entry, such as by freeing resources or disabling callbacks. + */ + protected abstract void cleanUp(); + + @Override + public final void setRemoved() { + synchronized (removedLock) { + cleanUp(); + idPool.removeId(this); + removed = true; + } + } + + @Override + public final boolean removed() { + synchronized (removedLock) { + return removed; + } + } + + @Override + public final void setId(String id) throws NullPointerException, IllegalArgumentException { + checkNotNull(id, "The ID cannot be null"); + boolean inDeserialization = Arrays.stream(new Exception().getStackTrace()) + .map(e -> e.getClassName()) + .anyMatch(n -> n.matches(".*(Step|Source)Converter")); + if (!inDeserialization) { + throw new IllegalStateException( + "This method may only be called during project deserialization"); + } + idPool.get(getClass()).add(id); + this.id = id; + } + + @Override + public final String getId() { + return id; + } + + @Override + public String toString() { + return getId(); + } + + /** + * Pool of used IDs. + */ + private static class IdPool extends HashMap, Set> { + private static final IdPool INSTANCE = new IdPool(); + + /** + * Checks if an ID is already used by an instance of a pipeline entry class. + */ + public boolean checkId(Class clazz, String id) { + return get(clazz).contains(id); + } + + /** + * Removes the ID of the given entry. + */ + public void removeId(PipelineEntry e) { + get(e.getClass()).remove(e.getId()); + } + + @Override + @SuppressWarnings("unchecked") + public Set get(Object key) { + return computeIfAbsent((Class) key, k -> new HashSet<>()); + } + + } + +} diff --git a/core/src/main/java/edu/wpi/grip/core/PipelineEntry.java b/core/src/main/java/edu/wpi/grip/core/PipelineEntry.java new file mode 100644 index 0000000000..89091a9e2b --- /dev/null +++ b/core/src/main/java/edu/wpi/grip/core/PipelineEntry.java @@ -0,0 +1,61 @@ +package edu.wpi.grip.core; + +import edu.wpi.grip.core.sockets.InputSocket; +import edu.wpi.grip.core.sockets.OutputSocket; +import edu.wpi.grip.core.sockets.Socket; + +import java.util.List; +import java.util.NoSuchElementException; + +/** + * An entry in the pipeline. + */ +public interface PipelineEntry { + + /** + * Gets a read-only list of the input sockets to this entry. + * This may be empty, but may never be null. + */ + List getInputSockets(); + + /** + * Gets a read-only list of the output sockets from this entry. + * This may be empty, but may never be null. + */ + List getOutputSockets(); + + /** + * Gets the socket with the given ID in this entry. + * + * @param uid the UID of the socket to get + * + * @throws NoSuchElementException if there is no socket with the given UID, + */ + Socket getSocketByUid(String uid) throws NoSuchElementException; + + /** + * Sets this entry as removed from the pipeline. + */ + void setRemoved(); + + /** + * Checks if this entry has been removed from the pipeline. + */ + boolean removed(); + + /** + * Sets the ID of this entry. This should only be used by deserialization. + * + * @param newId the new ID + * + * @throws NullPointerException if the ID is null + * @throws IllegalArgumentException if the ID is already taken + */ + void setId(String newId) throws NullPointerException, IllegalArgumentException; + + /** + * Gets the ID of this entry. This ID should be unique for all instances of the concrete class. + */ + String getId(); + +} diff --git a/core/src/main/java/edu/wpi/grip/core/Source.java b/core/src/main/java/edu/wpi/grip/core/Source.java index 5c4e9087dc..9974f62658 100644 --- a/core/src/main/java/edu/wpi/grip/core/Source.java +++ b/core/src/main/java/edu/wpi/grip/core/Source.java @@ -1,5 +1,7 @@ package edu.wpi.grip.core; +import edu.wpi.grip.core.events.SourceRemovedEvent; +import edu.wpi.grip.core.sockets.InputSocket; import edu.wpi.grip.core.sockets.OutputSocket; import edu.wpi.grip.core.sources.CameraSource; import edu.wpi.grip.core.sources.HttpSource; @@ -9,6 +11,7 @@ import edu.wpi.grip.core.util.ExceptionWitness; import com.google.common.collect.ImmutableList; +import com.google.common.eventbus.Subscribe; import com.google.inject.Inject; import java.io.IOException; @@ -21,17 +24,23 @@ /** * Base class for an input into the pipeline. */ -public abstract class Source { +public abstract class Source extends AbstractPipelineEntry { private static final Logger logger = Logger.getLogger(Source.class.getName()); private final ExceptionWitness exceptionWitness; /** * @param exceptionWitnessFactory Factory to create the exceptionWitness. */ - protected Source(ExceptionWitness.Factory exceptionWitnessFactory) { + protected Source(String id, ExceptionWitness.Factory exceptionWitnessFactory) { + super(id); this.exceptionWitness = exceptionWitnessFactory.create(this); } + @Override + public final List getInputSockets() { + return ImmutableList.of(); + } + /** * This is used by the GUI to distinguish different sources. For example, {@link * edu.wpi.grip.core.sources.ImageFileSource} returns the filename of the image. @@ -45,6 +54,7 @@ protected Source(ExceptionWitness.Factory exceptionWitnessFactory) { * * @return @return An array of {@link OutputSocket}s for the outputs that the source produces. */ + @Override public final ImmutableList getOutputSockets() { final List outputSockets = createOutputSockets(); for (OutputSocket socket : outputSockets) { @@ -99,6 +109,28 @@ public final void initializeSafely() { } } + /** + * Sets this source as removed if an event is posted to clean it up. + * Subclasses that need additional functionality should subscribe to the event bus. + */ + @Subscribe + public void onSourceRemoved(SourceRemovedEvent event) { + if (event.getSource() == this) { + setRemoved(); + } + } + + /** + * Cleans up this source when it gets removed from the pipeline. The default implementation is a + * NOP, but subclasses should override this method if they need to free any resources or + * de-register callbacks. + */ + @Override + @SuppressWarnings("PMD") + protected void cleanUp() { + // Default to NOP + } + public interface SourceFactory { Source create(Class type, Properties properties) throws IOException; } diff --git a/core/src/main/java/edu/wpi/grip/core/Step.java b/core/src/main/java/edu/wpi/grip/core/Step.java index dde05e335c..20f17dc235 100644 --- a/core/src/main/java/edu/wpi/grip/core/Step.java +++ b/core/src/main/java/edu/wpi/grip/core/Step.java @@ -22,7 +22,7 @@ * sockets, and it runs the operation whenever one of the input sockets changes. */ @XStreamAlias(value = "grip:Step") -public class Step { +public class Step extends AbstractPipelineEntry { private static final Logger logger = Logger.getLogger(Step.class.getName()); private static final String MISSING_SOCKET_MESSAGE_END = " must have a value to run this step."; @@ -32,8 +32,6 @@ public class Step { private final OperationDescription description; private final List inputSockets; private final List outputSockets; - private final Object removedLock = new Object(); - private boolean removed = false; /** * @param operation The operation that is performed at this step. @@ -47,6 +45,7 @@ public class Step { List inputSockets, List outputSockets, ExceptionWitness.Factory exceptionWitnessFactory) { + super(makeId(Step.class)); this.operation = operation; this.description = description; this.inputSockets = inputSockets; @@ -64,6 +63,7 @@ public OperationDescription getOperationDescription() { /** * @return An array of {@link InputSocket InputSockets} that hold the inputs to this step. */ + @Override public ImmutableList getInputSockets() { return ImmutableList.copyOf(inputSockets); } @@ -71,6 +71,7 @@ public ImmutableList getInputSockets() { /** * @return A list of {@link OutputSocket OutputSockets} that hold the outputs of this step. */ + @Override public ImmutableList getOutputSockets() { return ImmutableList.copyOf(outputSockets); } @@ -112,10 +113,9 @@ protected final void runPerformIfPossible() { try { // We need to ensure that if perform disabled is switching states that we don't run the - // perform method - // while that is happening. + // perform method while that is happening. synchronized (removedLock) { - if (!removed) { + if (!removed()) { this.operation.perform(); } } @@ -132,27 +132,9 @@ protected final void runPerformIfPossible() { witness.clearException(); } - /** - * Sets this step as having been removed. - */ - public final void setRemoved() { - // We need to wait for the perform method to complete before returning. - // if we don't wait then the perform method could end up being run concurrently with the - // perform methods execution - synchronized (removedLock) { - removed = true; - operation.cleanUp(); - } - } - - /** - * Allows checks to see if this step has had its perform method disabled. If this value ever - * returns false it will never return true again. - * - * @return true if runPerformIfPossible can run successfully - */ - protected boolean removed() { - return removed; + @Override + protected void cleanUp() { + operation.cleanUp(); } @Singleton @@ -166,6 +148,7 @@ public Factory(ExceptionWitness.Factory exceptionWitnessFactory) { /** * @param operationData The operation data to use to construct the step. + * * @return The constructed Step. */ public Step create(OperationMetaData operationData) { diff --git a/core/src/main/java/edu/wpi/grip/core/serialization/Project.java b/core/src/main/java/edu/wpi/grip/core/serialization/Project.java index 0b24feb084..e505e3653d 100644 --- a/core/src/main/java/edu/wpi/grip/core/serialization/Project.java +++ b/core/src/main/java/edu/wpi/grip/core/serialization/Project.java @@ -50,7 +50,8 @@ public class Project { private final ObservableBoolean saveIsDirty = new ObservableBoolean(); @Inject - public void initialize(StepConverter stepConverter, + public void initialize(ProjectConverter projectConverter, + StepConverter stepConverter, SourceConverter sourceConverter, SocketConverter socketConverter, ConnectionConverter connectionConverter, @@ -58,6 +59,7 @@ public void initialize(StepConverter stepConverter, VersionConverter versionConverter) { model = new ProjectModel(pipeline, VersionManager.CURRENT_VERSION); xstream.setMode(XStream.NO_REFERENCES); + xstream.registerConverter(projectConverter); xstream.registerConverter(stepConverter); xstream.registerConverter(sourceConverter); xstream.registerConverter(socketConverter); @@ -142,7 +144,7 @@ void open(Reader reader) throws InvalidSaveException { } } catch (ConversionException e) { // Incompatible save, or a bug with de/serialization - throw new InvalidSaveException("Incompatible operations in save file", e); + throw new InvalidSaveException("There are incompatible operations in the pipeline", e); } catch (StreamException e) { // Invalid XML throw new InvalidSaveException("Invalid XML", e); diff --git a/core/src/main/java/edu/wpi/grip/core/serialization/ProjectConverter.java b/core/src/main/java/edu/wpi/grip/core/serialization/ProjectConverter.java new file mode 100644 index 0000000000..0eeabc8993 --- /dev/null +++ b/core/src/main/java/edu/wpi/grip/core/serialization/ProjectConverter.java @@ -0,0 +1,40 @@ +package edu.wpi.grip.core.serialization; + +import edu.wpi.grip.core.Pipeline; +import edu.wpi.grip.core.VersionManager; + +import com.github.zafarkhaja.semver.Version; +import com.thoughtworks.xstream.converters.Converter; +import com.thoughtworks.xstream.converters.MarshallingContext; +import com.thoughtworks.xstream.converters.UnmarshallingContext; +import com.thoughtworks.xstream.io.HierarchicalStreamReader; +import com.thoughtworks.xstream.io.HierarchicalStreamWriter; + +public class ProjectConverter implements Converter { + + private static final String VERSION_ATTRIBUTE = "version"; + + @Override + public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) { + ProjectModel model = (ProjectModel) source; + writer.addAttribute(VERSION_ATTRIBUTE, model.getVersion().toString()); + writer.startNode("grip:Pipeline"); + context.convertAnother(model.getPipeline()); + writer.endNode(); + } + + @Override + public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) { + String v = reader.getAttribute(VERSION_ATTRIBUTE); + Version version = v == null ? VersionManager.LAST_UNVERSIONED_RELEASE : Version.valueOf(v); + reader.moveDown(); + Pipeline pipeline = (Pipeline) context.convertAnother(null, Pipeline.class); + reader.moveUp(); + return new ProjectModel(pipeline, version); + } + + @Override + public boolean canConvert(Class type) { + return type.equals(ProjectModel.class); + } +} diff --git a/core/src/main/java/edu/wpi/grip/core/serialization/SocketConverter.java b/core/src/main/java/edu/wpi/grip/core/serialization/SocketConverter.java index 2c6b4d6226..fdad0ccdf3 100644 --- a/core/src/main/java/edu/wpi/grip/core/serialization/SocketConverter.java +++ b/core/src/main/java/edu/wpi/grip/core/serialization/SocketConverter.java @@ -51,13 +51,12 @@ public void marshal(Object obj, HierarchicalStreamWriter writer, MarshallingCont // Save the location of the socket in the pipeline. socket.getStep().ifPresent(step -> { - writer.addAttribute(STEP_ATTRIBUTE, String.valueOf(pipeline.getSteps().indexOf(step))); + writer.addAttribute(STEP_ATTRIBUTE, step.getId()); writer.addAttribute(SOCKET_ATTRIBUTE, socket.getUid()); }); socket.getSource().ifPresent(source -> { - writer.addAttribute(SOURCE_ATTRIBUTE, String.valueOf(pipeline.getSources() - .indexOf(source))); + writer.addAttribute(SOURCE_ATTRIBUTE, source.getId()); writer.addAttribute(SOCKET_ATTRIBUTE, socket.getUid()); }); @@ -110,16 +109,12 @@ public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext co // (such as connections) can reference sockets in the pipeline. Socket socket; if (reader.getAttribute(STEP_ATTRIBUTE) != null) { - final int stepIndex = Integer.parseInt(reader.getAttribute(STEP_ATTRIBUTE)); - - final Step step = pipeline.getSteps().get(stepIndex); + final Step step = stepFor(reader.getAttribute(STEP_ATTRIBUTE)); socket = (direction == Socket.Direction.INPUT) ? socketFor(step.getInputSockets(), reader.getAttribute(SOCKET_ATTRIBUTE)) : socketFor(step.getOutputSockets(), reader.getAttribute(SOCKET_ATTRIBUTE)); } else if (reader.getAttribute(SOURCE_ATTRIBUTE) != null) { - final int sourceIndex = Integer.parseInt(reader.getAttribute(SOURCE_ATTRIBUTE)); - - final Source source = pipeline.getSources().get(sourceIndex); + final Source source = sourceFor(reader.getAttribute(SOURCE_ATTRIBUTE)); socket = socketFor(source.getOutputSockets(), reader.getAttribute(SOCKET_ATTRIBUTE)); } else { throw new ConversionException("Sockets must have either a step or source attribute"); @@ -169,6 +164,32 @@ private Class getDeserializedType(Socket socket) { + socketType); } + private Source sourceFor(String attributeValue) { + if (attributeValue.matches("\\d+")) { + // All numbers, it's an index + return pipeline.getSources().get(Integer.parseInt(attributeValue)); + } else { + // Alphanumeric + return pipeline.getSources().stream() + .filter(s -> s.getId().equals(attributeValue)) + .findAny() + .get(); + } + } + + private Step stepFor(String attributeValue) { + if (attributeValue.matches("\\d+")) { + // All numbers, it's an index + return pipeline.getSteps().get(Integer.parseInt(attributeValue)); + } else { + // Alphanumeric + return pipeline.getSteps().stream() + .filter(s -> s.getId().equals(attributeValue)) + .findAny() + .get(); + } + } + private S socketFor(List sockets, String attributeValue) { if (attributeValue.matches("\\d+")) { // All numbers, assume it's an index diff --git a/core/src/main/java/edu/wpi/grip/core/serialization/SourceConverter.java b/core/src/main/java/edu/wpi/grip/core/serialization/SourceConverter.java index 4db9491694..9837505e9e 100644 --- a/core/src/main/java/edu/wpi/grip/core/serialization/SourceConverter.java +++ b/core/src/main/java/edu/wpi/grip/core/serialization/SourceConverter.java @@ -27,6 +27,8 @@ */ public class SourceConverter implements Converter { + private static final String ID_ATTRIBUTE = "id"; + @Inject private EventBus eventBus; @Inject @@ -36,6 +38,7 @@ public class SourceConverter implements Converter { @Override public void marshal(Object obj, HierarchicalStreamWriter writer, MarshallingContext context) { + writer.addAttribute(ID_ATTRIBUTE, ((Source) obj).getId()); context.convertAnother(((Source) obj).getProperties()); } @@ -45,6 +48,7 @@ public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext co try { final Class sourceClass = (Class) project.xstream.getMapper() .realClass(reader.getNodeName()); + final String id = reader.getAttribute(ID_ATTRIBUTE); final Properties properties = (Properties) context.convertAnother(null, Properties.class); // Although sources may block briefly upon creation, we intentionally do this in one thread @@ -53,6 +57,9 @@ public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext co // sources that // are in the process of loading. final Source source = sourceFactory.create(sourceClass, properties); + if (id != null) { + source.setId(id); + } // Instead of returning the source, post it to the event bus so both the core and GUI // classes know it diff --git a/core/src/main/java/edu/wpi/grip/core/serialization/StepConverter.java b/core/src/main/java/edu/wpi/grip/core/serialization/StepConverter.java index 885e034891..9c807c5ce3 100644 --- a/core/src/main/java/edu/wpi/grip/core/serialization/StepConverter.java +++ b/core/src/main/java/edu/wpi/grip/core/serialization/StepConverter.java @@ -28,6 +28,7 @@ public class StepConverter implements Converter { private static final String NAME_ATTRIBUTE = "name"; + private static final String ID_ATTRIBUTE = "id"; @Inject private EventBus eventBus; @@ -43,6 +44,7 @@ public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingC final Step step = ((Step) source); writer.addAttribute(NAME_ATTRIBUTE, step.getOperationDescription().name()); + writer.addAttribute(ID_ATTRIBUTE, step.getId()); // Also save any sockets in the step for (InputSocket socket : step.getInputSockets()) { @@ -64,11 +66,12 @@ public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext co throw new ConversionException("Unknown operation: " + operationName); } - // Instead of simply returning the step and having XStream insert it into the pipeline using - // reflection, send a - // StepAddedEvent. This allows other interested classes (such as PipelineView) to also know - // when steps are added. - pipeline.addStep(stepFactory.create(operationMetaData.get())); + final String id = reader.getAttribute(ID_ATTRIBUTE); + Step step = stepFactory.create(operationMetaData.get()); + if (id != null) { + step.setId(id); + } + pipeline.addStep(step); while (reader.hasMoreChildren()) { context.convertAnother(this, Socket.class); diff --git a/core/src/main/java/edu/wpi/grip/core/sources/CameraSource.java b/core/src/main/java/edu/wpi/grip/core/sources/CameraSource.java index 9936ec1ecb..b437dbfc2e 100644 --- a/core/src/main/java/edu/wpi/grip/core/sources/CameraSource.java +++ b/core/src/main/java/edu/wpi/grip/core/sources/CameraSource.java @@ -133,7 +133,7 @@ public class CameraSource extends Source implements RestartableService { final FrameGrabberFactory grabberFactory, final ExceptionWitness.Factory exceptionWitnessFactory, @Assisted final Properties properties) throws MalformedURLException { - super(exceptionWitnessFactory); + super(makeId(CameraSource.class), exceptionWitnessFactory); this.eventBus = eventBus; this.frameOutputSocket = outputSocketFactory.create(imageOutputHint, "image-output"); this.frameRateOutputSocket = outputSocketFactory.create(frameRateOutputHint, "fps-output"); @@ -329,22 +329,26 @@ public State state() { } @Subscribe - public void onSourceRemovedEvent(SourceRemovedEvent event) throws InterruptedException, - TimeoutException, IOException { + public void onSourceRemoved(SourceRemovedEvent event) { if (event.getSource() == this) { try { - // Stop the camera service and wait for it to terminate. - // If we just use stopAsync(), the camera service won't always have terminated by the time - // a new camera source is added. For webcam sources, this means that the video stream - // won't be freed and new sources won't be able to connect to the webcam until the - // application is closed. - this.stopAndAwait(); + setRemoved(); } finally { this.eventBus.unregister(this); } } } + @Override + protected void cleanUp() { + // Stop the camera service and wait for it to terminate. + // If we just use stopAsync(), the camera service won't always have terminated by the time + // a new camera source is added. For webcam sources, this means that the video stream + // won't be freed and new sources won't be able to connect to the webcam until the + // application is closed. + stopAndAwait(); + } + public interface Factory { CameraSource create(int deviceNumber) throws IOException; diff --git a/core/src/main/java/edu/wpi/grip/core/sources/HttpSource.java b/core/src/main/java/edu/wpi/grip/core/sources/HttpSource.java index a78f65cfc6..3ecfab72f2 100644 --- a/core/src/main/java/edu/wpi/grip/core/sources/HttpSource.java +++ b/core/src/main/java/edu/wpi/grip/core/sources/HttpSource.java @@ -3,7 +3,6 @@ import edu.wpi.grip.core.Source; import edu.wpi.grip.core.events.SourceHasPendingUpdateEvent; -import edu.wpi.grip.core.events.SourceRemovedEvent; import edu.wpi.grip.core.http.ContextStore; import edu.wpi.grip.core.http.GripServer; import edu.wpi.grip.core.sockets.OutputSocket; @@ -13,7 +12,6 @@ import com.google.common.collect.ImmutableList; import com.google.common.eventbus.EventBus; -import com.google.common.eventbus.Subscribe; import com.google.inject.assistedinject.Assisted; import com.google.inject.assistedinject.AssistedInject; import com.thoughtworks.xstream.annotations.XStreamAlias; @@ -87,7 +85,7 @@ public interface Factory { GripServer server, ContextStore store, @Assisted String path) { - super(exceptionWitnessFactory); + super(makeId(HttpSource.class), exceptionWitnessFactory); this.path = path; this.imageHandler = handlers.computeIfAbsent(path, p -> new HttpImageHandler(store, p)); this.imageOutput = osf.create(outputHint, "image-output"); @@ -138,11 +136,9 @@ public void initialize() { imageHandler.getImage().ifPresent(this::setImage); } - @Subscribe - public void onSourceRemovedEvent(SourceRemovedEvent event) { - if (event.getSource() == this) { - imageHandler.removeCallback(callback); - } + @Override + protected void cleanUp() { + imageHandler.removeCallback(callback); } } diff --git a/core/src/main/java/edu/wpi/grip/core/sources/ImageFileSource.java b/core/src/main/java/edu/wpi/grip/core/sources/ImageFileSource.java index 397f84fa35..f256a58dc6 100644 --- a/core/src/main/java/edu/wpi/grip/core/sources/ImageFileSource.java +++ b/core/src/main/java/edu/wpi/grip/core/sources/ImageFileSource.java @@ -69,7 +69,7 @@ private ImageFileSource( final OutputSocket.Factory outputSocketFactory, final ExceptionWitness.Factory exceptionWitnessFactory, final String path) { - super(exceptionWitnessFactory); + super(makeId(ImageFileSource.class), exceptionWitnessFactory); this.path = checkNotNull(path, "Path can not be null"); this.name = Files.getNameWithoutExtension(this.path); this.outputSocket = outputSocketFactory.create(imageOutputHint, "image-output"); diff --git a/core/src/main/java/edu/wpi/grip/core/sources/MultiImageFileSource.java b/core/src/main/java/edu/wpi/grip/core/sources/MultiImageFileSource.java index d643dcb1f9..abc46f065a 100644 --- a/core/src/main/java/edu/wpi/grip/core/sources/MultiImageFileSource.java +++ b/core/src/main/java/edu/wpi/grip/core/sources/MultiImageFileSource.java @@ -104,7 +104,7 @@ private MultiImageFileSource( final ExceptionWitness.Factory exceptionWitnessFactory, final String[] paths, final int index) { - super(exceptionWitnessFactory); + super(makeId(MultiImageFileSource.class), exceptionWitnessFactory); this.eventBus = eventBus; this.outputSocket = outputSocketFactory.create(imageOutputHint, "image-output"); this.index = new AtomicInteger(checkElementIndex(index, paths.length, "File List Index")); diff --git a/core/src/main/java/edu/wpi/grip/core/sources/NetworkTableEntrySource.java b/core/src/main/java/edu/wpi/grip/core/sources/NetworkTableEntrySource.java index f7b4863599..da0c868d09 100644 --- a/core/src/main/java/edu/wpi/grip/core/sources/NetworkTableEntrySource.java +++ b/core/src/main/java/edu/wpi/grip/core/sources/NetworkTableEntrySource.java @@ -102,7 +102,7 @@ public String toProperty() { @Named("ntManager") MapNetworkReceiverFactory networkReceiverFactory, @Assisted String path, @Assisted Types type) { - super(exceptionWitnessFactory); + super(makeId(NetworkTableEntrySource.class), exceptionWitnessFactory); this.eventBus = eventBus; this.path = path; this.type = type; @@ -150,7 +150,13 @@ public void initialize() { @Subscribe public void onSourceRemovedEvent(SourceRemovedEvent event) { if (event.getSource() == this) { - networkReceiver.close(); + setRemoved(); } } + + @Override + protected void cleanUp() { + networkReceiver.close(); + } + } diff --git a/core/src/test/java/edu/wpi/grip/core/MockSource.java b/core/src/test/java/edu/wpi/grip/core/MockSource.java index cdd48b77ec..fddbb09a16 100644 --- a/core/src/test/java/edu/wpi/grip/core/MockSource.java +++ b/core/src/test/java/edu/wpi/grip/core/MockSource.java @@ -12,7 +12,7 @@ public class MockSource extends Source { protected MockSource() { - super(origin -> null); + super(makeId(MockSource.class), origin -> null); } @Override diff --git a/core/src/test/java/edu/wpi/grip/core/sources/MockNumberSource.java b/core/src/test/java/edu/wpi/grip/core/sources/MockNumberSource.java index 98f2790a09..daa2cf5d61 100644 --- a/core/src/test/java/edu/wpi/grip/core/sources/MockNumberSource.java +++ b/core/src/test/java/edu/wpi/grip/core/sources/MockNumberSource.java @@ -15,17 +15,17 @@ import java.util.Properties; public class MockNumberSource extends Source { - + private static int numberOf = 0; private final int id; private final OutputSocket outputSocket; - private final SocketHint outputSocketHint = + private final SocketHint outputSocketHint = SocketHints.Outputs.createNumberSocketHint("Num", Math.PI); @SuppressFBWarnings(value = "ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD", justification = "Do not need to synchronize inside of a constructor") public MockNumberSource(Factory exceptionWitnessFactory, double value, OutputSocket.Factory osf) { - super(exceptionWitnessFactory); + super(makeId(MockNumberSource.class), exceptionWitnessFactory); id = numberOf++; outputSocket = osf.create(outputSocketHint); outputSocket.setValue(new Double(value)); diff --git a/ui/src/main/java/edu/wpi/grip/ui/pipeline/input/RangeInputSocketController.java b/ui/src/main/java/edu/wpi/grip/ui/pipeline/input/RangeInputSocketController.java index f1cfe9a76c..2c9d897db2 100644 --- a/ui/src/main/java/edu/wpi/grip/ui/pipeline/input/RangeInputSocketController.java +++ b/ui/src/main/java/edu/wpi/grip/ui/pipeline/input/RangeInputSocketController.java @@ -12,6 +12,7 @@ import java.util.List; +import javafx.application.Platform; import javafx.fxml.FXML; import javafx.geometry.Pos; import javafx.scene.control.Label; @@ -105,8 +106,11 @@ private String getLowHighLabelText() { @Subscribe public void updateSliderValue(SocketChangedEvent event) { if (event.isRegarding(this.getSocket())) { - this.slider.setLowValue(this.getSocket().getValue().get().get(0).doubleValue()); - this.slider.setHighValue(this.getSocket().getValue().get().get(1).doubleValue()); + // There's no guarantee that the event was fired from the FX thread + Platform.runLater(() -> { + this.slider.setLowValue(this.getSocket().getValue().get().get(0).doubleValue()); + this.slider.setHighValue(this.getSocket().getValue().get().get(1).doubleValue()); + }); } } From a04f0121c259fe4963d2dd04512f379c52fc1cc7 Mon Sep 17 00:00:00 2001 From: Sam Carlberg Date: Fri, 28 Oct 2016 14:46:30 -0400 Subject: [PATCH 6/9] Add tests --- .../wpi/grip/core/AbstractPipelineEntry.java | 7 +- .../wpi/grip/core/serialization/Project.java | 2 +- .../grip/core/AbstractPipelineEntryTest.java | 95 +++++++++++++++++++ .../grip/core/serialization/ProjectTest.java | 34 +++++++ .../grip/core/sockets/MockInputSocket.java | 4 +- .../grip/core/sockets/MockOutputSocket.java | 6 +- 6 files changed, 142 insertions(+), 6 deletions(-) create mode 100644 core/src/test/java/edu/wpi/grip/core/AbstractPipelineEntryTest.java diff --git a/core/src/main/java/edu/wpi/grip/core/AbstractPipelineEntry.java b/core/src/main/java/edu/wpi/grip/core/AbstractPipelineEntry.java index f89f2252c0..0d7018db43 100644 --- a/core/src/main/java/edu/wpi/grip/core/AbstractPipelineEntry.java +++ b/core/src/main/java/edu/wpi/grip/core/AbstractPipelineEntry.java @@ -4,6 +4,7 @@ import edu.wpi.grip.core.sockets.OutputSocket; import edu.wpi.grip.core.sockets.Socket; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.CaseFormat; import org.apache.commons.lang.RandomStringUtils; @@ -21,7 +22,8 @@ */ public abstract class AbstractPipelineEntry implements PipelineEntry { - private static final IdPool idPool = IdPool.INSTANCE; + @VisibleForTesting + static final IdPool idPool = IdPool.INSTANCE; protected final Object removedLock = new Object(); private boolean removed = false; @@ -120,7 +122,8 @@ public String toString() { /** * Pool of used IDs. */ - private static class IdPool extends HashMap, Set> { + @VisibleForTesting + static class IdPool extends HashMap, Set> { private static final IdPool INSTANCE = new IdPool(); /** diff --git a/core/src/main/java/edu/wpi/grip/core/serialization/Project.java b/core/src/main/java/edu/wpi/grip/core/serialization/Project.java index e505e3653d..aefb0c81b1 100644 --- a/core/src/main/java/edu/wpi/grip/core/serialization/Project.java +++ b/core/src/main/java/edu/wpi/grip/core/serialization/Project.java @@ -144,7 +144,7 @@ void open(Reader reader) throws InvalidSaveException { } } catch (ConversionException e) { // Incompatible save, or a bug with de/serialization - throw new InvalidSaveException("There are incompatible operations in the pipeline", e); + throw new InvalidSaveException("There are incompatible sources or steps in the pipeline", e); } catch (StreamException e) { // Invalid XML throw new InvalidSaveException("Invalid XML", e); diff --git a/core/src/test/java/edu/wpi/grip/core/AbstractPipelineEntryTest.java b/core/src/test/java/edu/wpi/grip/core/AbstractPipelineEntryTest.java new file mode 100644 index 0000000000..a563eb1bfa --- /dev/null +++ b/core/src/test/java/edu/wpi/grip/core/AbstractPipelineEntryTest.java @@ -0,0 +1,95 @@ +package edu.wpi.grip.core; + +import edu.wpi.grip.core.sockets.InputSocket; +import edu.wpi.grip.core.sockets.MockInputSocket; +import edu.wpi.grip.core.sockets.MockOutputSocket; +import edu.wpi.grip.core.sockets.OutputSocket; + +import com.google.common.collect.ImmutableList; + +import org.junit.Test; + +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +import static edu.wpi.grip.core.AbstractPipelineEntry.idPool; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class AbstractPipelineEntryTest { + + private static class TestEntryImpl extends AbstractPipelineEntry { + + TestEntryImpl() { + super(makeId(TestEntryImpl.class)); + } + + @Override + public List getInputSockets() { + return ImmutableList.of(); + } + + @Override + public List getOutputSockets() { + return ImmutableList.of(); + } + + @Override + protected void cleanUp() { + // NOP + } + + } + + @Test + public void testMakeId() { + TestEntryImpl t = new TestEntryImpl(); + String id = t.getId(); + assertTrue("ID was not added to the pool", idPool.checkId(TestEntryImpl.class, id)); + t.setRemoved(); + assertFalse("ID was not removed from the pool", idPool.checkId(TestEntryImpl.class, id)); + } + + @Test(expected = IllegalStateException.class) + public void testSetId() { + TestEntryImpl t = new TestEntryImpl(); + t.setId("foo"); + fail("setId() should fail if called outside deserialization"); + } + + @Test + public void testSetRemoved() { + AtomicBoolean didCleanUp = new AtomicBoolean(false); + TestEntryImpl t = new TestEntryImpl() { + @Override + protected void cleanUp() { + didCleanUp.set(true); + } + }; + t.setRemoved(); + assertTrue("cleanUp was not called", didCleanUp.get()); + assertTrue("Entry was not removed", t.removed()); + } + + @Test + public void testGetSocketById() { + final InputSocket in = new MockInputSocket("foo"); + final OutputSocket out = new MockOutputSocket("bar"); + TestEntryImpl t = new TestEntryImpl() { + @Override + public List getInputSockets() { + return ImmutableList.of(in); + } + + @Override + public List getOutputSockets() { + return ImmutableList.of(out); + } + }; + assertEquals("Did not get correct socket", in, t.getSocketByUid(in.getUid())); + assertEquals("Did not get correct socket", out, t.getSocketByUid(out.getUid())); + } + +} diff --git a/core/src/test/java/edu/wpi/grip/core/serialization/ProjectTest.java b/core/src/test/java/edu/wpi/grip/core/serialization/ProjectTest.java index b119685574..55cf0bdbcf 100644 --- a/core/src/test/java/edu/wpi/grip/core/serialization/ProjectTest.java +++ b/core/src/test/java/edu/wpi/grip/core/serialization/ProjectTest.java @@ -12,6 +12,7 @@ import edu.wpi.grip.core.events.OperationAddedEvent; import edu.wpi.grip.core.events.ProjectSettingsChangedEvent; import edu.wpi.grip.core.events.SourceAddedEvent; +import edu.wpi.grip.core.exception.InvalidSaveException; import edu.wpi.grip.core.operations.PythonScriptFile; import edu.wpi.grip.core.operations.network.MockGripNetworkModule; import edu.wpi.grip.core.settings.ProjectSettings; @@ -130,6 +131,39 @@ public void testSerializeEmptyPipeline() throws Exception { 0, pipeline.getConnections().size()); } + @Test + public void testLoadInvalidXML() { + try { + project.open(""); + } catch (InvalidSaveException e) { + if (!e.getMessage().equals("Invalid XML")) { + // Not the exception we were expecting, throw it + throw e; + } + } + } + + @Test + public void testLoadIncompatibleEntries() { + String xml = "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " " + + ""; + try { + project.open(xml); + } catch (InvalidSaveException e) { + if (!e.getMessage().equals("There are incompatible sources or steps in the pipeline")) { + throw e; + } + } + } + @Test public void testSerializePipelineWithSteps() throws Exception { pipeline.addStep(stepFactory.create(additionOperation)); diff --git a/core/src/test/java/edu/wpi/grip/core/sockets/MockInputSocket.java b/core/src/test/java/edu/wpi/grip/core/sockets/MockInputSocket.java index 3855983097..f69c9abfc1 100644 --- a/core/src/test/java/edu/wpi/grip/core/sockets/MockInputSocket.java +++ b/core/src/test/java/edu/wpi/grip/core/sockets/MockInputSocket.java @@ -5,6 +5,8 @@ public class MockInputSocket extends InputSocketImpl { public MockInputSocket(String name) { - super(new EventBus(), SocketHints.Outputs.createBooleanSocketHint(name, false), "mock"); + super(new EventBus(), + SocketHints.Outputs.createBooleanSocketHint(name, false), + "mock-input-" + name); } } diff --git a/core/src/test/java/edu/wpi/grip/core/sockets/MockOutputSocket.java b/core/src/test/java/edu/wpi/grip/core/sockets/MockOutputSocket.java index 72a59780d7..103063b9df 100644 --- a/core/src/test/java/edu/wpi/grip/core/sockets/MockOutputSocket.java +++ b/core/src/test/java/edu/wpi/grip/core/sockets/MockOutputSocket.java @@ -3,7 +3,9 @@ import com.google.common.eventbus.EventBus; public class MockOutputSocket extends OutputSocketImpl { - public MockOutputSocket(String socketName) { - super(new EventBus(), SocketHints.Outputs.createBooleanSocketHint(socketName, false), "mock"); + public MockOutputSocket(String name) { + super(new EventBus(), + SocketHints.Outputs.createBooleanSocketHint(name, false), + "mock-output-" + name); } } From 676fa485786b35367336c2ad78214fec419b0a21 Mon Sep 17 00:00:00 2001 From: Sam Carlberg Date: Thu, 16 Mar 2017 22:24:12 -0400 Subject: [PATCH 7/9] Fix ClassifierSource --- .../main/java/edu/wpi/grip/core/sources/ClassifierSource.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/edu/wpi/grip/core/sources/ClassifierSource.java b/core/src/main/java/edu/wpi/grip/core/sources/ClassifierSource.java index 78d8c1805a..7d6488c402 100644 --- a/core/src/main/java/edu/wpi/grip/core/sources/ClassifierSource.java +++ b/core/src/main/java/edu/wpi/grip/core/sources/ClassifierSource.java @@ -39,7 +39,7 @@ protected ClassifierSource(EventBus eventBus, OutputSocket.Factory osf, ExceptionWitness.Factory exceptionWitnessFactory, @Assisted String filePath) { - super(exceptionWitnessFactory); + super(makeId(ClassifierSource.class), exceptionWitnessFactory); this.classifierSocket = osf.create(classifierHint); this.filePath = filePath; } From 46d38866c4b33050a88d1f206a0ff2c2adf83ca6 Mon Sep 17 00:00:00 2001 From: Sam Carlberg Date: Thu, 16 Mar 2017 22:49:51 -0400 Subject: [PATCH 8/9] Make CameraSource use cleanUp() to do cleanup. Appease checkstyle --- .../wpi/grip/core/serialization/Project.java | 2 +- .../wpi/grip/core/sources/CameraSource.java | 65 +++++++------------ .../input/RangeInputSocketController.java | 1 - 3 files changed, 26 insertions(+), 42 deletions(-) diff --git a/core/src/main/java/edu/wpi/grip/core/serialization/Project.java b/core/src/main/java/edu/wpi/grip/core/serialization/Project.java index 47e11685fa..54bcebcfe7 100644 --- a/core/src/main/java/edu/wpi/grip/core/serialization/Project.java +++ b/core/src/main/java/edu/wpi/grip/core/serialization/Project.java @@ -167,7 +167,7 @@ void open(Reader reader) throws InvalidSaveException { * @param file the file to save to * * @return true if the project was successfully saved to the given file, or false if the file - * could not be written to + * could not be written to */ public boolean trySave(File file) { try { diff --git a/core/src/main/java/edu/wpi/grip/core/sources/CameraSource.java b/core/src/main/java/edu/wpi/grip/core/sources/CameraSource.java index 26b7e079b1..960a06f5d4 100644 --- a/core/src/main/java/edu/wpi/grip/core/sources/CameraSource.java +++ b/core/src/main/java/edu/wpi/grip/core/sources/CameraSource.java @@ -3,7 +3,6 @@ import edu.wpi.grip.core.Source; import edu.wpi.grip.core.events.SourceHasPendingUpdateEvent; -import edu.wpi.grip.core.events.SourceRemovedEvent; import edu.wpi.grip.core.sockets.OutputSocket; import edu.wpi.grip.core.sockets.SocketHint; import edu.wpi.grip.core.sockets.SocketHints; @@ -16,7 +15,6 @@ import com.google.common.base.StandardSystemProperty; import com.google.common.collect.ImmutableList; import com.google.common.eventbus.EventBus; -import com.google.common.eventbus.Subscribe; import com.google.common.util.concurrent.MoreExecutors; import com.google.inject.assistedinject.Assisted; import com.google.inject.assistedinject.AssistedInject; @@ -342,49 +340,36 @@ public State state() { return cameraService.state(); } - @Subscribe - public void onSourceRemoved(SourceRemovedEvent event) { - if (event.getSource() == this) { - try { - // Stop the camera service and wait for it to terminate. - // If we just use stopAsync(), the camera service won't always have terminated by the time - // a new camera source is added. For webcam sources, this means that the video stream - // won't be freed and new sources won't be able to connect to the webcam until the - // application is closed. - if (StandardSystemProperty.OS_NAME.value().toLowerCase().contains("mac")) { - // Workaround for #716. This affects webcams as well as IP camera sources. - // Use only stopAsync() to avoid blocking. Since we have no way of knowing when - // the capture has actually been freed, we use a dumb delay to try to make sure it's - // freed before returning. THIS IS NOT A GOOD SOLUTION. But it's the best one we have - // until the bug is fixed. - stopAsync(); - try { - // Wait a bit to try to make sure the capture is actually freed before returning - Thread.sleep(100); - } catch (InterruptedException ignore) { - // We did our best. Hopefully, the webcam has been freed at this point. - Thread.currentThread().interrupt(); - } - } else { - this.stopAndAwait(); + @Override + protected void cleanUp() { + try { + // Stop the camera service and wait for it to terminate. + // If we just use stopAsync(), the camera service won't always have terminated by the time + // a new camera source is added. For webcam sources, this means that the video stream + // won't be freed and new sources won't be able to connect to the webcam until the + // application is closed. + if (StandardSystemProperty.OS_NAME.value().toLowerCase().contains("mac")) { + // Workaround for #716. This affects webcams as well as IP camera sources. + // Use only stopAsync() to avoid blocking. Since we have no way of knowing when + // the capture has actually been freed, we use a dumb delay to try to make sure it's + // freed before returning. THIS IS NOT A GOOD SOLUTION. But it's the best one we have + // until the bug is fixed. + stopAsync(); + try { + // Wait a bit to try to make sure the capture is actually freed before returning + Thread.sleep(100); + } catch (InterruptedException ignore) { + // We did our best. Hopefully, the webcam has been freed at this point. + Thread.currentThread().interrupt(); } - setRemoved(); - } finally { - this.eventBus.unregister(this); + } else { + this.stopAndAwait(); } + } finally { + this.eventBus.unregister(this); } } - @Override - protected void cleanUp() { - // Stop the camera service and wait for it to terminate. - // If we just use stopAsync(), the camera service won't always have terminated by the time - // a new camera source is added. For webcam sources, this means that the video stream - // won't be freed and new sources won't be able to connect to the webcam until the - // application is closed. - stopAndAwait(); - } - public interface Factory { CameraSource create(int deviceNumber) throws IOException; diff --git a/ui/src/main/java/edu/wpi/grip/ui/pipeline/input/RangeInputSocketController.java b/ui/src/main/java/edu/wpi/grip/ui/pipeline/input/RangeInputSocketController.java index 179972772d..5e82665b05 100644 --- a/ui/src/main/java/edu/wpi/grip/ui/pipeline/input/RangeInputSocketController.java +++ b/ui/src/main/java/edu/wpi/grip/ui/pipeline/input/RangeInputSocketController.java @@ -13,7 +13,6 @@ import java.util.List; -import javafx.application.Platform; import javafx.fxml.FXML; import javafx.geometry.Pos; import javafx.scene.control.Label; From 51915a904e90b39887482d24c6b6fd801c09cdc5 Mon Sep 17 00:00:00 2001 From: Sam Carlberg Date: Thu, 16 Mar 2017 22:57:48 -0400 Subject: [PATCH 9/9] Temporarily disable GodClass check for MainWindowController This class does need a serious refactor, though --- ui/src/main/java/edu/wpi/grip/ui/MainWindowController.java | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/src/main/java/edu/wpi/grip/ui/MainWindowController.java b/ui/src/main/java/edu/wpi/grip/ui/MainWindowController.java index cec8ca26d0..dd4dcbaa4a 100644 --- a/ui/src/main/java/edu/wpi/grip/ui/MainWindowController.java +++ b/ui/src/main/java/edu/wpi/grip/ui/MainWindowController.java @@ -64,6 +64,7 @@ /** * The Controller for the application window. */ +@SuppressWarnings("PMD.GodClass") // temporary public class MainWindowController { @FXML